DevOps GitOps Secret-Management: 6 Produktionsmuster von Sealed Secrets bis External Secrets Operator
Secrets in Git committen? Sie sitzen auf einer Zeitbombe
GitOps dreht sich um "Git als Single Source of Truth", aber Kubernetes Secrets sind lediglich Base64-kodiert — nicht verschlüsselt. Base64-kodierte Secrets in ein Git-Repository zu pushen ist gleichbedeutend damit, Datenbankpasswörter, API-Schlüssel und TLS-Zertifikate im Klartext zu speichern. Sobald das Repository geleakt wird (interne Fehlkonfiguration, Drittanbieter-Lieferkettenverletzung, vergessene Bereinigung in einem Fork), sind alle Secrets sofort offengelegt.
2026 ist DevOps GitOps Secret-Management nicht mehr optional — es ist eine harte Anforderung für Produktionsbereitstellungen. Dieser Artikel behandelt 6 produktionsreife Secret-Management-Muster, von verschlüsselter Speicherung mit Sealed Secrets bis zur dynamischen Injektion mit External Secrets Operator, und hilft Ihnen, die Secret-Sicherheit in GitOps-Workflows vollständig zu lösen.
Wichtigste Erkenntnisse
- 3 Architekturpattern für GitOps Secret-Management verstehen: verschlüsselte Speicherung, externe Injektion und Hybrid
- Vollständige Bereitstellung und Konfiguration von Sealed Secrets, External Secrets Operator und SOPS beherrschen
- Automatische Secret-Rotation und Multi-Cluster-Synchronisation implementieren
- 5 häufigste Produktionsfehler und 10 wiederkehrende Fehler vermeiden
- Eine Vergleichsentscheidungsmatrix zur Toolauswahl erhalten
Inhaltsverzeichnis
- GitOps Secret-Management Kernkonzepte
- Muster 1: Sealed Secrets Verschlüsselte Speicherung
- Muster 2: External Secrets Operator + Vault
- Muster 3: SOPS + Age/GPG Verschlüsselung
- Muster 4: Automatische Secret-Rotation
- Muster 5: Multi-Cluster Secret-Synchronisation
- Muster 6: Audit und Compliance
- 5 Häufige Fallstricke und Lösungen
- 10 Häufige Fehlerbehebung
- Erweiterte Optimierungstipps
- Vergleichsanalyse
- Empfohlene Online-Tools
GitOps Secret-Management Kernkonzepte
Drei Architekturpattern
┌─────────────────────────────────────────────────────────────┐
│ GitOps Secret-Management: 3 Pattern │
├─────────────────┬──────────────────┬────────────────────────┤
│ Verschlüsselte │ Externe │ Hybrid │
│ Speicherung │ Injektion │ (SOPS + ESO) │
│ (Sealed Secrets)│ (ESO + Vault) │ │
├─────────────────┼──────────────────┼────────────────────────┤
│ Git speichert │ Git speichert │ Git speichert │
│ Chiffretext │ nur Referenzen │ verschlüsselte Werte │
│ Cluster-interne │ Cluster holt │ + Referenzen │
│ Entschlüsselung │ Klartext │ Cluster-Entschlüsselung│
│ Offline-Audit │ Externe Abhän- │ oder Abruf │
│ Neu-verschlüsseln│ gigkeiten │ Flexible Kombination │
│ zum Rotieren │ Native Rotation │ Teilweise Rotation │
│ │ unterstützt │ unterstützt │
└─────────────────┴──────────────────┴────────────────────────┘
Das grundlegende Problem mit K8s Secrets
# Ein Standard-Secret erstellen
kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password='SuperSecret123!'
# Die "Verschlüsselung" ist nur Base64-Kodierung — jeder kann es dekodieren
kubectl get secret db-credentials -o yaml
# Dekodieren erfordert nur einen Befehl
kubectl get secret db-credentials \
-o jsonpath='{.data.password}' | base64 -d
# Ausgabe: SuperSecret123!
# Mit dem ToolsKu Base64-Kodierer/Dekodierer überprüfen
# https://toolsku.com/de/encode/base64
Kernprinzipien des DevOps GitOps Secret-Managements
| Prinzip | Beschreibung | Folge bei Verletzung |
|---|---|---|
| Null Klartext | Niemals Klartext-Secrets in Git speichern | Repository-Leck = vollständige Kompromittierung |
| Geringste Berechtigung | Jede App greift nur auf eigene Secrets zu | Laterale Secret-Leckage |
| Auto-Rotation | Secrets werden regelmäßig automatisch aktualisiert | Langlebige Schlüssel werden brutal erzwungen |
| Auditierbar | Jeder Secret-Zugriff wird protokolliert | Leck-Quelle nicht rückverfolgbar |
| Wiederherstellbar | Secrets können nach Verlust schnell wiederhergestellt werden | Geschäftsunterbrechung |
Muster 1: Sealed Secrets Verschlüsselte Speicherung
Sealed Secrets ist eine Open-Source-GitOps-Secret-Management-Lösung von Bitnami. Die Kernidee: Secrets lokal mit kubeseal verschlüsseln, SealedSecret-Ressourcen generieren, die in Git committet werden, und der cluster-interne Sealed Secrets Controller entschlüsselt sie automatisch zurück zu K8s Secrets.
Architektur
┌──────────────────────────────────────────────────────┐
│ Entwickler-Arbeitsstation │
│ ┌────────────┐ ┌────────────┐ │
│ │ secret.yaml │───▶│ kubeseal │ │
│ │ (Klartext, │ │ (Verschlüs- │ │
│ │ nicht │ │ selungs- │ │
│ │ committen) │ │ tool) │ │
│ └────────────┘ └─────┬──────┘ │
│ │ verschlüsseln │
│ ┌─────▼──────┐ │
│ │sealed-secret│ │
│ │.yaml(Chiffre)│ │
│ └─────┬──────┘ │
└──────────────────────────┼───────────────────────────┘
│ git push
┌──────────────────────────▼───────────────────────────┐
│ Git-Repository │
│ (speichert nur verschlüsselte SealedSecrets) │
└──────────────────────────┬───────────────────────────┘
│ git pull (ArgoCD/Flux)
┌──────────────────────────▼───────────────────────────┐
│ K8s-Cluster │
│ ┌──────────────────────────────┐ │
│ │ Sealed Secrets Controller │ │
│ │ (entschlüsselt mit privatem │ │
│ │ Schlüssel) │ │
│ └──────────────┬───────────────┘ │
│ │ entschlüsseln │
│ ┌─────▼──────┐ │
│ │ K8s Secret │ │
│ │ (Klartext, │ │
│ │ cluster- │ │
│ │ intern) │ │
│ └────────────┘ │
└───────────────────────────────────────────────────────┘
Sealed Secrets installieren
# Controller im Cluster installieren
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.27.0/controller.yaml
# Auf Bereitschaft des Controllers warten
kubectl wait --for=condition=available --timeout=120s \
deployment/sealed-secrets-controller -n kube-system
# kubeseal CLI installieren
KUBESEAL_VERSION=0.27.0
curl -sLO "https://github.com/bitnami-labs/sealed-secrets/releases/download/v${KUBESEAL_VERSION}/kubeseal-linux-amd64"
chmod +x kubeseal-linux-amd64
sudo mv kubeseal-linux-amd64 /usr/local/bin/kubeseal
# Installation überprüfen
kubeseal --version
Secrets verschlüsseln
# Aus Klartext-Secret-Datei verschlüsseln
kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password='SuperSecret123!' \
--dry-run=client -o yaml | \
kubeseal --format yaml > sealed-secret.yaml
# Aus Dateien erstellen
kubectl create secret generic tls-cert \
--from-file=tls.crt=server.crt \
--from-file=tls.key=server.key \
--dry-run=client -o yaml | \
kubeseal --format yaml > sealed-tls.yaml
# Namespace und Namen angeben (Strict-Modus)
kubectl create secret generic api-key \
--from-literal=key=abc123xyz \
--namespace production \
--dry-run=client -o yaml | \
kubeseal --format yaml \
--scope namespace-wide > sealed-api-key.yaml
SealedSecret-Ressourcenbeispiel
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: db-credentials
namespace: production
spec:
encryptedData:
username: AgBfj8k2mN3pQ7sT9vWxYz...
password: AgCdH5lM6nO8qR0tU2wXyZa...
template:
metadata:
name: db-credentials
namespace: production
type: Opaque
Verschlüsselungsbereich-Steuerung
# strict: nur mit gleichem Namespace und Namen entschlüsselbar (Standard)
kubeseal --scope strict
# namespace-wide: innerhalb desselben Namespace umbenennbar
kubeseal --scope namespace-wide
# cluster-wide: in jedem Namespace im Cluster verwendbar
kubeseal --scope cluster-wide
| Bereich | Sicherheitsstufe | Flexibilität | Anwendungsfall |
|---|---|---|---|
| strict | Höchste | Niedrigste | Produktions-Secrets |
| namespace-wide | Mittel | Mittel | Multi-App gleicher Namespace |
| cluster-wide | Niedrigste | Höchste | Geteilte Ressourcen wie Zertifikate |
Backup und Wiederherstellung des Hauptschlüssels
# Hauptschlüssel-Backup (alle SealedSecrets werden unentschlüsselbar, wenn verloren!)
kubectl get secret -n kube-system \
sealed-secrets-key -o yaml > sealed-secrets-key-backup.yaml
# Hauptschlüssel in neuem Cluster wiederherstellen
kubectl apply -f sealed-secrets-key-backup.yaml
# Hauptschlüssel rotieren
kubectl delete secret -n kube-system sealed-secrets-key
# Controller generiert automatisch einen neuen Schlüssel, alte SealedSecrets funktionieren weiterhin
Muster 2: External Secrets Operator + Vault
External Secrets Operator (ESO) integriert externe Secret-Management-Systeme (Vault, AWS Secrets Manager, GCP Secret Manager usw.) mit Kubernetes. Git-Repositories speichern nur SecretStore-Referenzen, und der cluster-interne ESO Controller ruft Secrets aus externen Systemen ab und erstellt K8s Secrets.
Architektur
┌──────────────────────────────────────────────────────┐
│ Git-Repository │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ SecretStore │ │ ExternalSecret│ │
│ │ (Vault-Ver- │ │ (Secret-Ref- │ │
│ │ bindungs- │ │ Mapping) │ │
│ │ konfig) │ │ │ │
│ └──────────────┘ └──────────────┘ │
└──────────────────────┬───────────────────────────────┘
│ git pull
┌──────────────────────▼───────────────────────────────┐
│ K8s-Cluster │
│ ┌──────────────────────────────┐ │
│ │ External Secrets Operator │ │
│ │ (überwacht ExternalSecrets) │ │
│ └──────────────┬───────────────┘ │
│ │ Secrets abrufen │
│ ┌────────────▼────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ K8s │ │ HashiCorp│ │
│ │ Secret │ │ Vault │ │
│ └──────────┘ └──────────┘ │
└───────────────────────────────────────────────────────┘
ESO installieren
# Mit Helm 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 \
--set installCRDs=true \
--set replicaCount=2 \
--set leaderElect=true
# Überprüfen
kubectl get pods -n external-secrets
HashiCorp Vault SecretStore konfigurieren
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
namespace: production
spec:
provider:
vault:
server: "https://vault.internal.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "eso-role"
serviceAccountRef:
name: "external-secrets-sa"
Vault-Richtlinienkonfiguration
# Vault-Richtlinie: ESO auf bestimmte Pfade beschränken
path "secret/data/production/*" {
capabilities = ["read"]
}
path "secret/data/production/database/*" {
capabilities = ["read", "list"]
}
# Zugriff auf andere Umgebungen verweigern
path "secret/data/staging/*" {
capabilities = ["deny"]
}
path "secret/data/development/*" {
capabilities = ["deny"]
}
ExternalSecret-Ressource
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: production
spec:
refreshInterval: 15m
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: db-credentials
creationPolicy: Owner
template:
type: Opaque
data:
DATABASE_URL: "postgresql://{{ .username }}:{{ .password }}@db.internal:5432/mydb"
data:
- secretKey: username
remoteRef:
key: secret/data/production/database
property: username
- secretKey: password
remoteRef:
key: secret/data/production/database
property: password
Multi-Quelle: ClusterSecretStore
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: aws-secrets-manager
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
jwt:
serviceAccountRef:
name: eso-aws-sa
namespace: external-secrets
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: aws-api-keys
namespace: production
spec:
refreshInterval: 5m
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: aws-api-keys
dataFrom:
- extract:
key: production/api-keys
Muster 3: SOPS + Age/GPG Verschlüsselung
SOPS (Secrets OPerationS) ist ein von Mozilla entwickeltes Secret-Verschlüsselungstool, das mehrere Verschlüsselungs-Backends einschließlich AES, PGP und Age unterstützt. Im Gegensatz zu Sealed Secrets verschlüsselt SOPS die Datei selbst — es kann YAML/JSON/ENV-Formate verschlüsseln und verschlüsselt nur Werte, nicht Schlüssel. Das bedeutet, Sie können Git Diff verwenden, um strukturelle Änderungen in Secrets zu sehen.
Architektur
┌──────────────────────────────────────────────────────┐
│ Entwickler-Arbeitsstation │
│ ┌────────────┐ ┌────────────┐ │
│ │ secret.yaml │───▶│ SOPS │ │
│ │ (Klartext, │ │ + Age/GPG │ │
│ │ nicht │ │ │ │
│ │ committen) │ │ │ │
│ └────────────┘ └─────┬──────┘ │
│ │ verschlüsseln │
│ ┌─────▼──────┐ │
│ │secret.enc. │ │
│ │yaml(Chiffre)│ │
│ └─────┬──────┘ │
└──────────────────────────┼───────────────────────────┘
│ git push
┌──────────────────────────▼───────────────────────────┐
│ Git-Repository │
│ (speichert verschlüsselte Dateien, Schlüssel │
│ sichtbar, Werte verschlüsselt) │
└──────────────────────────┬───────────────────────────┘
│ git pull
┌──────────────────────────▼───────────────────────────┐
│ CI/CD-Pipeline │
│ ┌──────────────────────────────┐ │
│ │ sops --decrypt + kubectl apply│ │
│ │ oder Flux Kustomization SOPS │ │
│ │ Integration │ │
│ └──────────────────────────────┘ │
└───────────────────────────────────────────────────────┘
SOPS und Age installieren
# SOPS installieren
curl -sLO https://github.com/getsops/sops/releases/download/v3.9.0/sops-v3.9.0.linux.amd64
chmod +x sops-v3.9.0.linux.amd64
sudo mv sops-v3.9.0.linux.amd64 /usr/local/bin/sops
# Age installieren (empfohlenes Verschlüsselungs-Backend, einfacher als GPG)
curl -sLO https://github.com/FiloSottile/age/releases/download/v1.2.0/age-v1.2.0-linux-amd64.tar.gz
tar xzf age-v1.2.0-linux-amd64.tar.gz
sudo mv age/age /usr/local/bin/
sudo mv age/age-keygen /usr/local/bin/
# Age-Schlüsselpaar generieren
age-keygen -o age.key
# Öffentlicher Schlüssel: age1abc123xyz...
# Überprüfen
sops --version
age --version
Secret-Dateien verschlüsseln
# secret.yaml (vor der Verschlüsselung)
apiVersion: v1
kind: Secret
metadata:
name: app-config
namespace: production
type: Opaque
stringData:
DB_HOST: "db.internal.example.com"
DB_PASSWORD: "SuperSecret123!"
API_KEY: "sk-proj-abc123xyz456"
REDIS_URL: "redis://redis.internal:6379"
# Mit öffentlichem Age-Schlüssel verschlüsseln
sops --encrypt \
--age age1abc123xyz456... \
--encrypted-regex '^(DB_PASSWORD|API_KEY)$' \
--in-place secret.yaml
# secret.yaml (nach der Verschlüsselung)
apiVersion: v1
kind: Secret
metadata:
name: app-config
namespace: production
type: Opaque
stringData:
DB_HOST: "db.internal.example.com"
DB_PASSWORD: ENC[AES256_GCM,data:Wk5kPQ==,tag:abc123==,type:str]
API_KEY: ENC[AES256_GCM,data:Zm9vYmFy,tag:def456==,type:str]
REDIS_URL: "redis://redis.internal:6379"
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age1abc123xyz456...
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA...
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-06-16T10:00:00Z"
mac: ENC[AES256_GCM,data:abc==,tag:xyz==,type:str]
pgp: []
encrypted_regex: ^(DB_PASSWORD|API_KEY)$
version: 3.9.0
Flux CD SOPS-Integration
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: app-secrets
namespace: flux-system
spec:
interval: 10m
path: ./clusters/production
prune: true
sourceRef:
kind: GitRepository
name: flux-system
decryption:
provider: sops
secretRef:
name: sops-age-key
# Age-Privatschlüssel-Secret für Flux-Entschlüsselung erstellen
kubectl create secret generic sops-age-key \
--namespace flux-system \
--from-file=age.agekey=age.key \
--dry-run=client -o yaml | \
kubeseal --format yaml > sealed-sops-key.yaml
Multi-Schlüssel-Verschlüsselung (Teamzusammenarbeit)
# .sops.yaml-Konfigurationsdatei — projektweite Verschlüsselungskonfiguration
cat > .sops.yaml << 'EOF'
creation_rules:
- path_regex: ^clusters/production/.*\.yaml$
key_groups:
- age:
- age1abc123xyz456 # Produktionsschlüssel
- age1def789uvw012 # SRE-Team-Schlüssel
- path_regex: ^clusters/staging/.*\.yaml$
key_groups:
- age:
- age1ghi345rst678 # Staging-Schlüssel
- path_regex: ^clusters/.*\.yaml$
key_groups:
- age:
- age1abc123xyz456 # Standardschlüssel
EOF
Muster 4: Automatische Secret-Rotation
Die Secret-Rotation ist der am meisten vernachlässigte Aspekt des DevOps GitOps Secret-Managements. Statische Secrets, die sich nie ändern, schaffen ein unendliches Angriffsfenster, sobald sie geleakt sind. Die Best Practice 2026: Alle Produktions-Secrets alle 90 Tage rotieren und hochsensible Secrets alle 7 Tage.
Rotationsarchitektur
┌──────────────────────────────────────────────────────┐
│ Secret-Auto-Rotationsarchitektur │
│ │
│ ┌──────────┐ Auslöser ┌──────────────┐ │
│ │ CronJob │─────────────▶│ Vault Rotate │ │
│ │ (Zeitplan)│ │ (neu erzeugen)│ │
│ └──────────┘ └──────┬───────┘ │
│ │ neues Secret │
│ ┌──────▼───────┐ │
│ │ ExternalSecret│ │
│ │ (auto refresh)│ │
│ └──────┬───────┘ │
│ │ aktualisieren │
│ ┌──────▼───────┐ │
│ │ K8s Secret │ │
│ │ (auto Update) │ │
│ └──────┬───────┘ │
│ │ Rolling Restart │
│ ┌──────▼───────┐ │
│ │ Pods │ │
│ │ (neuen Schlüssel│ │
│ │ lesen) │ │
│ └──────────────┘ │
└──────────────────────────────────────────────────────┘
ESO Auto-Rotationskonfiguration
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: rotating-api-key
namespace: production
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: api-key
creationPolicy: Owner
template:
type: Opaque
metadata:
annotations:
reflector.v1.k8s.emberstack.com/reflection-allowed: "true"
data:
- secretKey: api-key
remoteRef:
key: secret/data/production/api
property: key
Vault Dynamische Secrets
# Vault dynamische Datenbank-Anmeldeinformationskonfiguration
resource "vault_database_secret_backend_connection" "postgresql" {
backend = "database"
name = "postgresql-production"
allowed_roles = ["app-readonly", "app-readwrite"]
postgresql {
connection_url = "postgresql://{{username}}:{{password}}@db.internal:5432/mydb?sslmode=require"
username = "vault_admin"
password = "VaultAdminPassword123!"
}
}
resource "vault_database_secret_backend_role" "app_readwrite" {
backend = "database"
name = "app-readwrite"
db_name = vault_database_secret_backend_connection.postgresql.name
default_ttl = 3600
max_ttl = 86400
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}}\";"
]
}
Automatische Erkennung von Secret-Änderungen in Anwendungen
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
annotations:
secret.reloader.stakater.com/reload: "api-key,db-credentials"
spec:
containers:
- name: api-server
image: registry.example.com/api-server:v2.1.0
envFrom:
- secretRef:
name: api-key
- secretRef:
name: db-credentials
# Reloader installieren — automatischer Rolling Restart von Pods bei Secret-Änderungen
helm repo add stakater https://stakater.github.io/stakater-charts
helm repo update
helm install reloader stakater/reloader \
--namespace reloader \
--create-namespace \
--set reloader.watchGlobally=false
Muster 5: Multi-Cluster Secret-Synchronisation
In Multi-Cluster-Szenarien müssen Secrets sicher über Cluster hinweg synchronisiert werden. DevOps GitOps Secret-Management erfordert: Jeder Cluster hat seinen eigenen Secret-Lebenszyklus, während die Konsistenz gewahrt bleibt.
Multi-Cluster-Architektur
┌──────────────────────────────────────────────────────┐
│ Multi-Cluster Secret-Sync-Architektur │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ HashiCorp Vault (Zentral) │ │
│ │ ┌──────────┐ ┌──────────┐ │ │
│ │ │prod-east/│ │prod-west/│ │ │
│ │ │ secrets │ │ secrets │ │ │
│ │ └──────────┘ └──────────┘ │ │
│ └──────────┬───────────────┬──────────┘ │
│ │ │ │
│ ┌────────▼──────┐ ┌──────▼────────┐ │
│ │ Cluster East │ │ Cluster West │ │
│ │ ┌──────────┐ │ │ ┌──────────┐ │ │
│ │ │ ESO │ │ │ │ ESO │ │ │
│ │ │Controller│ │ │ │Controller│ │ │
│ │ └────┬─────┘ │ │ └────┬─────┘ │ │
│ │ ▼ │ │ ▼ │ │
│ │ ┌──────────┐ │ │ ┌──────────┐ │ │
│ │ │ Secrets │ │ │ │ Secrets │ │ │
│ │ └──────────┘ │ │ └──────────┘ │ │
│ └───────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────┘
Cluster-spezifischer unabhängiger SecretStore
# Cluster East
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-east
namespace: production
spec:
provider:
vault:
server: "https://vault.internal.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes/east"
role: "eso-east-role"
serviceAccountRef:
name: "external-secrets-sa"
# Cluster West
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-west
namespace: production
spec:
provider:
vault:
server: "https://vault.internal.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes/west"
role: "eso-west-role"
serviceAccountRef:
name: "external-secrets-sa"
TLS-Zertifikat Multi-Cluster-Verteilung
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: wildcard-tls
namespace: ingress-nginx
spec:
refreshInterval: 24h
secretStoreRef:
name: vault-east
kind: SecretStore
target:
name: wildcard-tls
creationPolicy: Owner
template:
type: kubernetes.io/tls
data:
- secretKey: tls.crt
remoteRef:
key: secret/data/shared/tls/wildcard
property: cert
- secretKey: tls.key
remoteRef:
key: secret/data/shared/tls/wildcard
property: key
Sealed Secrets Multi-Cluster Hauptschlüssel-Sync
# Hauptschlüssel exportieren
kubectl get secret -n kube-system sealed-secrets-key \
-o yaml > sealed-secrets-master-key.yaml
# In Ziel-Cluster importieren
kubectl apply -f sealed-secrets-master-key.yaml -n kube-system
# Controller neustarten, um Schlüssel zu laden
kubectl rollout restart deployment/sealed-secrets-controller -n kube-system
Muster 6: Audit und Compliance
DevOps GitOps Secret-Management muss Audit-Anforderungen erfüllen: wer hat wann auf welches Secret zugegriffen. Compliance-Standards 2026 (SOC2, ISO 27001, MLPS 2.0) erfordern allesamt nachverfolgbaren Secret-Zugriff.
Vault-Audit-Logs
# Vault-Audit-Logging aktivieren
audit {
type = "file"
options = {
file_path = "/vault/audit/audit.log"
mode = "0600"
}
}
# Syslog-Audit aktivieren
audit {
type = "syslog"
options = {
facility = "AUTH"
tag = "vault"
address = "syslog.internal:514"
}
}
ESO-Zugriffsprotokollierung
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: audited-vault
annotations:
audit.external-secrets.io/enabled: "true"
audit.external-secrets.io/log-access: "true"
spec:
provider:
vault:
server: "https://vault.internal.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "eso-audited-role"
serviceAccountRef:
name: "external-secrets-sa"
Kubernetes-Audit-Richtlinie
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: RequestResponse
resources:
- group: ""
resources: ["secrets"]
namespaces: ["production", "staging"]
omitStages:
- RequestReceived
- level: Metadata
resources:
- group: ""
resources: ["secrets"]
namespaces: ["default"]
omitStages:
- RequestReceived
- level: RequestResponse
resources:
- group: "external-secrets.io"
resources: ["externalsecrets", "secretstores"]
omitStages:
- RequestReceived
Secret-Zugriffsüberwachung
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: restrict-secret-access
spec:
validationFailureAction: Audit
background: true
rules:
- name: block-default-sa-secret-access
match:
resources:
kinds: [Pod]
validate:
message: "Zugriff auf Secrets mit dem Standard-ServiceAccount ist verboten"
pattern:
spec:
serviceAccountName: "!default"
- name: require-secret-annotations
match:
resources:
kinds: [Secret]
names: ["db-*", "api-*", "tls-*"]
validate:
message: "Produktions-Secrets müssen Besitzer- und Ablaufanmerkungen haben"
pattern:
metadata:
annotations:
owner: "?*"
expiry: "?*"
5 Häufige Fallstricke und Lösungen
Fallstrick 1: Verlust des Sealed Secrets Hauptschlüssels
Symptom: Nach Cluster-Neuaufbau können alle SealedSecrets nicht entschlüsselt werden. Der Controller meldet failed to unseal.
Ursache: Sealed Secrets verwendet asymmetrische Verschlüsselung. Der private Schlüssel existiert nur im Cluster. Zerstörung des Clusters verliert den privaten Schlüssel.
Lösung:
# 1. Regelmäßiges Hauptschlüssel-Backup (automatisiert)
kubectl get secret -n kube-system \
sealed-secrets-key -o yaml | \
sops --encrypt --age age1abc123xyz456... \
/dev/stdin > sealed-secrets-key.enc.yaml
# 2. Verschlüsselten Schlüssel in einem anderen Git-Repo speichern
git add sealed-secrets-key.enc.yaml
git commit -m "backup: sealed secrets master key $(date +%Y%m%d)"
# 3. Wiederherstellungsprozess
sops --decrypt sealed-secrets-key.enc.yaml | \
kubectl apply -f -
Fallstrick 2: ExternalSecret-Aktualisierungsverzögerung verursacht Pod-Startfehler
Symptom: Neu bereitgestellte Pods starten nicht, weil das Secret noch nicht existiert — ESO hat es noch nicht aus Vault abgerufen.
Lösung:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-secrets
spec:
refreshInterval: 5m
target:
creationPolicy: Owner
template:
type: Opaque
dataFrom:
- extract:
key: production/app-secrets
# Sicherstellen, dass ESO synchronisiert hat, bevor die Anwendung bereitgestellt wird
# Methode 1: ArgoCD Sync Hooks verwenden
# Methode 2: Im CI auf Secret-Existenz warten
kubectl wait --for=condition=Ready \
externalsecret/app-secrets -n production --timeout=120s
Fallstrick 3: SOPS-Schlüsselrotation bricht Entschlüsselung alter Chiffretexte
Symptom: Nachdem das Team Age-Schlüssel rotiert hat, können alte verschlüsselte Dateien nicht mehr entschlüsselt werden.
Lösung:
# Multi-Schlüssel-Verschlüsselung verwenden — Koexistenz alter und neuer Schlüssel
sops --encrypt \
--age age1NEWKEY...,age1OLDKEY... \
--in-place secret.yaml
# Oder Schlüsselgruppen über .sops.yaml verwalten
# Nach Schlüsselaktualisierung mit altem Schlüssel entschlüsseln und dann mit neuem neu verschlüsseln
sops --decrypt --age age1OLDKEY... secret.yaml | \
sops --encrypt --age age1NEWKEY... \
--filename-override secret.yaml /dev/stdin > secret_new.yaml
Fallstrick 4: Leaked Secrets können nicht schnell widerrufen werden
Symptom: Ein API-Key wurde geleakt, aber SealedSecret erfordert Neuverschlüsselung und Commit — das Widerruffenster ist zu lang.
Lösung:
# ESO-Ansatz: alten Schlüssel direkt in Vault deaktivieren
vault kv metadata put -delete-version-after=0s \
secret/data/production/api-key
# Sealed Secrets-Ansatz: Notfall-Löschung über Controller
kubectl delete secret api-key -n production
# Sofort neu verschlüsseln und neues SealedSecret committen
# Generischer Ansatz: Netzwerkebene-Blockierung
kubectl apply -f - <<EOF
apiVersion: networkpolicies.k8s.io/v1
kind: NetworkPolicy
metadata:
name: block-compromised
namespace: production
spec:
podSelector: {}
policyTypes: [Egress]
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- compromised-api.example.com/32
EOF
Fallstrick 5: Teamübergreifend geteilte Secrets führen zu Zugriffskontroll-Chaos
Symptom: Mehrere Teams verwenden denselben Vault-Token für den Zugriff auf Secrets, was es unmöglich macht, zu unterscheiden, wer was getan hat.
Lösung:
# Vault-Richtlinien pro Team erstellen
path "secret/data/team-a/*" {
capabilities = ["read", "list"]
}
path "secret/data/team-b/*" {
capabilities = ["read", "list"]
}
# Explizite Verweigerung für Querzugriff
path "secret/data/team-a/*" {
capabilities = ["deny"]
}
# An team-b-Rolle binden
# Vault-Namespaces zur Isolierung verwenden
namespace "team-a" {
path "secret/*" {
capabilities = ["read", "list", "create", "update"]
}
}
10 Häufige Fehlerbehebung
1. failed to unseal: no private key found
# Prüfen, ob der Controller den Hauptschlüssel hält
kubectl get secrets -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key
# Hauptschlüssel wiederherstellen
kubectl apply -f sealed-secrets-key-backup.yaml -n kube-system
kubectl rollout restart deployment/sealed-secrets-controller -n kube-system
2. ExternalSecret "not ready": could not get secret data
# SecretStore-Verbindung prüfen
kubectl describe secretstore vault-backend -n production
# Vault-Authentifizierung prüfen
kubectl logs -n external-secrets -l app.kubernetes.io/name=external-secrets
# ServiceAccount-Berechtigungen überprüfen
kubectl auth can-i get secret -n production \
--as=system:serviceaccount:production:external-secrets-sa
3. sops error decrypting: could not find a matching key
# Prüfen, ob der Age-Schlüssel korrekt ist
age-keygen -y age.key
# Schlüsselkonfiguration in .sops.yaml überprüfen
cat .sops.yaml
# Schlüssel für Entschlüsselung explizit angeben
sops --decrypt --age age1YOURKEY... secret.yaml
4. SealedSecret "sealed-secrets" is invalid: metadata.name
# Namespace und Namen während der Verschlüsselung überprüfen
# Strict-Modus erfordert exakte Übereinstimmung
kubeseal --scope strict --namespace production \
--name db-credentials < secret.yaml > sealed.yaml
5. Vault seal status: sealed
# Vault-Status prüfen
vault status
# Vault entsiegeln (erfordert 2 von 3 Unseal-Schlüsseln)
vault operator unseal <key1>
vault operator unseal <key2>
# Auto-Unseal-Konfiguration (für Produktion empfohlen)
# AWS KMS / GCP KMS / Azure Key Vault für Auto-Unseal verwenden
6. refreshInterval: cannot unmarshal
# Sicherstellen, dass das refreshInterval-Format korrekt ist
# Korrekt: "15m", "1h", "24h"
# Falsch: 15, "15", "15minutes"
spec:
refreshInterval: 15m
7. kubeseal: error: failed to get certificate
# Prüfen, ob der Controller läuft
kubectl get deployment sealed-secrets-controller -n kube-system
# Zertifikats-Secret prüfen
kubectl get secret -n kube-system sealed-secrets-key
# Öffentlichen Schlüssel vom Controller abrufen
kubeseal --fetch-cert > sealed-secrets-cert.pem
kubeseal --cert sealed-secrets-cert.pem < secret.yaml
8. ExternalSecret status: SecretSyncedError
# Detaillierten Fehler anzeigen
kubectl describe externalsecret db-credentials -n production
# Häufige Ursache: Vault-Pfad existiert nicht
# Pfade in Vault prüfen
vault kv list secret/production/
# Häufige Ursache: unzureichende Berechtigungen
vault policy read eso-role
9. SOPS: mac mismatch: file has been modified
# Datei wurde nach der Verschlüsselung manuell geändert, MAC-Verifizierung fehlgeschlagen
# Sicherer Ansatz: neu verschlüsseln
sops --decrypt secret.yaml > secret_plain.yaml
# Ändern und dann neu verschlüsseln
sops --encrypt --age age1YOURKEY... secret_plain.yaml > secret.yaml
10. Kubernetes: secret "xxx" not found
# ESO hat die Synchronisation noch nicht abgeschlossen
kubectl get externalsecret -n production
# ESO-Controller-Logs prüfen
kubectl logs -n external-secrets -l app.kubernetes.io/name=external-secrets \
--tail=100
# Synchronisation manuell auslösen
kubectl annotate externalsecret db-credentials \
force-sync=$(date +%s) -n production \
--overwrite
Erweiterte Optimierungstipps
1. Git Pre-commit-Hook zur Verhinderung von Klartext-Secret-Commits
#!/bin/bash
# .git/hooks/pre-commit
# Gestagte Dateien nach verdächtigen Secrets durchsuchen
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.yaml$|\.yml$|\.env$')
for FILE in $STAGED_FILES; do
# Base64-kodiertes K8s-Secret erkennen
if grep -qE 'kind:\s*Secret' "$FILE" 2>/dev/null; then
if ! grep -qE 'kind:\s*SealedSecret|encryptedData|sops:' "$FILE" 2>/dev/null; then
echo "FEHLER: Unverschlüsselte Secret-Datei gefunden: $FILE"
echo "Bitte vor dem Committen mit kubeseal oder sops verschlüsseln"
exit 1
fi
fi
# Häufige Secret-Muster erkennen
if grep -qiE '(password|secret|api.key|token)\s*[:=]\s*["\x27]?[A-Za-z0-9+/=]{16,}' "$FILE" 2>/dev/null; then
if ! grep -qE 'ENC\[|sops:|encryptedData' "$FILE" 2>/dev/null; then
echo "WARNUNG: Verdächtiges Klartext-Secret in: $FILE"
echo "Bitte Verschlüsselung bestätigen oder externes Secret-Management verwenden"
fi
fi
done
exit 0
2. ArgoCD Secret-Management-Integration
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: app-with-secrets
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/gitops-manifests.git
targetRevision: main
path: overlays/production
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
3. Secret-Template-Engine
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-config
namespace: production
spec:
refreshInterval: 15m
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: app-config
template:
type: Opaque
engineVersion: v2
data:
DATABASE_URL: "postgresql://{{ .db_user }}:{{ .db_pass }}@{{ .db_host }}:5432/{{ .db_name }}?sslmode=require"
REDIS_URL: "redis://:{{ .redis_pass }}@{{ .redis_host }}:6379/0"
JWT_SECRET: "{{ .jwt_secret }}"
CONFIG_JSON: |
{
"database": {
"host": "{{ .db_host }}",
"port": 5432,
"name": "{{ .db_name }}"
},
"redis": {
"host": "{{ .redis_host }}"
}
}
data:
- secretKey: db_user
remoteRef:
key: secret/data/production/database
property: username
- secretKey: db_pass
remoteRef:
key: secret/data/production/database
property: password
- secretKey: db_host
remoteRef:
key: secret/data/production/database
property: host
- secretKey: db_name
remoteRef:
key: secret/data/production/database
property: dbname
- secretKey: redis_pass
remoteRef:
key: secret/data/production/redis
property: password
- secretKey: redis_host
remoteRef:
key: secret/data/production/redis
property: host
- secretKey: jwt_secret
remoteRef:
key: secret/data/production/auth
property: jwt_secret
4. Secret-Gesundheitsprüfung
apiVersion: batch/v1
kind: CronJob
metadata:
name: secret-health-check
namespace: security
spec:
schedule: "0 8 * * 1"
jobTemplate:
spec:
template:
spec:
serviceAccountName: secret-checker
containers:
- name: checker
image: bitnami/kubectl:1.30
command:
- /bin/bash
- -c
- |
echo "=== Secret-Gesundheitsprüfung $(date) ==="
echo ""
echo "--- Ablaufende TLS-Zertifikate prüfen ---"
kubectl get secrets --all-namespaces \
-o json | jq -r '.items[] |
select(.type=="kubernetes.io/tls") |
"\(.metadata.namespace)/\(.metadata.name)"' | \
while read secret; do
ns=$(echo $secret | cut -d/ -f1)
name=$(echo $secret | cut -d/ -f2)
cert=$(kubectl get secret $name -n $ns \
-o jsonpath='{.data.tls\.crt}' | base64 -d)
expiry=$(echo "$cert" | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ -n "$expiry" ]; then
days=$(( ($(date -d "$expiry" +%s) - $(date +%s)) / 86400 ))
if [ $days -lt 30 ]; then
echo "WARNUNG: Zertifikat $secret läuft in ${days} Tagen ab"
fi
fi
done
echo ""
echo "--- ExternalSecret-Synchronisationsstatus prüfen ---"
kubectl get externalsecrets --all-namespaces \
-o json | jq -r '.items[] |
select(.status.conditions[]?.type=="Ready" and
.status.conditions[]?.status!="True") |
"\(.metadata.namespace)/\(.metadata.name): NICHT BEREIT"'
restartPolicy: OnFailure
Vergleich: Sealed Secrets vs External Secrets vs SOPS
| Dimension | Sealed Secrets | External Secrets Operator | SOPS |
|---|---|---|---|
| Verschlüsselung | Asymmetrisch (Cluster-Pubkey) | Keine Git-Inhaltsverschlüsselung | Symmetrisch/Asymmetrisch |
| Git-Inhalt | Chiffretext (SealedSecret) | Referenzen (ExternalSecret) | Chiffretext (Werte verschlüsselt) |
| Externe Abhängigkeiten | Keine (autark) | Vault/AWS/GCP usw. | Keine (autark) |
| Secret-Rotation | Neuverschlüsselung erforderlich | Native Unterstützung | Neuverschlüsselung erforderlich |
| Multi-Cluster | Hauptschlüssel synchronisieren | SecretStore pro Cluster | Verschlüsselungsschlüssel teilen |
| Git-Diff-freundlich | Nein (Chiffretext unlesbar) | Ja (Referenzen lesbar) | Ja (Schlüssel lesbar, Werte verschlüsselt) |
| Offline-Entschlüsselung | Nein (benötigt Cluster-Privkey) | Nein (benötigt externen Dienst) | Ja (lokaler Schlüssel ausreichend) |
| Lernkurve | Niedrig | Mittel | Mittel |
| Betriebskomplexität | Niedrig | Hoch (Vault erforderlich) | Niedrig |
| Audit-Fähigkeit | Git-Verlauf | Vault-Audit-Logs | Git-Verlauf |
| Dynamische Secrets | Nicht unterstützt | Unterstützt (Vault dynamische Credentials) | Nicht unterstützt |
| Skalierungsfit | Klein-mittlere Teams | Mittel-große Unternehmen | Jede Skala |
| Am besten für | Einfaches GitOps | Unternehmens-Secret-Management | Multi-Format-Verschlüsselung |
Auswahl-Entscheidungsbaum
Haben Sie bereits Vault/AWS Secrets Manager?
├── Ja → External Secrets Operator
│ └── Dynamische Secrets benötigt? → ESO + Vault dynamische Credentials
└── Nein → Verschlüsselten Chiffretext in Git speichern?
├── Ja → Git-Diff-freundlich?
│ ├── Ja → SOPS + Age
│ └── Nein → Sealed Secrets
└── Nein → Dynamische Secrets benötigt?
├── Ja → Vault + ESO bereitstellen
└── Nein → Sealed Secrets
Empfohlene Online-Tools
- Base64-Kodierer/Dekodierer: /de/encode/base64 — K8s-Secret- und SealedSecret-Daten kodieren/dekodieren
- RSA-Schlüsselgenerator: /de/encode/rsa — RSA-Schlüsselpaare für SOPS-GPG-Verschlüsselung generieren
- Hash-Rechner: /de/encode/hash — Secret-Fingerabdrücke und Prüfsummen berechnen
Zusammenfassung: Es gibt keine Silberkugel für DevOps GitOps Secret-Management, aber es gibt einen Best-Practice-Pfad. Kleine Teams beginnen mit Sealed Secrets — null externe Abhängigkeiten für GitOps-Secret-Sicherheit. Mittel-große Unternehmen wählen External Secrets Operator + Vault mit nativer Secret-Rotation und dynamischen Credentials. Wenn Sie Multi-Format-Verschlüsselung und Git-Diff-Freundlichkeit benötigen, ist SOPS + Age die beste Wahl. Die Kernprinzipien 2026: null Klartextspeicherung, automatische Rotation, auditierbar und nachverfolgbar. Denken Sie daran — Secret-Lecks sind keine Frage des "Ob", sondern des "Wann".
Verwandte Beiträge:
- GitOps mit ArgoCD in Produktion — Vollständiger ArgoCD-Bereitstellungsautomatisierungs-Leitfaden
- GitOps mit Flux CD in Produktion — Flux CD Continuous Delivery in der Praxis
- Docker-Container-Sicherheitshärtung — 8-Schicht-Container-Verteidigungssystem
Externe Referenzen:
Probiere diese browser-lokalen Tools aus — keine Registrierung erforderlich →