· 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 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

AnsatzOrt der EntschlüsselungSchlüsselablageBelegte Auflage
Flux, spec.decryptionZielclusterSecret in flux-systemSchlüssel im Secret muss auf .agekey enden, metadata, kind, apiVersion bleiben Klartext
sops-secrets-operatorZielcluster, im Operator-PodSecret, als Datei gemountet, Pfad über SOPS_AGE_KEY_FILESchützt nach eigener Aussage nur gegen das Git-Repository, nicht gegen Cluster-Zugriff
KSOPS als kustomize-PluginWo kustomize build läuft, bei Argo CD im repo-serverBeim Build-ProzessBraucht --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/sops

Prü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 --version

Schritt 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,
      age1qe5lxzzeppw5k79vxn3872272sgy224g2nzqlzy3uljs84say3yqgvd0sw

Setzen 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-ersetzen

Verschlü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

Nur die Werte unterhalb von stringData sind verschlüsselt. apiVersion, kind, metadata und die Schlüsselnamen bleiben lesbar, damit die Datei für die Werkzeuge der GitOps-Kette weiter als Kubernetes-Ressource erkennbar ist.

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.yaml

Schritt 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/stdin

Prü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_FILE

Schritt 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-age

Als 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 -d

Die 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.yaml

Wo 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, Jabi IT

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
Zurück zum Blog

Passende Artikel

Alle Artikel »