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.
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 – ausser 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.
| 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 | ausserhalb (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 (ausser 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