Kubernetes Secrets zijn geen secrets. Het zijn base64-gecodeerde platte tekst, opgeslagen in etcd, vaak zichtbaar voor iedereen met cluster toegang. Dit is de default, en het is beangstigend.
Elke cloud provider biedt een Key Management Service. AWS heeft Secrets Manager, Google heeft Secret Manager, Azure heeft Key Vault. Ze werken prima — totdat je moet migreren, of je wilt begrijpen wat er met je secrets gebeurt, of je simpelweg je meest gevoelige data niet in andermans infrastructuur wilt.
HashiCorp Vault is het self-hosted alternatief. Jij draait het, jij controleert het, jij begrijpt het.
Waarom Self-Hosted Secrets?
Cloud KMS gebruiken betekent:
- Je secrets bestaan in een systeem dat je niet controleert
- Je kunt niet auditen wat er intern gebeurt
- Migratie wordt een nachtmerrie (secrets zijn de meest plakkerige lock-in)
- Je betaalt per secret, per request
Self-hosted Vault betekent:
- Volledige zichtbaarheid in hoe secrets worden opgeslagen en benaderd
- Geen vendor lock-in voor je meest kritieke data
- Werkt identiek over clouds, on-prem, en edge
- Je secrets verlaten nooit infrastructuur die je controleert
Dit is soevereiniteit. Niet omdat cloud KMS slecht is, maar omdat secrets te belangrijk zijn om niet volledig te begrijpen.
Vault Architectuur Basis
Vault heeft drie kernconcepten:
Secrets Engines — Waar secrets worden opgeslagen
kv(key-value) voor statische secretsdatabasevoor dynamische database credentialspkivoor certificaat generatie
Auth Methods — Hoe clients hun identiteit bewijzen
kubernetesvoor podstokenvoor directe toeganguserpass,ldap,oidcvoor mensen
Policies — Wat geauthenticeerde clients kunnen benaderen
flowchart TD
subgraph vault["Vault"]
subgraph auth["Auth Methods"]
K8S["kubernetes"]
USER["userpass"]
end
K8S --> POL["Policies"]
USER --> POL
subgraph engines["Secrets Engines"]
KV["KV"]
PKI["PKI"]
DB["DB"]
end
POL --> KV
POL --> PKI
POL --> DB
end
Vault Installeren op Kubernetes
Met de officiële Helm chart:
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
helm install vault hashicorp/vault \
--namespace vault \
--create-namespace \
--set server.ha.enabled=true \
--set server.ha.replicas=3
Voor GitOps met ArgoCD:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: vault
namespace: argocd
spec:
project: default
source:
repoURL: https://helm.releases.hashicorp.com
chart: vault
targetRevision: 0.27.0
helm:
values: |
server:
ha:
enabled: true
replicas: 3
raft:
enabled: true
dataStorage:
size: 10Gi
storageClass: longhorn
injector:
enabled: true
destination:
server: https://kubernetes.default.svc
namespace: vault
syncPolicy:
automated:
prune: true
selfHeal: true
Initialiseren en Unsealen
Vault start sealed. Initialiseer het:
kubectl exec -it vault-0 -n vault -- vault operator init
Dit output:
- 5 unseal keys (bewaar deze veilig!)
- Initiële root token
Bewaar deze veilig. Als je de unseal keys verliest, verlies je toegang tot alle secrets.
Unseal elke Vault pod (heb 3 van 5 keys nodig):
kubectl exec -it vault-0 -n vault -- vault operator unseal <key1>
kubectl exec -it vault-0 -n vault -- vault operator unseal <key2>
kubectl exec -it vault-0 -n vault -- vault operator unseal <key3>
Herhaal voor vault-1 en vault-2.
Auto-Unseal
Handmatig unsealen schaalt niet. Voor productie, gebruik auto-unseal met een KMS of transit key:
# Andere Vault gebruiken voor transit auto-unseal
server:
ha:
enabled: true
config: |
seal "transit" {
address = "https://vault-primary.example.com:8200"
disable_renewal = "false"
key_name = "autounseal"
mount_path = "transit/"
tls_skip_verify = "false"
}
Of met cloud KMS (ja, voor dit ene ding is de trade-off acceptabel):
server:
config: |
seal "awskms" {
region = "eu-west-1"
kms_key_id = "alias/vault-unseal"
}
KV Secrets Engine Enablen
# Login met root token
kubectl exec -it vault-0 -n vault -- vault login
# Enable KV v2 secrets engine
kubectl exec -it vault-0 -n vault -- vault secrets enable -path=secret kv-v2
# Maak een secret
kubectl exec -it vault-0 -n vault -- vault kv put secret/myapp/config \
database_url="postgres://user:pass@db:5432/myapp" \
api_key="super-secret-key"
Kubernetes Authenticatie
Enable pods om te authenticeren met Vault:
# Enable Kubernetes auth
kubectl exec -it vault-0 -n vault -- vault auth enable kubernetes
# Configureer het
kubectl exec -it vault-0 -n vault -- vault write auth/kubernetes/config \
kubernetes_host="https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT"
Maak een role voor je applicatie:
kubectl exec -it vault-0 -n vault -- vault write auth/kubernetes/role/myapp \
bound_service_account_names=myapp \
bound_service_account_namespaces=default \
policies=myapp-policy \
ttl=1h
Policies Maken
Policies definiëren wat geauthenticeerde clients kunnen benaderen:
# myapp-policy.hcl
path "secret/data/myapp/*" {
capabilities = ["read"]
}
path "secret/metadata/myapp/*" {
capabilities = ["list"]
}
Pas het toe:
kubectl exec -it vault-0 -n vault -- vault policy write myapp-policy - <<EOF
path "secret/data/myapp/*" {
capabilities = ["read"]
}
path "secret/metadata/myapp/*" {
capabilities = ["list"]
}
EOF
Secrets Injecteren in Pods
De Vault Agent Injector injecteert automatisch secrets in pods via annotations:
apiVersion: v1
kind: ServiceAccount
metadata:
name: myapp
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "myapp"
vault.hashicorp.com/agent-inject-secret-config: "secret/data/myapp/config"
vault.hashicorp.com/agent-inject-template-config: |
{{- with secret "secret/data/myapp/config" -}}
export DATABASE_URL="{{ .Data.data.database_url }}"
export API_KEY="{{ .Data.data.api_key }}"
{{- end }}
spec:
serviceAccountName: myapp
containers:
- name: app
image: myapp:v1.0.0
command: ["/bin/sh", "-c"]
args:
- source /vault/secrets/config && ./myapp
De injector:
- Voegt een init container toe die authenticeert met Vault
- Schrijft secrets naar
/vault/secrets/config - Je app leest ze bij startup
External Secrets Operator Alternatief
De Vault Injector werkt goed maar heeft beperkingen. External Secrets Operator is een alternatief dat Vault secrets synct naar native Kubernetes Secrets:
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
spec:
provider:
vault:
server: "http://vault.vault:8200"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "myapp"
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: myapp-secrets
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: myapp-secrets
creationPolicy: Owner
data:
- secretKey: DATABASE_URL
remoteRef:
key: myapp/config
property: database_url
- secretKey: API_KEY
remoteRef:
key: myapp/config
property: api_key
Dit maakt een normale Kubernetes Secret die je pods normaal kunnen gebruiken:
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: app
envFrom:
- secretRef:
name: myapp-secrets
Dynamische Database Credentials
Statische secrets zijn prima, maar dynamische secrets zijn beter. Vault kan unieke database credentials genereren per pod:
# Enable database secrets engine
kubectl exec -it vault-0 -n vault -- vault secrets enable database
# Configureer PostgreSQL connectie
kubectl exec -it vault-0 -n vault -- vault write database/config/mydb \
plugin_name=postgresql-database-plugin \
connection_url="postgresql://{{username}}:{{password}}@postgres.default:5432/myapp" \
allowed_roles="myapp-db" \
username="vault_admin" \
password="vault_admin_password"
# Maak role die credentials genereert
kubectl exec -it vault-0 -n vault -- vault write database/roles/myapp-db \
db_name=mydb \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"
Nu kunnen pods unieke credentials aanvragen:
annotations:
vault.hashicorp.com/agent-inject-secret-db: "database/creds/myapp-db"
vault.hashicorp.com/agent-inject-template-db: |
{{- with secret "database/creds/myapp-db" -}}
export PGUSER="{{ .Data.username }}"
export PGPASSWORD="{{ .Data.password }}"
{{- end }}
Elke pod krijgt unieke credentials die:
- Auto-roteren
- Individueel kunnen worden ingetrokken
- Een audit trail creëren per pod
PKI: Certificate Authority
Vault kan je interne CA zijn:
# Enable PKI
kubectl exec -it vault-0 -n vault -- vault secrets enable pki
# Configureer max TTL
kubectl exec -it vault-0 -n vault -- vault secrets tune -max-lease-ttl=87600h pki
# Genereer root CA
kubectl exec -it vault-0 -n vault -- vault write -field=certificate pki/root/generate/internal \
common_name="My Org Root CA" \
ttl=87600h > CA_cert.crt
# Maak role voor uitgifte certs
kubectl exec -it vault-0 -n vault -- vault write pki/roles/internal-certs \
allowed_domains="internal,svc.cluster.local" \
allow_subdomains=true \
max_ttl=72h
Integreer met cert-manager voor automatisch certificaatbeheer.
High Availability
Voor productie, draai Vault in HA mode met Raft storage:
server:
ha:
enabled: true
replicas: 3
raft:
enabled: true
setNodeId: true
config: |
cluster_name = "vault-cluster"
storage "raft" {
path = "/vault/data"
retry_join {
leader_api_addr = "http://vault-0.vault-internal:8200"
}
retry_join {
leader_api_addr = "http://vault-1.vault-internal:8200"
}
retry_join {
leader_api_addr = "http://vault-2.vault-internal:8200"
}
}
Dit creëert een 3-node Raft cluster:
- Eén leader, twee followers
- Automatische leader election bij failure
- Data gerepliceerd over alle nodes
Backup Strategie
Vault data is kritiek. Back het up:
# Maak snapshot
kubectl exec -it vault-0 -n vault -- vault operator raft snapshot save /tmp/vault-snapshot.snap
# Kopieer naar lokaal
kubectl cp vault/vault-0:/tmp/vault-snapshot.snap ./vault-snapshot.snap
Automatiseer met een CronJob:
apiVersion: batch/v1
kind: CronJob
metadata:
name: vault-backup
spec:
schedule: "0 */6 * * *" # Elke 6 uur
jobTemplate:
spec:
template:
spec:
serviceAccountName: vault-backup
containers:
- name: backup
image: hashicorp/vault:1.15
command:
- /bin/sh
- -c
- |
vault operator raft snapshot save /backup/vault-$(date +%Y%m%d-%H%M%S).snap
volumeMounts:
- name: backup
mountPath: /backup
volumes:
- name: backup
persistentVolumeClaim:
claimName: vault-backup
restartPolicy: OnFailure
Audit Logging
Enable audit logging om alle toegang te tracken:
kubectl exec -it vault-0 -n vault -- vault audit enable file file_path=/vault/audit/audit.log
Elke secret toegang wordt gelogd:
{
"time": "2025-07-02T10:00:00Z",
"type": "response",
"auth": {
"token_type": "service",
"policies": ["myapp-policy"]
},
"request": {
"path": "secret/data/myapp/config",
"operation": "read"
}
}
Mijn Productie Setup
Hier is mijn daadwerkelijke Vault configuratie:
server:
ha:
enabled: true
replicas: 3
raft:
enabled: true
dataStorage:
size: 10Gi
storageClass: longhorn
auditStorage:
enabled: true
size: 10Gi
storageClass: longhorn
resources:
requests:
memory: 256Mi
cpu: 250m
limits:
memory: 512Mi
cpu: 500m
extraEnvironmentVars:
VAULT_CACERT: /vault/userconfig/vault-tls/ca.crt
injector:
enabled: true
resources:
requests:
memory: 64Mi
cpu: 50m
limits:
memory: 128Mi
cpu: 100m
ui:
enabled: true
Belangrijke beslissingen:
- HA met Raft — Geen externe storage afhankelijkheid
- Audit storage — Compliance en debugging
- Resource limits — Vault is lightweight
- UI enabled — Voor occasionele handmatige operaties
Migratie van Cloud KMS
Al AWS Secrets Manager of vergelijkbaar in gebruik? Migreer geleidelijk:
- Installeer Vault naast je bestaande KMS
- Sync bestaande secrets naar Vault
- Update apps één voor één om Vault te gebruiken
- Decommissionneer cloud KMS wanneer leeg
Gebruik External Secrets Operator om van beide te lezen tijdens transitie:
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: aws-secrets-manager
spec:
provider:
aws:
service: SecretsManager
region: eu-west-1
---
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: vault
spec:
provider:
vault:
server: "http://vault.vault:8200"
Waarom Dit Ertoe Doet
Secrets zijn de sleutels tot je koninkrijk. Ze verdienen infrastructuur die je begrijpt en controleert.
Self-hosted Vault geeft je:
- Zichtbaarheid — Zie precies hoe secrets worden opgeslagen en benaderd
- Portability — Hetzelfde secrets management overal
- Controle — Geen verrassende pricing, geen vendor beslissingen die je raken
- Begrip — Wanneer iets kapot gaat, kun je het fixen
De operationele overhead is echt maar beheersbaar. De rust van het controleren van je meest gevoelige data? Onbetaalbaar.
Je secrets zijn te belangrijk om andermans probleem te zijn. Self-hosted Vault geeft jou de controle over je meest kritieke infrastructuur.
