Ein Kubernetes-Secret, das du aus einem Manifest anlegst, hat seinen Wert vorher irgendwo gelagert, sei es im Git-Repository, in einer CI-Variable oder auf dem Rechner, der das Manifest gerendert hat. Der External Secrets Operator (ESO) dreht die Richtung um. Im Repository steht nur noch eine Referenz, den Wert liest ein Controller im Cluster aus einem externen Store und legt daraus ein normales Kubernetes-Secret an. Dass base64 keine Verschlüsselung ist, war Thema des Artikels „Kubernetes-Secrets: warum base64 kein Schutz ist“ in dieser Reihe im Juni. Hier geht es um die Folgefrage, woher der Wert kommt und wer ihn rotiert.

Was ESO tatsächlich tut

ESO erweitert Kubernetes um Custom Resources, die beschreiben, wo Secrets liegen und wie sie synchronisiert werden. Drei Ressourcen tragen das Modell. Ein SecretStore (auf einen Namespace begrenzt) enthält Zugang und Authentifizierung zum Backend. Ein ClusterSecretStore leistet dasselbe clusterweit. Ein ExternalSecret (ebenfalls namespaced) sagt, welche Daten geholt werden, und verweist auf einen der beiden Stores.

Der Controller sucht den Store, baut einen Client mit dessen Zugangsdaten, holt die Werte, legt das Kubernetes-Secret an und hält es danach synchron. Der Fluss ist ein Pull. Der Cluster fragt beim Store an, der Store schreibt nichts in den Cluster.

Installation

helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets \
  -n external-secrets --create-namespace

Die Beispiele unten nutzen external-secrets.io/v1, so führt es die aktuelle Dokumentation.

Zugang zum Vault einrichten

apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
  name: vault-backend
  namespace: my-namespace
spec:
  provider:
    vault:
      server: "https://vault.example.com"
      path: "secret"        # KV-Mount
      version: "v2"         # KV-Engine v1 oder v2
      auth:
        kubernetes:
          mountPath: "kubernetes"
          role: "my-app"
          serviceAccountRef:
            name: "my-sa"

Der Vault-Provider unterstützt mehrere Anmeldewege, darunter Token, AppRole, Kubernetes, LDAP/UserPass, JWT/OIDC, AWS IAM und TLS-Zertifikate. Bei der Token-Variante liegt ein statischer Token als Kubernetes-Secret im Cluster. Das ist selbst wieder ein Secret, das jemand rotieren muss. Die Kubernetes-Authentifizierung über den ServiceAccount vermeidet das. Die Rolle my-app muss dafür auf der Vault-Seite existieren. Statt Vault funktioniert derselbe Aufbau mit AWS Secrets Manager (service: SecretsManager).

Das Secret anfordern

Den Wert legst du im Vault ab, etwa mit vault kv put secret/my-app password=CHANGE_ME. Im Cluster beschreibst du nur noch die Referenz.

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: my-app-credentials
  namespace: my-namespace
spec:
  refreshInterval: "1h0m0s"
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: my-app-credentials
    creationPolicy: Owner
  data:
    - secretKey: db-password
      remoteRef:
        key: my-app
        property: password

Im Manifest steht kein Wert, nur Key und Property. Der key ist relativ zum path des Stores. Ein Secret unter secret/my-app steht als my-app im remoteRef. Ob der Sync geklappt hat, zeigt der Status der Ressource:

kubectl get externalsecret my-app-credentials -n my-namespace \
  -o jsonpath='{.status.conditions[?(@.type=="Ready")].reason}'
# bei Erfolg: SecretSynced

Rotation und Löschen

refreshInterval legt fest, wie oft der Controller den Wert neu liest. Ohne Angabe gilt 1h0m0s. Mit 0s wird das Secret einmal angelegt und danach nie mehr aktualisiert. Ändert sich der Wert im Vault, kommt er beim nächsten Abgleich im Kubernetes-Secret an. Ob deine Anwendung ihn sieht, entscheidet die Art der Einbindung. Als Umgebungsvariable liest der Container den neuen Wert erst nach einem Neustart. Als Volume-Mount aktualisiert der kubelet die Datei automatisch, mit Verzögerung. Bei subPath-Mounts kommt gar nichts an.

Zwei Felder regeln das Löschen, und sie meinen Verschiedenes. creationPolicy bestimmt, wem das Secret gehört. Standard ist Owner. Wird die ExternalSecret-Ressource gelöscht, verschwindet dann auch das Secret. Orphan legt das Secret an, ohne einen Owner zu setzen, und lässt es beim Löschen stehen. Merge mischt Felder in ein bestehendes Secret, None legt nichts an.

deletionPolicy betrifft dagegen den Store. Standard ist Retain. Werden die Werte im Vault gelöscht, bleibt das Kubernetes-Secret bestehen. Bei Delete entfernt ESO das Secret, sobald alle Datenfelder beim Provider fehlen, bei Merge nur die betroffenen Schlüssel. Wer im Vault aufräumt, hat den Wert bei den Standardwerten damit noch nicht aus dem Cluster entfernt.

Ein Store für mehrere Namespaces

Ein SecretStore kann keine Ressourcen aus anderen Namespaces referenzieren. Ein ClusterSecretStore lässt sich von ExternalSecret-Ressourcen in jedem Namespace nutzen, dort steht dann kind: ClusterSecretStore in der secretStoreRef. Über conditions (Namespace-Liste, Selektor oder Regex) grenzt du ein, wer ihn verwenden darf. Ohne diese Grenze reicht ein ExternalSecret in irgendeinem Namespace, um die Zugriffsrechte des Stores zu nutzen. Bei Anmeldung per ServiceAccount oder Secret-Referenz verlangt die Dokumentation für den ClusterSecretStore zusätzlich eine namespace-Angabe.

Was ESO nicht löst

Nach dem Sync existiert ein gewöhnliches Kubernetes-Secret, und das gilt Kubernetes zufolge standardmäßig als unverschlüsselt im etcd gespeichert. ESO ersetzt deshalb weder die Verschlüsselung at rest (EncryptionConfiguration, gegebenenfalls mit KMS-Provider) noch RBAC. Wer im Namespace Secrets lesen darf, liest auch den synchronisierten Wert. Was sich verschiebt, sind Quelle der Wahrheit, Rotation und Repository-Inhalt. Der Wert entsteht und ändert sich an einer Stelle, und in Git oder in der CI-Konfiguration liegt keine Kopie mehr.

Dazu kommt eine neue Vertrauensgrenze. Der Controller braucht Zugriff auf den Store, und wer einen breit berechtigten ClusterSecretStore nutzen darf, erbt diesen Zugriff. Rolle im Vault und conditions gehören deshalb ebenso auf Least Privilege wie jeder andere Zugang.

Einordnung

ESO verlegt das Problem vom Manifest in den Store und gibt dir dort Rotation und eine zentrale Quelle. Die Kosten sind ein zusätzlicher Controller und eine zusätzliche Abhängigkeit vom Store im Betrieb. Den Schutz des fertigen Secrets im Cluster übernehmen weiter Verschlüsselung at rest und RBAC.

Fragen oder Anmerkungen zu diesem Setup: mm@mhm-dl.de.

Über das Anmeldeformular für den Newsletter trägst du deine E-Mail-Adresse ein, kreuzt die Einwilligung an und bestätigst die Anmeldung danach per Mail. Mit der Anmeldung willigst du ein, dass der Newsletter unter anderem folgende Themen behandelt: Schulungen zu Docker, Kubernetes, CI/CD, Git-Workflows und DevSecOps-Werkzeugen sowie die Anforderungen aus NIS2, CRA und DORA und deren technische Umsetzung.

Hinterlasse einen Kommentar

Diese Seite verwendet Akismet, um Spam zu reduzieren. Erfahre, wie deine Kommentardaten verarbeitet werden..