· Kubernetes · 9 Min. Lesezeit
Secrets im Git, aber verschlüsselt: SOPS in der Praxis
GitOps verlangt den kompletten Soll-Zustand in Git. Passwörter und Token gehören da nicht im Klartext hinein. Wie Sie SOPS mit age einrichten, an welcher Stelle im Ablauf entschlüsselt wird, welche Stelle Argo CD ausdrücklich nicht empfiehlt und wo die belegbaren Grenzen des Verfahrens liegen.

GitOps lebt davon, dass der komplette Soll-Zustand in Git steht. Für Passwörter, Token und Schlüssel gilt das nicht. Die Flux-Dokumentation formuliert das ohne Einschränkung: Secrets im Klartext oder in base64 in einem Git-Repository abzulegen ist unsicher, unabhängig von Sichtbarkeit und Zugriffsrechten des Repositories.
SOPS schließt diese Lücke, ohne den GitOps-Ablauf zu verlassen. Die Frage vor der Einrichtung ist nicht ob SOPS, sondern an welcher Stelle Ihrer Kette entschlüsselt wird. Das bestimmt, wo der private Schlüssel liegt, wer ihn erreicht und welche Komponente Sie zusätzlich absichern müssen.
Wann SOPS das richtige Werkzeug ist
SOPS passt, wenn Ihr Soll-Zustand ohnehin in Git liegt und Sie die Secrets im selben Repository, im selben Review-Ablauf und in derselben Historie führen wollen. Es passt nicht, wenn Sie eine zentrale Verwaltung mit Auditierung, dynamischen Zugangsdaten und Ablaufzeiten brauchen. SOPS ist kein Secret-Store mit Lebenszyklus.
Der aktuelle Stand ist SOPS v3.13.2 vom 30. Juni 2026. Wichtig für die Schreibweise: Seit 3.9.0 gibt es eigene Unterbefehle für Verschlüsseln, Entschlüsseln, Rotieren und Bearbeiten. Ein Tutorial, das heute noch sops -e und sops -d schreibt, arbeitet mit der alten Flag-Schreibweise. Die Eingabe über stdin funktioniert nur mit den neuen Unterbefehlen.
Die drei Orte, an denen entschlüsselt werden kann
| Ansatz | Ort der Entschlüsselung | Schlüsselablage | Belegte Auflage |
|---|---|---|---|
Flux, spec.decryption | Zielcluster | Secret in flux-system | Schlüssel im Secret muss auf .agekey enden, metadata, kind, apiVersion bleiben Klartext |
sops-secrets-operator | Zielcluster, im Operator-Pod | Secret, als Datei gemountet, Pfad über SOPS_AGE_KEY_FILE | Schützt nach eigener Aussage nur gegen das Git-Repository, nicht gegen Cluster-Zugriff |
| KSOPS als kustomize-Plugin | Wo kustomize build läuft, bei Argo CD im repo-server | Beim Build-Prozess | Braucht --enable-alpha-plugins --enable-exec, Argo CD rät davon ausdrücklich ab |
Die drei Ansätze sind nicht austauschbar. Die ersten beiden entschlüsseln auf dem Zielcluster, der dritte während der Manifest-Erzeugung. Argo CD empfiehlt in der eigenen Dokumentation zum Secret Management den Zielcluster nachdrücklich. Ich folge dem, weil die Begründung nachvollziehbar ist: Argo CD legt die von Plugins erzeugten Manifeste samt Secrets in seiner Redis-Instanz ab, und dieselben Manifeste sind über die repo-server-API erreichbar. KSOPS verschiebt die Schutzbedürftigkeit damit auf zwei Komponenten, die vorher keine Klartext-Secrets gesehen haben.
Schritt 1: SOPS installieren und die Echtheit prüfen
Die Release-Seite zu v3.13.2 liefert neben dem Binary Prüfsummen, eine Sigstore-Signatur, SLSA-Provenance und SBOMs. Die Reihenfolge ist nicht beliebig: erst die Prüfsummendatei gegen die Signatur, dann die Prüfsumme selbst. Umgekehrt ist der Vorgang wertlos, weil eine manipulierte Prüfsummendatei zu einem manipulierten Binary passt.
curl -LO https://github.com/getsops/sops/releases/download/v3.13.2/sops-v3.13.2.linux.amd64
cosign verify-blob sops-v3.13.2.checksums.txt \
--bundle sops-v3.13.2.checksums.sigstore.json \
--certificate-identity-regexp=https://github.com/getsops \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com
sha256sum -c sops-v3.13.2.checksums.txt --ignore-missing
mv sops-v3.13.2.linux.amd64 /usr/local/bin/sops
chmod +x /usr/local/bin/sopsPrüfung: Der Versionsaufruf prüft standardmäßig auf eine neuere Version. Das ist seit 3.10.0 abgekündigt, aber noch Voreinstellung. In CI-Läufen und abgeschotteten Netzen schalten Sie es ab.
SOPS_DISABLE_VERSION_CHECK=true sops --versionSchritt 2: age-Schlüsselpaar erzeugen
age steht bei v1.3.1 vom 28. Dezember 2025. Version 1.2.1 war eine Sicherheitsversion, die eine Lücke geschlossen hat, über die sich unter Umständen ein beliebiges Binary ausführen ließ. Ältere Stände gehören ausgetauscht. Der öffentliche Schlüssel erscheint beim Erzeugen auf der Konsole, der private landet in der Datei. Legen Sie die Identität an den Standardpfad, den die Dokumentation zu age-Identitäten je Betriebssystem nennt. Unter Linux ist das $HOME/.config/sops/age/keys.txt.
age-keygen -o age.agekey
mkdir -p "$HOME/.config/sops/age"
cp age.agekey "$HOME/.config/sops/age/keys.txt"Prüfung: Die Datei ist eine Liste von age-X25519-Identitäten, eine pro Zeile. SOPS probiert sie der Reihe nach durch, bis eine entschlüsseln kann.
grep -c '^AGE-SECRET-KEY' "$HOME/.config/sops/age/keys.txt"Schritt 3: .sops.yaml anlegen
Die Konfiguration entscheidet, welche Dateien mit welchen Empfängern verschlüsselt werden und welche Felder betroffen sind. Für Kubernetes-Secrets ist die Auswahl der Felder nicht optional.
creation_rules:
- path_regex: .*.yaml
encrypted_regex: ^(data|stringData)$
age: >-
age1s3cqcks5genc6ru8chl0hkkd04zmxvczsvdxq99ekffe4gmvjpzsedk23c,
age1qe5lxzzeppw5k79vxn3872272sgy224g2nzqlzy3uljs84say3yqgvd0swSetzen Sie hier Ihre eigenen öffentlichen Schlüssel ein. Mehrere Empfänger sind der Normalfall, sobald mehr als eine Person oder mehr als ein Cluster entschlüsseln können soll. Später hinzugefügte Empfänger ziehen Sie mit sops updatekeys in die bestehenden Dateien nach.
Zur Auswahl der Felder gibt es sechs Optionen, darunter encrypted_regex und unencrypted_suffix. encrypted_regex verschlüsselt einen Wert, wenn sein Schlüssel auf den Ausdruck passt. Pro Regel darf höchstens eine dieser Optionen gesetzt sein.
Prüfung: Der Dateiname ist heikel. SOPS erwartet .sops.yaml und warnt seit 3.10.0, wenn es stattdessen .sops.yml findet. Ein ls -1 .sops.yaml vorab kostet nichts.
Schritt 4: Ein Kubernetes-Secret verschlüsseln
Flux verlangt ausdrücklich, dass metadata, kind und apiVersion im Klartext bleiben. Wer die ganze Datei verschlüsselt, bekommt eine Datei, die kein Werkzeug der Kette mehr als Kubernetes-Ressource erkennt. Der Klartext, hier für einen Dienst unter app.lab.rz.jabi-it.de:
apiVersion: v1
kind: Secret
metadata:
name: basic-auth
namespace: apps
stringData:
username: admin
password: bitte-ersetzenVerschlüsselt wird direkt in der Datei. --in-place und --output schließen sich dabei gegenseitig aus, die Kombination führt zu einem Fehler.
sops encrypt --encrypted-regex '^(data|stringData)$' -i basic-auth.yaml
Prüfung: Für die Kontrolle genügt ein einzelnes Feld, das nicht als Klartextdatei auf der Platte landet. Prüfen Sie zusätzlich mit dem Auge, dass apiVersion, kind und metadata lesbar geblieben sind.
sops decrypt --extract '["stringData"]["username"]' basic-auth.yamlSchritt 5: Den privaten Schlüssel in den Cluster bringen
Ab hier trennen sich die Wege. Zuerst der Flux-Weg. Der Schlüsselname im Secret ist vorgeschrieben, nicht frei wählbar.
cat age.agekey | kubectl create secret generic sops-age \
--namespace=flux-system --from-file=age.agekey=/dev/stdinPrüfung: Der Datenschlüssel im Secret muss auf .agekey enden. Für OpenPGP gilt .asc, für Cloud-KMS gelten feste Namen wie sops.aws-kms und sops.gcp-kms.
kubectl -n flux-system get secret sops-age -o jsonpath='{.data}'Arbeiten Sie nicht mit Flux, übernimmt der sops-secrets-operator von isindir die Entschlüsselung im Zielcluster. Er steht bei Chart-Version 0.28.0 und bringt SOPS v3.13.1 mit. Die dokumentierte Reihenfolge sieht zuerst die CRD vor, dann das Helm-Chart. Der age-Schlüssel wird als Datei gemountet und der Pfad zusätzlich über SOPS_AGE_KEY_FILE in extraEnv gesetzt. Beides zusammen, nicht nur eines davon.
kubectl apply -f config/crd/bases/isindir.github.com_sopssecrets.yaml
helm repo add sops https://isindir.github.io/sops-secrets-operator/
kubectl create namespace sops
helm upgrade --install sops sops/sops-secrets-operator --namespace sops
kubectl -n sops exec deploy/sops-sops-secrets-operator -- printenv SOPS_AGE_KEY_FILESchritt 6: Die Entschlüsselung in der Kustomization deklarieren
Die Entschlüsselung wird an der Kustomization deklariert, die das Verzeichnis mit den Secrets ausrollt. Ohne den Block spec.decryption ist dort kein Entschlüsselungs-Provider konfiguriert.
# Ausschnitt aus der Kustomization für das Verzeichnis mit den Secrets
spec:
decryption:
provider: sops
secretRef:
name: sops-ageAls Provider ist sops laut Referenz der Flux-Kustomization der einzige unterstützte. Einzelne Ressourcen lassen sich per Annotation kustomize.toolkit.fluxcd.io/decrypt ausnehmen, gültig sind Enabled als Voreinstellung und Disabled.
Prüfung: Der entscheidende Test ist, ob im Zielnamespace Klartext ankommt und nicht die Chiffre. Dieser Prüfschritt steht in keiner offiziellen Dokumentation, ich halte ihn trotzdem für den wichtigsten der ganzen Einrichtung. Erscheint eine Zeichenkette, die mit ENC[ beginnt, ist die Entschlüsselung nicht gelaufen.
kubectl -n apps get secret basic-auth -o jsonpath='{.data.username}' | base64 -dDie drei folgenreichsten Stolpersteine
1. Die ganze Datei wurde verschlüsselt
Symptom: Ein SOPS-verschlüsseltes Kubernetes-Secret wird von Flux oder kustomize abgelehnt, obwohl die Datei sauber verschlüsselt ist.
Ursache: Es wurde die ganze Datei verschlüsselt statt nur data und stringData. Flux benennt den Klartext von metadata, kind und apiVersion als harte Anforderung.
Prüfung: Die verschlüsselte Datei öffnen und kontrollieren, dass diese drei Felder lesbar sind.
Lösung: In .sops.yaml encrypted_regex auf ^(data|stringData)$ setzen und die Datei neu erzeugen.
2. Der Schlüsselname im Flux-Secret stimmt nicht
Symptom: Flux findet den age-Schlüssel nicht und die Kustomization bleibt im Fehler stehen, obwohl das Secret existiert.
Ursache: Der Datenschlüssel im Secret hat nicht die vorgeschriebene Endung. Flux erkennt age-Identitäten nur an .agekey, OpenPGP nur an .asc.
Prüfung: Die Datenschlüssel des Secrets mit kubectl -n flux-system get secret sops-age -o jsonpath='{.data}' auslesen.
Lösung: Das Secret mit --from-file=age.agekey=/dev/stdin anlegen, sodass der Datenschlüssel auf .agekey endet.
3. Der entfernte Schlüssel hat weiter Zugriff
Symptom: Ein ausgeschiedener Mitarbeiter oder ein kompromittierter Schlüssel wurde aus .sops.yaml entfernt, kann aber weiterhin alte Stände entschlüsseln.
Ursache: Das Entfernen eines Master-Schlüssels ändert den Datenschlüssel nicht. Wer ihn vorher kannte, kennt ihn weiter. Die Dokumentation zur Schlüsselverwaltung empfiehlt deshalb ausdrücklich, beim Entfernen den Datenschlüssel zu rotieren.
Prüfung: Im Dateikopf kontrollieren, dass der alte Empfänger verschwunden ist.
Lösung: Erst den Schlüssel per updatekeys entfernen, danach den Datenschlüssel erneuern.
sops rotate -i secret.enc.yamlWo die vollständige Sammlung der Stolpersteine steht
Diese drei Fälle blockieren den Ablauf entweder ganz oder brechen ein Sicherheitsversprechen, das Sie längst abgehakt hatten. Es gibt mehr davon. Die vollständige Sammlung aller elf Stolpersteine steht im Beitrag SOPS in GitOps: die vollständige Stolperstein-Sammlung, jeder Fall im selben Schema. Dort stehen unter anderem MAC-Fehler nach einer Handänderung, der Dateiname der Konfiguration, die Schlüsselpfade in Container und CI und die Alpha-Schalter von KSOPS.
Grenzen
SOPS verschlüsselt Werte, nicht Struktur. Die Sicherheitsseite des Projekts sagt das selbst: In YAML, JSON, ENV und INI bleiben die Schlüsselnamen im Klartext. Wer datenbank-passwort-produktion-kunde-xy als Schlüsselnamen wählt, hat in Git eine Information stehen, die er nicht stehen haben wollte. Auch der Diff-Vorteil wird dort relativiert: Diffs zeigen nur verschlüsselte Daten. Sie sehen, dass sich ein Feld geändert hat, nicht wie.
Der sops-secrets-operator benennt seine Grenze selbst. Er wurde nur entworfen, um das Schlüsselmaterial gegen Zugriff aus dem Git-Repository heraus zu schützen. Wer im Cluster per RBAC Secrets lesen darf, kann eine verschlüsselte SopsSecret anwenden und den Klartext bekommen. SOPS ersetzt Ihre Cluster-Berechtigungen also nicht.
Und eine ehrliche Lücke zum Schluss. Zur Aufbewahrung, Sicherung und Wiederherstellung des privaten age-Schlüssels macht die offizielle Dokumentation keine Vorgabe. Es gibt dort weder eine Backup-Empfehlung noch ein Rotationsintervall noch ein Verfahren für den Notfallzugriff. Das ist kein Detail, sondern die Frage, an der die ganze Konstruktion hängt, und Sie müssen sie selbst beantworten und aufschreiben. Wie ein Wiederherstellungstest aussieht, der diesen Namen verdient, steht im Beitrag Der Restore-Test, den niemand macht. Die passende Argo-CD-Installation dazu im Beitrag Argo CD deployen und die erste App ausrollen.
Sie wollen Secrets versionieren, ohne sie preiszugeben? Ich richte verschlüsselte Secret-Verwaltung ein, die zu Ihrem GitOps passt, und sorge dafür, dass der Schlüssel dahin gehört, wo er sicher ist. Mehr unter IT-Sicherheit & ISMS, das Erstgespräch ist unverbindlich.

Alex Jabi
Ich betreue IT, Informationssicherheit und Datenschutz für KMU und Praxen an der Bergstraße und im Odenwald, persönlich, dokumentiert und ohne Cloud-Zwang.
Mehr über mich


