pod-cert-signer — ein Signer für Pod-Zertifikate, der CA-Schlüssel bleibt in KMS.
Seit Kubernetes 1.37 fordert ein Pod sein X.509-Zertifikat über ein projiziertes Volume an. Das Kubelet erzeugt den Schlüssel, ein Signer entscheidet. pod-cert-signer prüft jede Anfrage gegen eine Policy, die standardmäßig ablehnt. Er signiert mit einem CA-Schlüssel, der AWS KMS nie verlässt, und sieht keinen privaten Schlüssel, weder den des Pods noch den eigenen.
- Im Einsatz: seit September 2026 in meinem Cluster, für sieben CrowdSec-Agenten, die sich statt mit Passwort mit Zertifikat anmelden
- Image:
ghcr.io/ohartwig/pod-cert-signer:0, amd64 und arm64, keyless signiert - Voraussetzung: Kubernetes 1.37 und ein KMS-Schlüssel
ECC_NIST_P256 - Lizenz: Apache-2.0 · kein client-go, das AWS-SDK nur für KMS
Das zweite Exemplar
Bisher
- Passwort oder API-Key liegt beim Client und beim Server, als Secret in etcd und im Backup
- Laufen die Kopien auseinander, gibt es 401: je nach Software läuft sie dann ungeschützt weiter, beendet sich oder meldet still nichts mehr
- Rotieren heißt beide Seiten gleichzeitig anfassen, also rotiert niemand
- Wer welchen Schlüssel hat, steht nirgends an einer Stelle
Mit pod-cert-signer
- Der Server vertraut einer CA, der Client weist sich mit einem Zertifikat aus, dessen Schlüssel nur das Kubelet seines Knotens kannte
- Ein falsches oder abgelaufenes Zertifikat scheitert im Handshake, sichtbar auf beiden Seiten
- Das Kubelet erneuert vor dem Ablauf, zu dem Anteil der Laufzeit, den die Policy setzt; niemand rotiert von Hand
- Wer was bekommt, steht in einer Policy-Datei, geändert per Review
Passt das zu meinem Cluster?
Nutze es wenn …
- dein Cluster Kubernetes 1.37 oder neuer fährt und AWS KMS erreicht
- Dienste im Cluster sich mit Passwörtern oder API-Keys anmelden und die Software Client-Zertifikate kann
- du Identitäten brauchst, die Namensräume überqueren, ohne eine CA als Secret im Cluster
- du lieber eine kleine Komponente liest als eine große konfigurierst
Nutze es nicht wenn …
- dein Cluster älter als 1.37 ist
- dein CA-Schlüssel in Vault, einem HSM oder einem anderen KMS liegen soll: unterstützt ist nur AWS KMS, ein anderes Backend braucht einen eigenen
crypto.Signer - du Zertifikate für öffentliche Namen brauchst: die Name-Constraints lassen nur Cluster-Namen zu
- deine Software ihr Zertifikat einmal liest und sich nicht zum Neuladen bewegen lässt
Warum ein eigener Signer
Kubernetes 1.37 hat Pod-Zertifikate stabil gemacht, aber keinen Signer mitgeliefert. Die Unterstützung in cert-manager ist seit Januar offen (cert-manager#8378). Ich brauchte Zertifikate für die CrowdSec-Agenten in meinem Cluster, die aus fremden Namensräumen mit der zentralen LAPI sprechen, und keine weitere geteilte CA im Cluster. Wie die Umstellung lief, mit allen Hindernissen, steht im Praxisbericht.
Der Signer ist klein gehalten, weil er die privilegierteste Komponente ist. Die Kubernetes-API spricht er mit ein paar hundert Zeilen auf der Standardbibliothek statt mit client-go. Das AWS-SDK nutzt er nur für Credentials und KMS: Authentifizierung schreibt man nicht selbst. Getestet wird gegen einen Fake-API-Server, der das Watch-Protokoll spricht und die Zeitregeln der API durchsetzt. Ein Mutationslauf entfernt zehn Regeln einzeln, und jede Mutante muss rot werden.
So bekommt ein Pod sein Zertifikat
Das CA-Zertifikat erzeugt der Signer einmal selbst, mit dem Schlüssel in KMS. Es trägt kritische Name-Constraints: nur .svc, .svc.cluster.local und die eigene Trust-Domain.
KMS_KEY_ID=alias/pod-cert-signer pod-cert-signer init \
-cn "Example workload CA" -trust-domain example.com > ca.crt
Ein Pod fordert sein Zertifikat mit einem Volume an:
volumes:
- name: tls
projected:
sources:
- podCertificate:
signerName: example.com/workload
keyType: ECDSAP256
keyPath: tls.key
certificateChainPath: tls.crt
- clusterTrustBundle:
signerName: example.com/workload
labelSelector: {}
path: ca.crt
Der Pod startet erst, wenn das Zertifikat ausgestellt ist. Wer was bekommt, steht in einer JSON-Policy, gemountet aus einer ConfigMap:
{"namespace": "team-a", "serviceAccount": "api", "usage": "server",
"dnsNames": ["api.team-a.svc", "api.team-a.svc.cluster.local"]}
Alles, was nicht gewährt ist, wird abgelehnt, mit einem von vier Gründen: NoGrant, UnsupportedKeyType, DNSNameNotGranted, InvalidStubPKCS10Request. Selbst ein umgangener Policy-Check kann wegen der Name-Constraints kein Zertifikat für einen öffentlichen Namen ausstellen.
Die Dokumentation
Schnellstart und Policy
KMS-Schlüssel, CA-Zertifikat, Deployment, erster Grant: vier Schritte. Dazu jedes Feld der Policy, das Zertifikatsprofil und die Metriken.
Entscheidungen
Warum KMS, warum keine Zwischen-CA, warum zwei Replikas ohne Leader-Election, warum klassische Signaturen. Und die Regeln, mit denen die API einen Status ablehnt.
Beispiel-Manifeste
Namespace, RBAC, Policy-ConfigMap, ClusterTrustBundle, Deployment, ein gehärteter Client-Pod und die IAM-Policy für den KMS-Schlüssel.
Container-Image
linux/amd64 und linux/arm64
ghcr.io/ohartwig/pod-cert-signer:0: Wolfi mit CA-Zertifikaten und dem Binary, sonst nichts. Gebaut aus den signierten Release-Binaries, gescannt, keyless signiert.
GitHub
Linux, amd64 und arm64
Quellcode, Releases mit Binaries, SHA256SUMS und cosign-Signatur, Issues. Apache-2.0.
Dienste, die sich ein Passwort teilen?
Ich helfe beim Umstieg auf Pod-Zertifikate: erst ein Dienstpaar, mit Rückweg, dann der Rest.