Cosign und SLSA: Images signieren
Keyless Signing und Verification in der Pipeline.
von PlatPrep
Im Dezember 2020 machte SolarWinds eine alte Lektion schmerzhaft neu: ein kompromittierter Build-Prozess, ein signiertes aber manipuliertes Artefakt, verteilt an tausende Kunden. Supply-Chain-Angriffe sind keine Theorie.
Für Kubernetes-Umgebungen stellt sich die konkrete Frage: wie weißt du, dass das Image, das du deployest, das Image ist, das deine Pipeline gebaut hat — und nicht etwas, das jemand mit gestohlenen Registry-Credentials dazwischengeschoben hat?
Cosign, Teil des Sigstore-Projekts, beantwortet diese Frage. Das Grundprinzip: Nach dem Build wird das Image signiert, die Signatur landet als eigenes OCI-Artefakt in der Registry neben dem Image. Wer deployed, verifiziert vorher Signatur und Identität des Signierers.
Keyless: Signieren ohne Schlüsselverwaltung
Der klassische Weg — ein privates Keypair generieren, den Private Key als CI-Secret hinterlegen — funktioniert, schafft aber genau das Problem wieder, das man loswerden wollte: ein langlebiges Geheimnis, das gestohlen, geleakt oder vergessen werden kann.
Keyless Signing löst das elegant. Beim Signieren holt sich Cosign von Fulcio (der Sigstore-CA) ein Zertifikat, das nur wenige Minuten gültig ist und an eine OIDC-Identität gebunden wird — bei GitHub Actions ist das die Identität des konkreten Workflows, etwa https://github.com/org/repo/.github/workflows/release.yml@refs/heads/main. Der private Schlüssel ist ephemer und wird nach der Signierung verworfen. Damit die Signatur später noch prüfbar ist, obwohl das Zertifikat längst abgelaufen ist, schreibt Cosign einen Eintrag in Rekor, ein öffentliches, append-only Transparency-Log. Der Rekor-Eintrag beweist: Diese Signatur entstand, während das Zertifikat gültig war.
In der GitHub-Actions-Pipeline sieht das so aus:
permissions:
id-token: write # OIDC-Token für Fulcio
packages: write
steps:
- name: Image signieren
run: cosign sign --yes ghcr.io/org/app@${{ steps.build.outputs.digest }}
Ein Detail mit Sicherheitsrelevanz: signiert (und deployed) wird der Digest, nicht der Tag. Tags sind beweglich — v1.2.3 kann morgen auf ein anderes Image zeigen. Der Digest ist der kryptographische Fingerabdruck des Inhalts.
Verifizieren: die Hälfte, die oft fehlt
Eine Signatur, die niemand prüft, ist Dekoration. Die Verifikation gehört dorthin, wo Images in den Cluster gelangen — als Admission-Control. Manuell sieht die Prüfung so aus:
cosign verify ghcr.io/org/app@sha256:abc123... \
--certificate-identity-regexp '^https://github.com/org/app/\.github/workflows/release\.yml@.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
Die beiden Flags sind der Kern: Es reicht nicht zu prüfen, dass signiert wurde — entscheidend ist, wer signiert hat. Ohne Identity-Einschränkung akzeptierst du jede gültige Sigstore-Signatur, auch die eines Angreifers mit eigenem GitHub-Account. Im Cluster übernimmt diese Prüfung ein Admission-Controller: Kyverno bringt mit verifyImages eine direkte Cosign-Integration mit, alternativ gibt es den Sigstore Policy Controller. Die Policy sagt dann: Images aus ghcr.io/org/* müssen vom Release-Workflow der Organisation signiert sein, sonst wird der Pod abgelehnt. Wie man solche Policies baut, zeigt der Artikel zu Kyverno.
Was dabei in der Praxis schiefgeht: Der Admission-Webhook braucht für jede Verifikation Registry-Zugriff — fällt die Registry aus oder drosselt sie, scheitern plötzlich Pod-Starts clusterweit. Eine bewusste Failure-Policy (fail-open vs. fail-closed) und Ausnahmen für System-Namespaces sind Pflicht, sonst blockiert die Security-Maßnahme im Ernstfall das Recovery. Und: Bestandsimages sind unsigniert — inklusive der Third-Party-Images von Ingress-Controller bis Monitoring-Stack, die nie durch die eigene Pipeline gelaufen sind. Für die gibt es zwei Wege: in die eigene Registry spiegeln und dort nachsignieren, oder explizit per Policy-Ausnahme zulassen. Die Policy zunächst im Audit-Modus fahren, Verstöße abarbeiten, dann erst enforcen.
Wo SLSA ins Bild kommt
Signieren beweist, wer ein Image freigegeben hat — nicht, wie es entstanden ist. Genau dort setzt SLSA (Supply-chain Levels for Software Artifacts) an. Kern ist die Provenance: eine signierte Attestation, die beschreibt, aus welchem Repo, welchem Commit und mit welchem Build-Prozess ein Artefakt gebaut wurde. Die SLSA-Build-Levels staffeln das Vertrauen: Level 1 heißt, Provenance existiert; Level 2 verlangt, dass sie vom Build-Service signiert ist und nicht vom Build-Job selbst gefälscht werden kann; Level 3 fordert zusätzlich, dass Build-Jobs voneinander isoliert sind und keinen Zugriff auf das Signier-Material haben.
Praktisch erzeugst du Provenance auf GitHub mit dem offiziellen slsa-github-generator oder der attest-build-provenance-Action; die Attestation landet als OCI-Artefakt neben dem Image und ist mit cosign verify-attestation prüfbar. Damit lässt sich bei Admission nicht nur “signiert von uns” durchsetzen, sondern “gebaut aus dem main-Branch dieses Repos” — das schließt den Angriffsvektor, dass jemand mit Workflow-Zugriff ein lokal gebautes Image durch die legitime Pipeline signieren lässt.
Der realistische Einstieg ist trotzdem klein: Images im CI signieren, Verifikation per Admission-Policy, beides zusammen an einem Tag implementierbar. Provenance und SBOM-Prüfung (siehe SBOMs mit Trivy) kommen als zweite Stufe. Den größeren Rahmen — von Registry-Härtung bis Dependency-Management — behandelt der Lernpfad Supply-Chain-Security.