Ein docker pull prüft nicht, wer ein Image gebaut hat. Er prüft, ob die Registry unter dem angegebenen Namen und Tag etwas ausliefert, das sich herunterladen lässt. Kubernetes verhält sich beim Deployment genauso: das Kubelet zieht das Image, das im Pod-Spec steht, unabhängig davon, ob es aus deiner eigenen Pipeline stammt oder von jemandem, der denselben Namen und Tag in eine erreichbare Registry gepusht hat. In den letzten beiden Artikeln dieser Serie ging es um cosign zum Signieren von Images und um Kyverno als Admission Controller für Policy-Regeln. Heute führe ich beides zusammen: eine ClusterPolicy, die unsignierte Images blockiert, bevor der Pod überhaupt startet.
Warum Signieren allein nichts erzwingt
cosign sign hinterlegt eine Signatur zum Image-Digest in der Registry, entweder mit einem privaten Schlüssel oder keyless über ein von Fulcio ausgestelltes Kurzzeitzertifikat. Das ist die Herkunftsangabe. Nur ruft nichts im normalen Kubernetes-Deploy-Pfad von sich aus cosign verify auf. Wer Schreibzugriff auf deine Registry oder deinen Namespace bekommt, kann ein unsigniertes oder falsch signiertes Image pushen, und kubectl apply nimmt es ohne Rückfrage an. Die Signatur existiert, geprüft wird sie an der entscheidenden Stelle nicht: beim Start des Pods.
Kyverno verifyImages als Durchsetzungspunkt
Kyverno klinkt sich als Admission Controller genau in diesen Moment ein. Eine verifyImages-Regel prüft bei jedem Pod, ob die referenzierten Images eine gültige Signatur tragen, bevor die Admission durchgeht. Läuft die Policy im Enforce-Modus, schlägt der Request fehl, wenn die Signatur fehlt oder nicht zum hinterlegten Schlüssel passt.
apiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: verify-image-signaturesspec: validationFailureAction: Enforce background: false rules: - name: verify-signature match: any: - resources: kinds: - Pod verifyImages: - imageReferences: - "registry.example.com/apps/*" attestors: - count: 1 entries: - keys: publicKeys: |- -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE... -----END PUBLIC KEY-----
background: false ist hier kein Stilfehler. verifyImages arbeitet nur zum Admission-Zeitpunkt, ein Background-Scan über bereits laufende Ressourcen ergibt für Signaturprüfung keinen Sinn, weil Kyverno dafür das Image erneut gegen die Registry auflösen müsste.
Wer eine solche Policy zum ersten Mal produktiv einführt, sollte nicht sofort mit validationFailureAction: Enforce starten. Im Audit-Modus protokolliert Kyverno jeden Verstoß, ohne den Request zu blockieren, dadurch siehst du zuerst, wie viele bereits laufende oder frisch deployte Workloads überhaupt unsignierte Images verwenden. Erst wenn diese Liste auf null oder auf bewusst ausgenommene Ausnahmen heruntergeht, macht der Wechsel auf Enforce Sinn. Ohne diesen Zwischenschritt reißt eine frisch aktivierte Policy im schlimmsten Fall bestehende Deployments ab, die niemand vorher auf Signaturpflicht geprüft hat.
Keyless verifizieren, ohne Schlüssel zu verwalten
Ein statischer Public Key bedeutet auch eine statische Schlüsselverwaltung: wo liegt der private Schlüssel, wer darf ihn benutzen, was passiert bei Rotation. Keyless Signing mit cosign umgeht das über kurzlebige Zertifikate von Fulcio, gebunden an eine OIDC-Identität, zum Beispiel den Workflow einer CI-Pipeline. Die Kyverno-Policy prüft dann nicht mehr gegen einen Schlüssel, sondern gegen Issuer und Subject dieser Identität.
verifyImages:
- imageReferences:
- "registry.example.com/apps/*"
attestors:
- count: 1
entries:
- keyless:
subject: "https://github.com/meine-org/meine-pipeline/.github/workflows/build.yml@refs/heads/main"
issuer: "https://token.actions.githubusercontent.com"
Damit lässt sich ein Image nur dann als vertrauenswürdig durchwinken, wenn es exakt dieser Workflow auf exakt diesem Branch signiert hat. Ein Fork oder ein anderer Branch erzeugt eine andere Subject-Angabe und fällt bei der Prüfung durch.
Signieren und Durchsetzung testen
Der Ablauf in der Pipeline bleibt derselbe wie im cosign-Artikel: Image bauen, pushen, signieren.
# mit statischem Schlüsselcosign sign --key cosign.key registry.example.com/apps/backend:1.4.0# keyless über OIDC, ohne interaktive Rückfragecosign sign --yes registry.example.com/apps/backend:1.4.0# lokale Verifikation vor dem Deploycosign verify --key cosign.pub registry.example.com/apps/backend:1.4.0
Versuchst du danach, ein unsigniertes Image in einen Namespace mit aktiver Policy zu deployen, bricht der Request am Admission Webhook ab, nicht erst beim Pod-Status:
$ kubectl run test --image=registry.example.com/apps/backend:unsignedError from server: admission webhook "validate.kyverno.svc-fail" denied the request:resource Pod/default/test was blocked due to the following policiesverify-image-signatures: verify-signature: 'image verification failed for registry.example.com/apps/backend:unsigned: .attestors[0].entries[0]: no matching signatures found'
Die genaue Formulierung der Fehlermeldung hängt von der Kyverno-Version ab, das Prinzip nicht: kein gültiger Nachweis, kein laufender Pod.
Was die Signatur nicht beweist
Eine Signatur beantwortet die Frage nach der Herkunft, nicht die Frage nach dem Inhalt. Sie sagt: dieses Image kommt exakt von diesem Schlüssel oder exakt von dieser CI-Identität. Sie sagt nichts darüber, ob im Image eine veraltete Bibliothek mit bekannten Schwachstellen steckt. Eine kompromittierte Pipeline signiert ein kompromittiertes Image genauso bereitwillig wie ein sauberes, die Signatur bleibt in beiden Fällen technisch gültig. Kyverno verifyImages ersetzt deshalb kein Scanning in der Pipeline, es ist eine zusätzliche, orthogonale Kontrolle: Herkunft erzwingen und Inhalt scannen sind zwei getrennte Fragen mit zwei getrennten Werkzeugen.
Wer den Bogen von der einzelnen Policy zur unternehmensweiten Lieferketten-Perspektive schlagen will, wird auf digital-business.blog fündig, dort ordne ich solche Bausteine regelmäßig in den größeren Zusammenhang ein.
Hinterlasse einen Kommentar