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.
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 cluster→spec.encryption.type: aescbc(oderaesgcm) - der kube-apiserver-Operator rollt eine
EncryptionConfigurationaus, verschlüsselt Secrets, ConfigMaps und Token, Schlüssel inopenshift-config-managed, Rotation wöchentlich - ein Migrations-Job schreibt alle bestehenden Objekte neu – prüfen:
oc get apiserver cluster -o yamlFeldstatus.encryption - der Klartext-Weg:
oc get secret db -o jsonpath='{.data.password}' | base64 -doderoc extract secret/db --to=./dir oc describe podzeigt nur env-Namen, nicht die Werte – aber ein Crash-Dump oderoc get secret -o yaml > all.yamlleakt allesimmutable: trueschü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ßerfsGroup/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 manuelleEncryptionConfiguration
type | erzwingt | wofür |
|---|---|---|
Opaque | nichts – beliebige Schlüssel | der Normalfall: DB-Passwort, API-Token |
kubernetes.io/tls | tls.crt + tls.key | Route-Zertifikate, serving-cert (F5) |
kubernetes.io/dockerconfigjson | .dockerconfigjson | Pull-Secret für eine private Registry (Schritt 5) |
kubernetes.io/basic-auth | username + password | z. B. ein Git-Repo-Zugang für einen BuildConfig |
kubernetes.io/ssh-auth | ssh-privatekey | ein Deploy-Key |
kubernetes.io/service-account-token | Annotation kubernetes.io/service-account.name | ein nicht ablaufendes Token für eine SA (A6) – z. B. pipeline-token |
# 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 env | als Volume-Mount | |
|---|---|---|
| im Manifest | envFrom.secretRef oder valueFrom.secretKeyRef | volumes[].secret + volumeMounts |
| Änderung am Secret | wirkt nicht – der Wert ist beim Pod-Start eingefroren | die Datei aktualisiert sich nach ~1 min (kubelet-Sync) |
| Ausnahme | – | subPath-Mounts aktualisieren nicht |
| sichtbar in | oc set env --list, ein Crash-Dump, /proc/<pid>/environ | nur 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:
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).
# 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.
# 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
| Wert | bedeutet |
|---|---|
aescbc | AES-CBC mit PKCS#7, der bewährte Default |
aesgcm | AES-GCM – muss man manuell rotieren, sonst Nonce-Reuse-Risiko |
(leer) / identity | keine 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.
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 inopenshift-config/pull-secret - die
default-SA jedes Namespace hat schon ein Pull-Secret für die interne Registry (C5) –imagePullSecretssteht 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
| Fall | wie |
|---|---|
| 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 Namespace | Secret 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 ziehen | der ziehenden SA die Rolle system:image-puller auf den Quell-Namespace geben |
# 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.
oc create secret ... --dry-run=client -o yaml | oc apply -f -env-Konsument übernimmt nichts – oc rollout restart deploy/<dienst> -n library. Ein Mount holt die neue Datei nach ~1 min, aber die App muss sie neu lesenAutomatisch: 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.
| Modell | was in Git liegt | wer entschlüsselt | der Schlüssel liegt |
|---|---|---|---|
| Sealed Secrets (Bitnami) | ein SealedSecret-CR, mit dem Public Key des Controllers verschlüsselt | der Controller im Cluster, macht daraus ein normales Secret | im Cluster (im Snapshot sichern) |
| External Secrets Operator | ein ExternalSecret-CR, der nur einen Verweis trägt | der Operator holt den Wert aus Vault / AWS SM / Azure KV und synct ihn in ein Secret | außerhalb (der Store) |
| SOPS (+ age/PGP) | eine verschlüsselte Werte-Datei (Werte chiffriert, Schlüsselnamen lesbar) | ein GitOps-Plugin (Argo CD / Flux) beim Apply | beim 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.
| Secret | Herkunft |
|---|---|
6× <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-postgresql | vom 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
Opaque ist der Normalfallenv = eingefroren (Rotation braucht rollout restart), Mount = ~1 min live (außer subPath)apiserver cluster → encryption.type: aescbc verschlüsselt etcd. Schützt das Backup, nicht vor RBACget secret hat, hat den Klartext (oc extract) – deshalb view statt edit für alle, die es nicht sehen sollenoc create secret --dry-run