Integration & Architektur

SOPS auf Kubernetes: Secrets verschlüsselt in Git ablegen und im Cluster entschlüsseln

Torben Kreuder
Torben Kreuder · Partner
·5 Min. Lesezeit

Wenn ein Kubernetes-Cluster aus Infrastructure-as-Code heraus befüllt wird — mit Terraform, Helm oder reinen Manifesten — taucht immer wieder dieselbe Frage auf: Wie hält man Secrets aus Orten heraus, an denen sie nichts zu suchen haben? Datenbank-Passwörter, API-Tokens, Zertifikate müssen am Ende im Cluster landen. Alles dazwischen — Git, State-Files, CI-Pipelines — darf sie nie im Klartext sehen.

Dieser Artikel beschreibt ein Setup, das wir projektübergreifend einsetzen: verschlüsselte Secrets liegen neben dem Code in Git, entschlüsselt werden sie erst im Cluster. Genutzt werden SOPS, der sops-secrets-operator und age für das Key-Management.


Das Problem mit den naheliegenden Ansätzen

Secrets als Klartext in einem Git-Repository abzulegen — selbst in einem privaten — vergrößert ihren Wirkungskreis drastisch. Jeder mit Repo-Zugriff, jedes Backup und jeder CI-Runner, der das Repo klont, hat das Passwort.

Terraform verschärft das. Der State trackt deployte Ressourcen inklusive der Werte von kubernetes_secret. Remote-State (S3, GitLab) leakt Secrets in weitere Storage-Ebenen. Selbst in-memory Plans können sensible Daten in CI-Logs sichtbar machen.

Die Recherche nach der richtigen Lösung ist mühsam, weil es viele Optionen gibt:

  • Cloud-Vaults (AWS KMS, GCP KMS, Azure Key Vault): stark, aber Provider-Lock-in, kostenpflichtig, und Secret-Rotation ist entkoppelt vom Code-Deployment.
  • Team-Passwort-Vaults (1Password, Bitwarden): gut für Menschen, schlecht für Automatisierung.
  • GitOps mit Verschlüsselung at rest: verschlüsselte Secrets neben dem Code, Entschlüsselung erst beim Deployment. Genau das macht SOPS-Operator.
Aufteilung des Secret-Zugriffs: Admin-Vault mit age-Identity getrennt vom Cluster mit entschlüsselten Secrets
Aufteilung des Secret-Zugriffs: Admin-Vault mit age-Identity getrennt vom Cluster mit entschlüsselten Secrets

Was SOPS und sops-secrets-operator machen

SOPS ist ein von Mozilla gepflegtes Tool, das YAML, JSON, ENV, INI und Binärdateien verschlüsseln und entschlüsseln kann. Als Backend kommen AWS KMS, GCP KMS, Azure Key Vault, age oder PGP in Frage.

Der sops-secrets-operator erweitert diesen Mechanismus in Richtung Kubernetes. Er führt eine Custom Resource Definition namens SopsSecret ein. Das verschlüsselte Manifest liegt in Git. Der Operator beobachtet den Cluster, entschlüsselt die sensiblen Felder und reconciled daraus normale Kubernetes-Secret-Ressourcen. Die konsumierenden Pods merken davon nichts — aus ihrer Perspektive bleibt alles beim Alten.

Ablauf des SOPS Operators von der verschlüsselten CRD zum entschlüsselten Kubernetes Secret
Ablauf des SOPS Operators von der verschlüsselten CRD zum entschlüsselten Kubernetes Secret

Warum age statt PGP

age ist ein schlankes Verschlüsselungs-Tool, 2019 von einem Google-Engineer geschrieben. Es löst bewusst nur ein Problem — Dateiverschlüsselung — und verzichtet auf den ausufernden Feature-Set (und die Angriffsfläche) von PGP/GnuPG.

Gegenüber GPG bietet age:

  • Einen Bruchteil des Featureumfangs und Codeumfangs.
  • Kein 20-jähriges Legacy mit alten Modes und Kompatibilitäts-Fallen.
  • Moderne Primitive (X25519 asymmetrische Verschlüsselung).
  • Einen einfacheren Key-Lifecycle (age-keygen produziert eine einzige Identity-Datei).

Mozilla empfiehlt age gegenüber PGP für SOPS. Für diesen Use Case stimmen wir zu.

Keys in age:

  • Identity (privater Schlüssel, beginnt mit AGE-SECRET-KEY-): gehört in einen Passwort-Vault. Wer die Identity hält, kann entschlüsseln.
  • Recipient (öffentlicher Schlüssel, beginnt mit age): kann bedenkenlos committet werden. Darauf wird verschlüsselt.

Verschlüsselungs- und Entschlüsselungs-Fluss

Lokale Verschlüsselung (was ein Entwickler vor dem Commit macht):

Encrypt-Flow: unverschlüsseltes SopsSecret + age-Recipient ergeben verschlüsseltes SopsSecret
Encrypt-Flow: unverschlüsseltes SopsSecret + age-Recipient ergeben verschlüsseltes SopsSecret

Entschlüsselung im Cluster (was der Operator kontinuierlich macht):

Decrypt-Flow: verschlüsseltes SopsSecret + age-Identity (Cluster-Secret) ergeben Kubernetes Secret
Decrypt-Flow: verschlüsseltes SopsSecret + age-Identity (Cluster-Secret) ergeben Kubernetes Secret

Der Operator braucht exakt ein Secret: die age-Identity, gemountet in den sops-secrets-operator-Pod. Alles andere in Git ist verschlüsselt.

Schritt-für-Schritt-Anleitung

1. age-Keypair erzeugen

age-keygen -o age-key.txt

Die Datei enthält sowohl den Recipient (öffentlich, als Kommentarzeile) als auch die Identity (privat). Direkt im Team-Passwort-Vault sichern.

2. age-Identity in den Cluster bringen

Identity in Base64 kodieren:

sed -n -e '/^AGE-SECRET-KEY/p' age-key.txt | base64

Secret per kubectl anlegen (nicht per Terraform — sonst landet die Identity im State-File):

apiVersion: v1
kind: Secret
metadata:
  name: sops-age-key-file
  namespace: YOUR_NAMESPACE
type: Opaque
data:
  key: YOUR_BASE64_ENCODED_IDENTITY

3. sops-secrets-operator installieren

Via Helm:

helm repo add sops https://isindir.github.io/sops-secrets-operator/
helm upgrade --install --create-namespace sops sops/sops-secrets-operator \
  --namespace YOUR_NAMESPACE

Oder via Terraform:

resource "helm_release" "sops_secrets_operator" {
  name             = "sops-secrets-operator"
  chart            = "sops-secrets-operator"
  repository       = "https://isindir.github.io/sops-secrets-operator/"
  namespace        = "YOUR_NAMESPACE"
  version          = "0.14.0"
  create_namespace = true
  values = [
    <<EOF
extraEnv:
  - name: SOPS_AGE_KEY_FILE
    value: /etc/sops-age-key-file/key
secretsAsFiles:
  - mountPath: /etc/sops-age-key-file
    name: sops-age-key-file
    secretName: sops-age-key-file
EOF
  ]
}

4. SopsSecret schreiben und verschlüsseln

apiVersion: isindir.github.com/v1alpha3
kind: SopsSecret
metadata:
  name: app-secrets
  namespace: YOUR_NAMESPACE
spec:
  secretTemplates:
    - name: app-secrets
      stringData:
        DATABASE_PASSWORD: supersecret
        API_TOKEN: also-supersecret

Mit SOPS und dem age-Recipient verschlüsseln:

sops --encrypt \
  --age 'YOUR_AGE_RECIPIENT_PUBLIC_KEY' \
  --encrypted-suffix Templates \
  app-secrets.yml > app-secrets.enc.yml

--encrypted-suffix Templates sorgt dafür, dass SOPS nur Felder unterhalb von secretTemplates verschlüsselt. Metadaten bleiben in Git lesbar.

5. Deployment in den Cluster

Direkt:

kubectl apply -f app-secrets.enc.yml

Oder als Terraform-Resource:

cat app-secrets.enc.yml | tfk8s --strip -o app-secrets.tf

Dann wie gewohnt terraform apply. Die Tools sops, age und tfk8s via Paketmanager installieren (z. B. brew install sops age tfk8s).

6. Verifizieren

kubectl get secrets app-secrets -n YOUR_NAMESPACE -o yaml

Wenn das Secret fehlt, in die Operator-Logs schauen:

kubectl logs -l app.kubernetes.io/name=sops-secrets-operator -n YOUR_NAMESPACE

Typischer Workflow im Alltag

CI/CD-Workflow: Entwickler verschlüsselt lokal → Git-Push → Pipeline applied gegen Cluster → Operator entschlüsselt
CI/CD-Workflow: Entwickler verschlüsselt lokal → Git-Push → Pipeline applied gegen Cluster → Operator entschlüsselt

Im Alltag sehen Änderungen an Secrets aus wie jeder andere PR. Ein Entwickler bearbeitet eine verschlüsselte Datei lokal mit sops, committet und pusht. Die Pipeline applied Manifeste gegen den Cluster, der Operator übernimmt die Entschlüsselung.

Eine bestehende verschlüsselte Datei bearbeiten:

export SOPS_AGE_KEY_FILE=./age-key.txt
sops app-secrets.enc.yml

SOPS öffnet den $EDITOR, du bearbeitest Klartext, SOPS verschlüsselt beim Speichern neu.

Was dieser Ansatz nicht löst

  • Zugriffskontrolle im Cluster. Jeder mit kubectl get secrets -o yaml-Berechtigung sieht das Klartext-Secret im Cluster. Ergänzend RBAC und Audit-Logs aufsetzen.
  • Terraform, das entschlüsselte Secrets referenziert. Wenn ein data.kubernetes_secret-Lookup den Wert für eine andere Terraform-Resource aus dem Cluster zieht, fließt er zurück in den State. Eine saubere Lösung dafür gibt es aktuell nicht — das Muster möglichst vermeiden.
  • Key-Rotation. Wenn die age-Identity kompromittiert ist, müssen alle SopsSecret-Manifeste in Git auf einen neuen Recipient umverschlüsselt und die Cluster-Identity-Secret rotiert werden. Den Prozess einmal durchdenken, bevor man ihn braucht.

Fazit

Secrets sauber zu verwalten gehört zu den härtesten Problemen im produktiven Betrieb. SOPS-Operator mit age hält Klartext aus Repositories, State-Files, CI-Logs und Cloud-Provider-Storage heraus — bei gleichzeitig unveränderter Kubernetes-Secret-Schnittstelle für Workloads. Das Setup ist eine Nachmittags-Aufgabe und zahlt sich bei jedem Deployment aus.

Kubernetes-Secrets-Setup besprechen →

Torben Kreuder
Über den Autor
Torben Kreuder
Partner

Torben bringt fast zwei Jahrzehnte professioneller Erfahrung in der Softwareentwicklung mit ein und hat einen Master Abschluss in Informatik der RWTH Aachen. Er hat Expertise in der mobilen-, Web- und Backendentwicklung über diverse Sprachen und Frameworks hinweg.