Das Secrets-Problem in Kubernetes
Jede Anwendung braucht Secrets – Datenbank-Passwörter, API-Keys, TLS-Zertifikate, Encryption-Keys. In Kubernetes besteht der Default-Ansatz darin, diese in Kubernetes Secrets abzulegen, die base64-kodiert (nicht verschlüsselt) und für jeden mit RBAC-Zugang zum Namespace zugänglich sind.
Das ist keine Sicherheit. Das ist Sicherheitstheater.
Gängige Anti-Patterns, die wir in Produktionsumgebungen sehen:
- Secrets fest einkodiert in Container-Images – in jedem Registry und Build-Log sichtbar
- Secrets in Environment-Variablen – sichtbar in der Ausgabe von
kubectl describe pod - Secrets in Git eingecheckt – selbst „private" Repos werden kompromittiert
- Gemeinsam genutzte statische Credentials – keine Rotation, kein Audit Trail, kein Ablauf
HashiCorp Vault löst diese Probleme durch zentralisiertes Secrets-Management mit dynamischer Credential-Generierung, automatischer Rotation, detailliertem Audit-Logging und feingranularer Zugriffssteuerung.
Schritt 1: Vault auf Kubernetes mit Helm deployen
Der empfohlene Ansatz ist, Vault im Hochverfügbarkeitsmodus mit integriertem Raft-Storage zu betreiben:
# Add the HashiCorp Helm repository
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
# Create a dedicated namespace
kubectl create namespace vault
# Install Vault with HA configuration
helm install vault hashicorp/vault \
--namespace vault \
--set server.ha.enabled=true \
--set server.ha.replicas=3 \
--set server.ha.raft.enabled=true \
--set server.dataStorage.size=10Gi \
--set server.dataStorage.storageClass=gp3 \
--set server.auditStorage.enabled=true \
--set server.auditStorage.size=10Gi \
--set injector.enabled=true \
--set ui.enabled=true
Nach dem Deployment initialisieren und unsealen Sie den ersten Vault-Pod:
# Initialize Vault with 5 key shares, 3 required to unseal
kubectl exec -n vault vault-0 -- vault operator init \
-key-shares=5 \
-key-threshold=3 \
-format=json > vault-init.json
# Unseal the first pod (repeat with 3 different keys)
kubectl exec -n vault vault-0 -- vault operator unseal <key-1>
kubectl exec -n vault vault-0 -- vault operator unseal <key-2>
kubectl exec -n vault vault-0 -- vault operator unseal <key-3>
# Join the other pods to the Raft cluster
kubectl exec -n vault vault-1 -- vault operator raft join \
http://vault-0.vault-internal:8200
kubectl exec -n vault vault-2 -- vault operator raft join \
http://vault-0.vault-internal:8200
# Unseal the remaining pods
# (In production, use auto-unseal with AWS KMS, GCP KMS, or Azure Key Vault)
Für produktive Deployments konfigurieren Sie immer Auto-Unseal, um manuelle Eingriffe nach Pod-Neustarts zu vermeiden:
# vault-config.hcl
seal "awskms" {
region = "us-east-1"
kms_key_id = "alias/vault-unseal-key"
}
Schritt 2: Kubernetes-Authentifizierung konfigurieren
Vault muss verifizieren können, dass Pods, die Secrets anfordern, auch die sind, die sie vorgeben zu sein. Die Kubernetes-Auth-Methode nutzt Service-Account-Tokens zur Authentifizierung:
# Login to Vault
export VAULT_ADDR="http://127.0.0.1:8200"
vault login <root-token>
# Enable Kubernetes auth method
vault auth enable kubernetes
# Configure it to communicate with the Kubernetes API
vault write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc:443"
Erstellen Sie eine Policy, die definiert, auf welche Secrets ein Service zugreifen darf:
# payment-service-policy.hcl
path "secret/data/payment-service/*" {
capabilities = ["read"]
}
path "database/creds/payment-service-role" {
capabilities = ["read"]
}
path "pki/issue/payment-service" {
capabilities = ["create", "update"]
}
Wenden Sie die Policy an und binden Sie sie an einen Kubernetes-Service-Account:
# Write the policy
vault policy write payment-service payment-service-policy.hcl
# Create a role that maps Kubernetes SA to Vault policy
vault write auth/kubernetes/role/payment-service \
bound_service_account_names=payment-service \
bound_service_account_namespaces=production \
policies=payment-service \
ttl=1h
Schritt 3: Dynamische Datenbank-Credentials umsetzen
Statische Datenbank-Passwörter sind ein Risiko – sie laufen nie ab, werden über Umgebungen hinweg geteilt und sind ohne Downtime kaum rotierbar. Vaults Database Secrets Engine generiert einzigartige, kurzlebige Credentials on demand.
# Enable the database secrets engine
vault secrets enable database
# Configure the PostgreSQL connection
vault write database/config/payments-db \
plugin_name=postgresql-database-plugin \
allowed_roles="payment-service-role" \
connection_url="postgresql://{{username}}:{{password}}@payments-db.production.svc:5432/payments?sslmode=require" \
username="vault_admin" \
password="initial-setup-password"
# Rotate the root password so only Vault knows it
vault write -force database/rotate-root/payments-db
# Create a role that generates credentials with a 1-hour TTL
vault write database/roles/payment-service-role \
db_name=payments-db \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
revocation_statements="REVOKE ALL ON ALL TABLES IN SCHEMA public FROM \"{{name}}\"; DROP ROLE IF EXISTS \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"
Jeder Pod-Start bekommt nun ein einzigartiges Datenbank-Credential, das automatisch abläuft. Wenn ein Credential kompromittiert wird, ist der Blast-Radius auf einen Pod und eine Stunde beschränkt.
Schritt 4: Secrets mit dem Vault Agent in Pods injizieren
Der Vault Agent Injector nutzt Kubernetes Mutating Webhooks, um Secrets automatisch über Annotationen in Pods zu injizieren. Keine Änderungen am Anwendungscode erforderlich.
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: payment-service
template:
metadata:
labels:
app: payment-service
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "payment-service"
# Static secrets
vault.hashicorp.com/agent-inject-secret-config: "secret/data/payment-service/config"
vault.hashicorp.com/agent-inject-template-config: |
{{- with secret "secret/data/payment-service/config" -}}
export STRIPE_API_KEY="{{ .Data.data.stripe_api_key }}"
export WEBHOOK_SECRET="{{ .Data.data.webhook_secret }}"
{{- end }}
# Dynamic database credentials
vault.hashicorp.com/agent-inject-secret-db: "database/creds/payment-service-role"
vault.hashicorp.com/agent-inject-template-db: |
{{- with secret "database/creds/payment-service-role" -}}
export DB_USERNAME="{{ .Data.username }}"
export DB_PASSWORD="{{ .Data.password }}"
{{- end }}
# Auto-renew credentials before they expire
vault.hashicorp.com/agent-cache-enable: "true"
vault.hashicorp.com/agent-cache-listener-port: "8200"
spec:
serviceAccountName: payment-service
containers:
- name: payment-service
image: myorg/payment-service:v1.2.3
command: ["/bin/sh", "-c"]
args:
- source /vault/secrets/config &&
source /vault/secrets/db &&
/app/payment-service
ports:
- containerPort: 8080
resources:
limits:
cpu: 500m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
Der Vault-Agent-Sidecar kümmert sich um Authentifizierung, Secret-Abruf und automatische Erneuerung – Ihre Anwendung liest einfach Dateien oder Environment-Variablen.
Schritt 5: Alternative mit dem External Secrets Operator
Für Teams, die einen Kubernetes-nativen Workflow bevorzugen, synchronisiert der External Secrets Operator (ESO) Vault-Secrets automatisch in Kubernetes Secrets:
# secret-store.yaml
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: vault-backend
spec:
provider:
vault:
server: "http://vault.vault.svc:8200"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "external-secrets"
serviceAccountRef:
name: external-secrets
namespace: external-secrets
---
# external-secret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: payment-service-secrets
namespace: production
spec:
refreshInterval: 5m
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: payment-service-secrets
creationPolicy: Owner
data:
- secretKey: stripe-api-key
remoteRef:
key: secret/data/payment-service/config
property: stripe_api_key
- secretKey: webhook-secret
remoteRef:
key: secret/data/payment-service/config
property: webhook_secret
Dieser Ansatz eignet sich gut, wenn Ihre Anwendungen bereits aus Kubernetes Secrets lesen und Sie Vault als Quelle der Wahrheit einführen wollen, ohne Anwendungscode zu ändern.
Schritt 6: Audit-Logging aktivieren
In regulierten Umgebungen müssen Sie nachweisen können, wer wann auf welche Secrets zugegriffen hat. Aktivieren Sie Vaults Audit-Log:
# Enable file-based audit logging
vault audit enable file file_path=/vault/audit/audit.log
# For production, ship logs to your SIEM
# Example: enable syslog backend
vault audit enable syslog tag="vault" facility="AUTH"
Jede Vault-Operation – Reads, Writes, Authentifizierungsversuche, Policy-Änderungen – wird zusammen mit dem vollständigen Request und Response (sensible Werte HMAC-gehasht) aufgezeichnet. Leiten Sie diese Logs an Ihr SIEM (Splunk, Elastic oder Datadog) weiter, um auf verdächtige Zugriffsmuster zu alarmieren.
Checkliste Production-Hardening
Bevor Sie in Produktion gehen, verifizieren Sie diese Punkte:
- Auto-Unseal konfiguriert mit Cloud-KMS (niemals Unseal-Keys auf Disk speichern)
- TLS aktiviert für jede Vault-Kommunikation (mTLS bevorzugt)
- Audit-Logging aktiviert und an SIEM weitergeleitet
- Root-Token widerrufen nach initialem Setup (Identity-basierte Auth nutzen)
- Namespaces konfiguriert, um Teams zu isolieren (Vault Enterprise)
- Disaster-Recovery-Replikation über Regionen hinweg konfiguriert
- Backup und Restore getestet mit Raft-Snapshots
- Sentinel-Policies, die organisatorische Regeln erzwingen (Vault Enterprise)
- Resource Limits gesetzt auf Vault-Pods, um Noisy-Neighbor-Probleme zu vermeiden
- Network Policies, die einschränken, welche Pods Vault erreichen dürfen
# Take a Raft snapshot for backup
vault operator raft snapshot save /tmp/vault-backup.snap
# Verify the snapshot
vault operator raft snapshot inspect /tmp/vault-backup.snap
# Automate daily backups with a CronJob
# vault-backup-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: vault-backup
namespace: vault
spec:
schedule: "0 2 * * *"
jobTemplate:
spec:
template:
spec:
serviceAccountName: vault-backup
containers:
- name: backup
image: hashicorp/vault:1.15
command:
- /bin/sh
- -c
- |
vault operator raft snapshot save /tmp/vault-$(date +%Y%m%d).snap
aws s3 cp /tmp/vault-$(date +%Y%m%d).snap \
s3://my-vault-backups/$(date +%Y%m%d).snap
env:
- name: VAULT_ADDR
value: "http://vault-active.vault.svc:8200"
- name: VAULT_TOKEN
valueFrom:
secretKeyRef:
name: vault-backup-token
key: token
restartPolicy: OnFailure
Fazit
Secrets-Management ist nicht optional – es ist eine fundamentale Sicherheitsanforderung, die sich nur schwerer nachrüsten lässt, je größer Ihre Infrastruktur wird. HashiCorp Vault, sauber auf Kubernetes deployt, bietet dynamische Secrets, automatische Rotation, feingranulare Zugriffskontrolle und umfassendes Audit-Logging – Dinge, die statische Secrets schlicht nicht leisten können.
Bei DevOpsVibe haben wir Vault über Dutzende von Kubernetes-Clustern hinweg deployt und betrieben – für Organisationen vom Start-up bis zum Enterprise. Ob Sie Hilfe beim initialen Deployment, bei der Migration von statischen Secrets oder bei der Integration in Ihre bestehenden CI/CD-Pipelines benötigen – unser Team bringt Sie in Wochen, nicht Monaten, zu produktivem Secrets-Management. Sichern Sie Ihre Infrastruktur noch heute.