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 secrets
  • database voor dynamische database credentials
  • pki voor certificaat generatie

Auth Methods — Hoe clients hun identiteit bewijzen

  • kubernetes voor pods
  • token voor directe toegang
  • userpass, ldap, oidc voor 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:

  1. Voegt een init container toe die authenticeert met Vault
  2. Schrijft secrets naar /vault/secrets/config
  3. 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:

  1. Installeer Vault naast je bestaande KMS
  2. Sync bestaande secrets naar Vault
  3. Update apps één voor één om Vault te gebruiken
  4. 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.