← Übersicht  ·  Skripte & Dateien  ·  cloud-host · OpenShift · Betrieb

Secrets in OpenShift

Die Zeichnungen dazu stehen hier bei den jeweiligen Schritten. Hier spielst du es von Hand durch: Typen anlegen, als env und als Mount konsumieren, den Klartext-Weg, Verschlüsselung at-rest einschalten, Pull-Secrets, Rotation ohne Ausfall – und der Weg raus aus Git.

Stand: 4. September 2026 OKD 4.x / OCP 4.x ns library

Voraussetzung: ein Cluster mit oc login, Namespace library (oc project library). Der at-rest-Teil (Schritt 4) ändert Cluster-Konfiguration und dauert einige Minuten – auf der SNO problemlos, aber nicht mitten in einer anderen Aufgabe.

1 Die Typen

Der type eines Secrets erzwingt, welche Schlüssel drin sein müssen – und wofür OpenShift es benutzen darf.

Ein Secret ist kein monolithisches Ding: der Typ legt fest, welche Schlüssel drin sein müssen und wofür es taugt. Was in etcd liegt, ist per Default nur base64 – echte Verschlüsselung ist ein Schalter. Und der Weg zum Klartext ist ein Einzeiler, den jeder Auditor kennt.

Der Typ macht die Regeln. base64 ist Kodierung, kein Schutz. die sechs Typen Typ · Pflicht-Schlüssel · wofür Opaquefrei, kein Schema – alles andere kubernetes.io/tlstls.crt, tls.key – Routes (B4), serving-certs (F5) …/dockerconfigjson.dockerconfigjson – Image-Pull (B10) …/service-account-tokentoken, ca.crt, namespace – alter SA-Token (A6) …/basic-authusername, password …/ssh-authssh-privatekey der Typ erzwingt die Schlüssel – ein tls-Secret ohne tls.key wird abgewiesen immutable: true → nur ersetzen · max 1 MiB · namespaced: ein Pod sieht nur die seines Namespace projiziertes Volume: mehrere Secrets/ConfigMaps + SA-Token in einen Baum, je Quelle ein Pfad was wirklich in etcd liegt per Default: base64 – jedes etcd-Backup = Klartext echte Verschlüsselung einschalten: oc edit apiserver cluster spec.encryption.type: aescbc # oder aesgcm der kube-apiserver-Operator rollt eine EncryptionConfiguration aus, verschlüsselt Secrets, ConfigMaps und Token Schlüssel in openshift-config-managed, Rotation wöchentlich ein Migrations-Job schreibt alle bestehenden Objekte neu prüfen: oc get apiserver cluster -o yaml Feld status.encryption nicht dasselbe wie TLS im Transit – das macht der API-Server sowieso Alternative gegen Backup-Leak: etcd-Backup selbst verschlüsseln (G7) der Klartext-Weg, den jeder Auditor kennt oc get secret db -o jsonpath='{.data.password}' | base64 -d oc extract secret/db --to=./dir jeder mit get secrets im Namespace sieht alles im Klartext oc describe pod zeigt env-Variablen-Namen, nicht die Werte – aber ein Crash-Dump kann sie leaken Vorsicht: oc get secret -o yaml > all.yaml schreibt alle Klartexte in eine Datei Bibliothek / cloud-host jedes Secret gehört genau einem der 8 Dienste (DB, Keycloak, Kafka…) alles im Namespace library – die Trennung pro Namespace begrenzt den Schaden ein kompromittierter Pod kommt nur an die Secrets seines Namespace Herkunft, Pull-Secrets, Rotation, raus aus Git: B10 welche SA welches Secret nutzen darf: über die Pod-Spec, nicht über RBAC am Secret Konsum (aus B8): env = bei Start eingefroren, Volume = live (tmpfs, Dateien 0644, root – außer fsGroup/defaultMode) Merksatz: der Typ bestimmt die Schlüssel, base64 schützt nichts, at-rest-Verschlüsselung ist ein Cluster-Schalter OCP-Unterschied: die apiserver/cluster-Verschlüsselung mit automatischer Rotation ist OpenShift-Komfort – das Kubernetes-Original ist manuelle EncryptionConfiguration.
Typ, Speicher, Klartext-Weg. Sechs Secret-Typen, jeder mit erzwungenen Schlüsseln (tls.crt/tls.key, .dockerconfigjson, …). In etcd steht per Default nur base64 – ein Backup ist Klartext; echte Verschlüsselung ist der Schalter spec.encryption.type: aescbc am apiserver/cluster. Und der Weg zum Klartext ist oc get secret … -o jsonpath | base64 -d, den jeder mit get secrets gehen kann.
  • der Typ erzwingt die Schlüssel: Opaque (frei), kubernetes.io/tls (tls.crt/tls.key), …/dockerconfigjson, …/service-account-token, …/basic-auth, …/ssh-auth
  • in etcd liegt per Default nur base64 – jedes etcd-Backup und jedes Disk-Image ist Klartext
  • echte Verschlüsselung at-rest einschalten: oc edit apiserver clusterspec.encryption.type: aescbc (oder aesgcm)
  • der kube-apiserver-Operator rollt eine EncryptionConfiguration aus, verschlüsselt Secrets, ConfigMaps und Token, Schlüssel in openshift-config-managed, Rotation wöchentlich
  • ein Migrations-Job schreibt alle bestehenden Objekte neu – prüfen: oc get apiserver cluster -o yaml Feld status.encryption
  • der Klartext-Weg: oc get secret db -o jsonpath='{.data.password}' | base64 -d oder oc extract secret/db --to=./dir
  • oc describe pod zeigt nur env-Namen, nicht die Werte – aber ein Crash-Dump oder oc get secret -o yaml > all.yaml leakt alles
  • immutable: true schützt vor versehentlicher Änderung, Größe max 1 MiB (etcd)
  • Secrets sind namespaced – ein Pod sieht nur die seines Namespace, das begrenzt den Schaden bei einem kompromittierten Pod
  • projiziertes Volume: mehrere Secrets/ConfigMaps und der SA-Token in einen Verzeichnisbaum, je Quelle ein Pfad
  • Konsum (B8): env eingefroren, Volume live (tmpfs, Dateien 0644/root – außer fsGroup/defaultMode)
  • Bibliothek: jedes Secret gehört genau einem der 8 Dienste, alles im Namespace library – die Trennung begrenzt den Schaden
  • OCP-Unterschied: die apiserver/cluster-Verschlüsselung mit Auto-Rotation ist OpenShift-Komfort – Kubernetes pur ist manuelle EncryptionConfiguration
typeerzwingtwofür
Opaquenichts – beliebige Schlüsselder Normalfall: DB-Passwort, API-Token
kubernetes.io/tlstls.crt + tls.keyRoute-Zertifikate, serving-cert (F5)
kubernetes.io/dockerconfigjson.dockerconfigjsonPull-Secret für eine private Registry (Schritt 5)
kubernetes.io/basic-authusername + passwordz. B. ein Git-Repo-Zugang für einen BuildConfig
kubernetes.io/ssh-authssh-privatekeyein Deploy-Key
kubernetes.io/service-account-tokenAnnotation kubernetes.io/service-account.nameein nicht ablaufendes Token für eine SA (A6) – z. B. pipeline-token
Laptop · Bash
# Opaque - der einfache Weg:
oc create secret generic catalog-db -n library \
  --from-literal=username=catalog \
  --from-literal=password='s3hr-geheim'

# aus einer Datei (der Schluessel wird der Dateiname):
oc create secret generic app-config -n library --from-file=./application.yaml

# ein TLS-Secret:
oc create secret tls catalog-tls -n library --cert=tls.crt --key=tls.key

# ansehen (Typ + Schluessel-Namen, nicht die Werte):
oc get secret catalog-db -n library -o yaml | grep -E 'type:|^  [a-z]'

immutable: true im Secret schützt vor versehentlicher Änderung (und spart etwas API-Last). Maximalgröße 1 MiB, weil alles in etcd landet.

2 env vs Mount – live

Der praktisch wichtigste Unterschied. Ein Pod nimmt ein Secret auf zwei Wegen, und sie verhalten sich verschieden.

als envals Volume-Mount
im ManifestenvFrom.secretRef oder valueFrom.secretKeyRefvolumes[].secret + volumeMounts
Änderung am Secretwirkt nicht – der Wert ist beim Pod-Start eingefrorendie Datei aktualisiert sich nach ~1 min (kubelet-Sync)
AusnahmesubPath-Mounts aktualisieren nicht
sichtbar inoc set env --list, ein Crash-Dump, /proc/<pid>/environnur als Datei im Container (tmpfs, 0644, root – außer fsGroup/defaultMode)

Zum Selbst-Sehen: ein Secret als Mount in einen Test-Pod, dann den Wert ändern und im Pod warten:

Laptop · Bash
oc create secret generic demo -n library --from-literal=k=eins
oc run demo --image=registry.access.redhat.com/ubi9/ubi-minimal -n library --restart=Never -- \
  sh -c 'while true; do cat /s/k; echo; sleep 5; done'
oc set volume pod/demo -n library --add --type=secret --secret-name=demo --mount-path=/s
oc logs -f demo -n library &
oc create secret generic demo -n library --from-literal=k=zwei --dry-run=client -o yaml | oc apply -f -
# nach ~1 min zeigt das Log 'zwei' - ein env haette weiter 'eins' gezeigt
oc delete pod demo -n library ; oc delete secret demo -n library

Faustregel: Zertifikate und Dateien als Mount (sie sollen sich rotieren lassen), einzelne Werte oft als env (einfacher im Code) – dann aber wissen, dass eine Rotation einen Pod-Neustart braucht (Schritt 6).

3 Der Klartext-Weg

Was in etcd liegt, ist per Default nur base64. Wer get secret darf, hat den Klartext – das ist die Rolle admin/edit, nicht view (B9).

Laptop · Bash
# ein Feld:
oc get secret catalog-db -n library -o jsonpath='{.data.password}' | base64 -d ; echo

# alle Felder in Dateien:
oc extract secret/catalog-db -n library --to=./out --confirm

# eine aenderbare Vorlage (fuer Git NUR nach dem Versiegeln, Schritt 7):
oc create secret generic catalog-db -n library \
  --from-literal=password=neu --dry-run=client -o yaml

oc describe pod zeigt nur die env-Namen, nicht die Werte – aber oc get secret -o yaml > all.yaml oder ein Crash-Dump leakt alles. Deshalb: Secret-YAML nie in ein Ticket, nie in einen Chat, nie unverschlüsselt in Git.

4 Verschlüsselung at-rest

Standardmässig ist jedes etcd-Backup und jedes Disk-Image der Node Klartext. Ein Cluster-Schalter ändert das.

Laptop · Bash
# einschalten:
oc patch apiserver cluster --type=merge -p '{"spec":{"encryption":{"type":"aescbc"}}}'

# der kube-apiserver-Operator rollt eine EncryptionConfiguration aus und
# schreibt alle bestehenden Secrets/ConfigMaps/Token neu. Fortschritt:
oc get apiserver cluster -o jsonpath='{.status.conditions}' | tr ',' '\n' | grep -i encrypt
oc get openshiftapiserver cluster -o jsonpath='{range .status.conditions[?(@.type=="Encrypted")]}{.reason}{"\n"}{end}'
# fertig, wenn reason = EncryptionCompleted
Wertbedeutet
aescbcAES-CBC mit PKCS#7, der bewährte Default
aesgcmAES-GCM – muss man manuell rotieren, sonst Nonce-Reuse-Risiko
(leer) / identitykeine Verschlüsselung (Ausschalten schreibt auch alles neu)

Der Schlüssel liegt in einem Secret im Namespace openshift-config-managed, der Operator rotiert ihn automatisch (wöchentlich). Das ist der OpenShift-Komfort – auf reinem Kubernetes ist das eine EncryptionConfiguration- Datei auf jedem Control-Plane-Node, von Hand.

Wichtig: at-rest schützt vor gestohlenem Backup / gestohlener Disk. Es schützt nicht vor jemandem mit get secret-Recht – der kube-apiserver entschlüsselt beim Lesen. Dafür ist RBAC da.

5 Pull-Secrets

Wie ein Pod ein Image aus einer privaten Registry zieht.

Woher kommt ein Secret? Wie zieht ein Pod ein privates Image? Wie tauscht man einen Wert aus, ohne dass etwas bricht? Und der Kernkonflikt: ein Klartext-Secret gehört nicht in Git, aber die restliche Config schon – drei Werkzeuge lösen das unterschiedlich.

Viele Quellen, ein Secret – und die Frage, wie es nicht in Git landet. Secret im Namespace, von einer dieser Quellen geprüft: oc get secret, Feld type du: oc create secret SA-Token-Controller (A6) service-ca-Operator (F5) cert-manager (ACME/CA) Helm-Chart (Klartext!) CI-Pipeline (Block E) Sealed Secrets / ESO Pull-Secret – damit ein Pod ein privates Image zieht oc create secret docker-registry regcred \ --docker-server=... --docker-username=... --docker-password=... oc secrets link default regcred --for=pull oder global: der Cluster-weite openshift-config/pull-secret die default-SA hat schon eins für die interne Registry (C5) imagePullSecrets steht an der SA oder direkt am Pod Rotation von Hand: Secret ersetzen, dann oc rollout restart env übernimmt sonst nichts, ein Mount holt die Datei, die App muss neu lesen automatisch: tls-Secrets vom service-ca-Operator (F5) cert-manager erneuert und aktualisiert das Secret vor Ablauf ein Reloader (Sidecar/Annotation) löst dann den Rollout aus raus aus Git – ein Klartext-Secret in Git ist base64, also für jeden mit Repo-Zugriff lesbar Sealed Secrets (Bitnami): du verschlüsselst mit dem Public Key des Controllers → SealedSecret-CRD in Git der Controller entschlüsselt im Cluster in ein echtes Secret · clusterspezifisch (der Key liegt im Cluster) External Secrets Operator (ESO): ExternalSecret-CRD referenziert einen Key in Vault / AWS SM / GCP SM / Azure KV der Operator synct ihn in ein Secret · der Wert lebt außerhalb des Clusters SOPS (+ age / PGP): verschlüsselte Werte-Datei in Git, ein GitOps-Plugin (Argo CD, Flux) entschlüsselt beim Apply Vergleich im GitOps-Kontext: E5 · die Vorlage bauen: oc create secret … --dry-run=client -o yaml, dann versiegeln Merksatz: Sealed Secrets = Key im Cluster, ESO = Secret in einem externen Store, SOPS = Key beim Deployer Bibliothek / cloud-host: der konkrete Weg wird im GitOps-Teil (Block E) gebaut – bis dahin Secrets von Hand mit oc create secret OCP-Unterschied: keiner – Sealed Secrets, ESO und SOPS sind Community-Werkzeuge, laufen als Operator/Controller auf jedem Cluster.
Herkunft, Pull, Rotation, Git. Ein Secret kommt von vielen Quellen – von dir, von Operatoren (SA-Token, service-ca, cert-manager), von Helm oder der CI. Ein Pull-Secret hängt man mit oc secrets link … --for=pull an eine SA. Rotation heißt: ersetzen und Pods rollen (bei service-ca-Certs automatisch). Und weil ein Klartext-Secret nicht in Git gehört, gibt es Sealed Secrets (Key im Cluster), den External Secrets Operator (Store außerhalb) und SOPS (Key beim Deployer).
  • Herkunft: du (oc create secret), der SA-Token-Controller (A6), der service-ca-Operator (F5), cert-manager, ein Helm-Chart (Klartext, schlecht), die CI-Pipeline (E), Sealed Secrets / ESO
  • Pull-Secret: oc create secret docker-registry regcred --docker-server=... --docker-username=... --docker-password=...
  • anhängen: oc secrets link default regcred --for=pull (je SA) oder global in openshift-config/pull-secret
  • die default-SA jedes Namespace hat schon ein Pull-Secret für die interne Registry (C5) – imagePullSecrets steht an der SA oder am Pod
  • Rotation von Hand: Secret ersetzen, dann oc rollout restart – env übernimmt sonst nichts, ein Mount holt die Datei, die App muss neu lesen
  • Rotation automatisch: tls-Secrets vom service-ca-Operator (F5), cert-manager erneuert vor Ablauf – ein Reloader löst den Rollout aus
  • warum kein Klartext-Secret in Git: base64 ist lesbar – jeder mit Repo-Zugriff hat die Zugangsdaten
  • Sealed Secrets: du verschlüsselst mit dem Public Key des Controllers → SealedSecret-CRD in Git → Controller entschlüsselt im Cluster (clusterspezifisch)
  • External Secrets Operator: ExternalSecret-CRD referenziert einen Key in Vault / AWS SM / Azure KV → Operator synct ihn in ein Secret (der Wert lebt außerhalb)
  • SOPS (+ age/PGP): verschlüsselte Werte-Datei in Git, ein GitOps-Plugin entschlüsselt beim Apply – Vergleich in E5
  • die Vorlage bauen: oc create secret … --dry-run=client -o yaml, dann versiegeln
  • Merksatz: Sealed Secrets = Key im Cluster, ESO = Store außerhalb, SOPS = Key beim Deployer
  • Bibliothek / cloud-host: der konkrete Weg wird im GitOps-Teil (Block E) gebaut – bis dahin Secrets von Hand
  • OCP-Unterschied: keiner – Sealed Secrets, ESO und SOPS sind Community-Werkzeuge, laufen auf jedem Cluster
Fallwie
die interne Registry (C5)nichts tun – die default- und builder-SA jedes Namespace haben schon ein Pull-Secret dafür
eine private externe Registry, nur für einen NamespaceSecret anlegen + an die SA hängen (siehe unten)
eine private Registry clusterweit (z. B. registry.redhat.io)den globalen pull-secret in openshift-config ergänzen
ein Image aus einem anderen Namespace ziehender ziehenden SA die Rolle system:image-puller auf den Quell-Namespace geben
Laptop · Bash
# private externe Registry, nur ns library:
oc create secret docker-registry regcred -n library \
  --docker-server=ghcr.io --docker-username=USER --docker-password=TOKEN

oc secrets link default regcred --for=pull -n library     # an die default-SA haengen
# oder direkt im Deployment: spec.template.spec.imagePullSecrets: [{name: regcred}]

# clusterweit (registry.redhat.io o.ae.):
oc get secret pull-secret -n openshift-config -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d > ps.json
#   ... ps.json um den neuen Eintrag ergaenzen ...
oc set data secret/pull-secret -n openshift-config --from-file=.dockerconfigjson=ps.json

6 Rotation ohne Ausfall

Einen Secret-Wert austauschen, ohne dass etwas bricht.

1den neuen Wert zusätzlich gültig machen (die DB akzeptiert beide Passwörter, das Cert hat Überlappung) – erst danach das Secret ändern
2Secret ersetzen: oc create secret ... --dry-run=client -o yaml | oc apply -f -
3ein env-Konsument übernimmt nichtsoc rollout restart deploy/<dienst> -n library. Ein Mount holt die neue Datei nach ~1 min, aber die App muss sie neu lesen
4den alten Wert ungültig machen – erst wenn alle Pods neu sind

Automatisch: TLS-Secrets vom service-ca-Operator (F5) und von cert-manager erneuern sich vor Ablauf selbst. Damit der Pod die neue Datei auch benutzt, löst ein Reloader (z. B. stakater/reloader) den rollout restart aus – oder man setzt eine checksum/config-Annotation am Pod-Template, die sich mit dem Secret ändert (macht der Helm-Chart, B12).

7 Raus aus Git

Der Kernkonflikt: die App-Config gehört versioniert in Git – ein Klartext-Secret nicht (base64 ist lesbar, jeder mit Repo-Zugriff hat die Zugangsdaten). Drei Modelle lösen das.

Modellwas in Git liegtwer entschlüsseltder Schlüssel liegt
Sealed Secrets (Bitnami)ein SealedSecret-CR, mit dem Public Key des Controllers verschlüsseltder Controller im Cluster, macht daraus ein normales Secretim Cluster (im Snapshot sichern)
External Secrets Operatorein ExternalSecret-CR, der nur einen Verweis trägtder Operator holt den Wert aus Vault / AWS SM / Azure KV und synct ihn in ein Secretaußerhalb (der Store)
SOPS (+ age/PGP)eine verschlüsselte Werte-Datei (Werte chiffriert, Schlüsselnamen lesbar)ein GitOps-Plugin (Argo CD / Flux) beim Applybeim Deployer (der age-Key)

Die Vorlage baut man immer gleich: oc create secret ... --dry-run=client -o yaml – und dann versiegeln (kubeseal), referenzieren (ESO) oder verschlüsseln (sops -e). Vergleich der drei im Atlas-Diagramm E5, der SOPS-Weg im Compose-Kontext in Secrets-Management.

8 Wie die Bibliothek es macht

Heute noch von Hand – GitOps (und damit ein Modell aus Schritt 7) ist Atlas-Block E.

SecretHerkunft
<svc>-db-credentials (Opaque)die Jenkins-Prod-Stage legt sie per oc create secret ... --dry-run | oc apply an – Werte aus Jenkins-Credentials, nichts in Git
keycloak / keycloak-postgresqlvom Bitnami-Helm-Chart erzeugt (Klartext im Chart-Wert bzw. auto-generiert)
pipeline-token (service-account-token)von Hand angelegt, nicht ablaufendes Token für die SA pipeline – nach sno.ps1 up weg, der Post-up-Hook legt es neu an
die internen TLS-Secrets– gibt es noch nicht, die Dienste reden intern HTTP (der service-ca-Weg wäre F5)

Jedes Secret gehört genau einem der 8 Dienste, alles im Namespace library – ein kompromittierter Pod sieht nur seine eigenen und die des Namespace, nicht die des ganzen Clusters.

+ Die Kurzfassung

Typerzwingt die Schlüssel und den Zweck – Opaque ist der Normalfall
Konsumenv = eingefroren (Rotation braucht rollout restart), Mount = ~1 min live (außer subPath)
at-restDefault ist base64 – apiserver clusterencryption.type: aescbc verschlüsselt etcd. Schützt das Backup, nicht vor RBAC
lesenwer get secret hat, hat den Klartext (oc extract) – deshalb view statt edit für alle, die es nicht sehen sollen
aus GitSealed Secrets (Key im Cluster) · ESO (Store außen) · SOPS (Key beim Deployer) – die Vorlage ist immer oc create secret --dry-run
⌂ Cockpit