Veröffentlicht am

Kubernetes Secrets mit HashiCorp Vault verwalten

Teilen:
Authors

Kubernetes Secrets Management mit HashiCorp Vault: Praxisanleitung

TL;DR

  • Native Kubernetes Secrets sind Base64-kodiert, nicht verschluesselt -- jeder mit etcd-Zugriff kann sie lesen.
  • HashiCorp Vault bietet Verschluesselung, Zugriffskontrolle, Audit-Logging und automatische Rotation aus einer Hand.
  • Der External Secrets Operator (ESO) synchronisiert Vault-Secrets als native Kubernetes Secrets -- die einfachste Integrationsmethode fuer bestehende Workloads.
  • Alternativ mountet der Secrets Store CSI Driver Secrets direkt ins Pod-Filesystem, ohne sie in etcd zu persistieren.
  • Dynamische Secrets (z.B. kurzlebige Datenbank-Credentials) reduzieren die Angriffsoberflaeche drastisch.

Warum native Kubernetes Secrets nicht ausreichen

Kubernetes Secrets sehen auf den ersten Blick nach einer Loesung aus. Sie sind in der API integriert, lassen sich mit kubectl verwalten und koennen als Environment-Variablen oder Volumes in Pods gemountet werden.

Das Problem: Die Daten sind nur Base64-kodiert. Das ist keine Verschluesselung. Ein einfaches echo "cGFzc3dvcmQ=" | base64 -d liefert das Klartext-Passwort. Jeder mit Lesezugriff auf das etcd-Backend oder die Kubernetes API (und den richtigen RBAC-Berechtigungen) sieht alle Secrets.

Dazu fehlen native Secrets wesentliche Features:

FeatureKubernetes SecretsHashiCorp Vault
Verschluesselung at restNur mit EncryptionConfigStandardmaessig (AES-256-GCM)
Automatische RotationNeinJa (Leases, dynamische Secrets)
Audit-LoggingNur ueber K8s Audit LogsDetailliertes internes Audit Log
Dynamische SecretsNeinJa (DB, AWS, Azure, GCP)
Feingranulare PoliciesRBAC (Namespace/Resource-Level)Pfad-basierte Policies pro Identity
Zentrale VerwaltungPro ClusterCluster-uebergreifend

Architektur: Vault mit Kubernetes verbinden

Es gibt drei gaengige Wege, Vault-Secrets in Kubernetes-Pods zu bringen. Jeder hat seine Staerken:

Option 1: External Secrets Operator (ESO)

Der ESO ist ein Kubernetes-Controller, der Secrets aus Vault abruft und als native Kubernetes Secrets im Cluster erstellt. Bestehende Workloads muessen nicht angepasst werden -- sie lesen weiterhin ein normales Kubernetes Secret.

Vorteile: Einfachste Migration, keine Aenderung an Deployments noetig. Nachteile: Secrets werden in etcd persistiert (wenn auch verschluesselt durch Vault).

Option 2: Secrets Store CSI Driver

Der CSI Driver mountet Secrets direkt als Dateien ins Pod-Filesystem. Die Secrets werden nie als Kubernetes-Objekt in etcd gespeichert.

Vorteile: Secrets existieren nur im Pod-Filesystem. Nachteile: Jedes Deployment braucht Volume-Konfiguration.

Option 3: Vault Agent Injector

Ein Mutating Webhook injiziert einen Vault Agent Sidecar in jeden Pod. Der Agent authentifiziert sich bei Vault und schreibt Secrets in ein Shared Volume.

Vorteile: Unterstuetzt Template-Rendering fuer komplexe Formate. Nachteile: Sidecar erhoet den Ressourcenverbrauch.

Vergleich der drei Optionen

KriteriumESOCSI DriverVault Agent
Secrets in etcdJaNeinNein
Deployment-Aenderung noetigNein (nur ExternalSecret CR)Ja (Volume Mount)Nein (Annotations)
Ressourcen-OverheadController (1 Pod)DaemonSetSidecar pro Pod
Template-RenderingNeinNeinJa
RotationUeber refreshIntervalUeber rotation configUeber Agent
KomplexitaetNiedrigMittelMittel-Hoch

Fuer die meisten Teams ist der ESO der beste Einstieg. Er erfordert die wenigsten Aenderungen an bestehenden Workloads.

Schritt fuer Schritt: Vault mit ESO einrichten

1. Vault deployen

Vault laesst sich per Helm im Cluster oder auf einer dedizierten VM betreiben. Fuer Produktion empfiehlt sich ein separater Cluster oder dedizierte VMs mit Raft Storage.

# Vault Helm Chart installieren (Dev-Modus fuer Tests)
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update

helm install vault hashicorp/vault \
  --namespace vault \
  --create-namespace \
  --set "server.dev.enabled=true" \
  --set "injector.enabled=false"

# Fuer Produktion statt dev mode:
# --set "server.ha.enabled=true"
# --set "server.ha.replicas=3"
# --set "server.ha.raft.enabled=true"

2. Kubernetes Auth Method konfigurieren

Die Kubernetes Auth Method erlaubt es Pods, sich mit ihrem ServiceAccount-Token bei Vault zu authentifizieren.

# Vault Shell oeffnen
kubectl exec -it vault-0 -n vault -- /bin/sh

# Auth Method aktivieren
vault auth enable kubernetes

# Kubernetes API Verbindung konfigurieren
vault write auth/kubernetes/config \
  kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"

# KV Secrets Engine aktivieren
vault secrets enable -path=kv kv-v2

# Test-Secret anlegen
vault kv put kv/myapp/db \
  username="app_user" \
  password="s3cur3-p4ssw0rd"

# Policy erstellen
vault policy write myapp-policy - <<EOF
path "kv/data/myapp/*" {
  capabilities = ["read"]
}
EOF

# Kubernetes Role erstellen
vault write auth/kubernetes/role/myapp-role \
  bound_service_account_names=myapp-sa \
  bound_service_account_namespaces=default \
  policies=myapp-policy \
  ttl=1h

3. External Secrets Operator installieren

helm repo add external-secrets https://charts.external-secrets.io
helm repo update

helm install external-secrets external-secrets/external-secrets \
  --namespace external-secrets \
  --create-namespace

4. SecretStore und ExternalSecret erstellen

Zuerst der SecretStore, der die Verbindung zu Vault definiert:

# secret-store.yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: vault-store
  namespace: default
spec:
  provider:
    vault:
      server: "http://vault.vault.svc.cluster.local:8200"
      path: "kv"
      version: "v2"
      auth:
        kubernetes:
          mountPath: "kubernetes"
          role: "myapp-role"
          serviceAccountRef:
            name: "myapp-sa"

Dann das ExternalSecret, das ein konkretes Secret aus Vault abruft:

# external-secret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: myapp-db-credentials
  namespace: default
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: vault-store
    kind: SecretStore
  target:
    name: myapp-db-secret
    creationPolicy: Owner
  data:
    - secretKey: DB_USERNAME
      remoteRef:
        key: kv/data/myapp/db
        property: username
    - secretKey: DB_PASSWORD
      remoteRef:
        key: kv/data/myapp/db
        property: password

Das Deployment referenziert dann das generierte Kubernetes Secret:

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      serviceAccountName: myapp-sa
      containers:
        - name: myapp
          image: myapp:latest
          envFrom:
            - secretRef:
                name: myapp-db-secret

5. Pruefen, ob alles funktioniert

# ExternalSecret-Status pruefen
kubectl get externalsecret myapp-db-credentials

# Das generierte Kubernetes Secret ansehen
kubectl get secret myapp-db-secret -o jsonpath='{.data.DB_USERNAME}' | base64 -d

# ESO-Logs bei Problemen
kubectl logs -n external-secrets -l app.kubernetes.io/name=external-secrets

Dynamische Secrets: Kurzlebige Datenbank-Credentials

Statische Secrets (Username + Passwort in Vault gespeichert) sind besser als Klartext in etcd, aber der eigentliche Vorteil von Vault liegt in dynamischen Secrets.

Dynamische Secrets werden bei jeder Anfrage neu generiert und haben eine begrenzte Lebensdauer. Wenn ein Pod ein Datenbank-Credential anfordert, erstellt Vault einen temporaeren User in der Datenbank, der nach Ablauf der Lease automatisch geloescht wird.

# PostgreSQL Secrets Engine aktivieren
vault secrets enable database

# Datenbankverbindung konfigurieren
vault write database/config/mydb \
  plugin_name=postgresql-database-plugin \
  allowed_roles="myapp-db-role" \
  connection_url="postgresql://{{username}}:{{password}}@postgres.default.svc:5432/mydb?sslmode=disable" \
  username="vault_admin" \
  password="vault_admin_password"

# Role fuer dynamische Credentials
vault write database/roles/myapp-db-role \
  db_name=mydb \
  creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
  default_ttl="1h" \
  max_ttl="24h"

Jetzt erstellt Vault bei jedem Abruf einen neuen Datenbank-User mit einer Stunde Gueltigkeitsdauer. Selbst wenn ein Credential leakt, ist es nach einer Stunde wertlos.

Haeufige Fehler und wie man sie vermeidet

Vault im Dev-Modus in Produktion. Dev-Modus speichert alles im RAM, keine Persistenz. Nach einem Restart sind alle Secrets weg. Fuer Produktion immer Raft oder Consul als Storage Backend nutzen.

Zu breite Vault Policies. path "secret/*" { capabilities = ["read"] } gibt jedem authentifizierten Pod Zugriff auf alle Secrets. Policies sollten so eng wie moeglich sein: ein Pfad pro Anwendung.

Unseal-Keys nicht sicher verwahrt. Bei einer produktiven Vault-Installation muessen die Unseal-Keys getrennt und sicher aufbewahrt werden. Am besten: Auto-Unseal ueber einen Cloud KMS (AWS KMS, Azure Key Vault, GCP Cloud KMS).

refreshInterval zu kurz gesetzt. Ein refreshInterval: 10s im ExternalSecret erzeugt extrem viele Vault-Anfragen. Fuer statische Secrets reicht 1h. Fuer dynamische Secrets orientiert man sich an der Lease-Dauer.

ServiceAccount-Token nicht korrekt konfiguriert. Die haeufigste Fehlerquelle bei der Kubernetes Auth Method. Seit Kubernetes 1.24 werden keine langlebigen SA-Tokens mehr automatisch erstellt. Der ESO erstellt kurzlebige TokenRequests -- dafuer muss der ServiceAccount existieren und die Vault-Role korrekt gebunden sein.

Migration bestehender Secrets

Eine Migration von nativen Kubernetes Secrets zu Vault sollte schrittweise erfolgen:

  1. Inventar erstellen: Alle Secrets im Cluster auflisten (kubectl get secrets --all-namespaces -o json)
  2. Priorisieren: Datenbank-Credentials und API-Keys zuerst, weniger kritische Secrets spaeter
  3. In Vault anlegen: Secrets in die KV Engine schreiben
  4. ExternalSecret erstellen: CR anlegen, der das gleiche Kubernetes Secret erzeugt (gleicher Name, gleiche Keys)
  5. Altes Secret loeschen: Erst wenn das ExternalSecret den Status SecretSynced hat
  6. Testen: Pods neu starten und pruefen, ob die Anwendung korrekt funktioniert

Weiterfuehrende Themen


Vault richtig aufzusetzen erfordert Erfahrung -- besonders bei HA-Konfigurationen, Auto-Unseal und der Migration bestehender Secrets. Wenn Sie dabei Unterstuetzung brauchen, erreichen Sie uns unter /kontakt.

Kubernetes-Beratung gesucht?

Wir helfen deutschen Unternehmen bei der Kubernetes-Implementierung, Migration und Optimierung. DSGVO-konform und praxiserprobt.

📖 Verwandte Artikel

Weitere interessante Beiträge zu ähnlichen Themen