Kai Ole Hartwig

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.

Lesen →
Schnellstart und Policy

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.

Lesen →
Entscheidungen

Beispiel-Manifeste

Namespace, RBAC, Policy-ConfigMap, ClusterTrustBundle, Deployment, ein gehärteter Client-Pod und die IAM-Policy für den KMS-Schlüssel.

Ansehen →
Beispiel-Manifeste

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.

Image ansehen →
Container-Image

GitHub

Linux, amd64 und arm64

Quellcode, Releases mit Binaries, SHA256SUMS und cosign-Signatur, Issues. Apache-2.0.

Auf GitHub ansehen →
GitHub

Dienste, die sich ein Passwort teilen?

Ich helfe beim Umstieg auf Pod-Zertifikate: erst ein Dienstpaar, mit Rückweg, dann der Rest.

Anfragen →