Die meisten „DevSecOps-Pipelines", die ich in Teams auditiere, brechen an derselben Stelle: die Checks sind da, laufen aber im warn-and-continue-Modus. Der Scanner findet ein CRITICAL — die Pipeline bleibt grün. Ein halbes Jahr später ist das keine Pipeline mehr, sondern ein Ritual.
Damit eine Security-Pipeline Production tatsächlich blockiert, braucht es zwei Dinge: fünf Stages, jede mit einem policy-getriebenen exit-code 1, und ein zweites Gate im Cluster — beim Admission. Ein CI-Gate ohne Admission-Verifikation ist eine trügerische Sicherheit: CI fängt, was gebaut wurde; der Cluster akzeptiert, was deployt wurde. Diese Mengen überschneiden sich, sind aber nicht deckungsgleich.
Fünf Stages, und warum genau fünf
Weniger als fünf lässt eine Lücke; mehr als fünf ist keine Pipeline mehr, sondern ein eigenes Programm.
1. Secret scan gitleaks / trufflehog / detect-secrets Fail bei jedem Match
2. SAST Semgrep / SonarQube / CodeQL Fail bei neuen HIGH/CRITICAL
3. Dependency scan Trivy fs / Snyk / OWASP DC Fail ab CVSS ≥ 7 ohne VEX
4. Container scan Trivy image / Grype Fail bei HIGH/CRITICAL CVE ohne Fix
5. Manifest / IaC Trivy config / Checkov / Conftest Fail bei Security-Misconfig
Die Logik ist shift-left: je früher der Defekt gefangen wird, desto günstiger. Secrets vor der git-History. SAST vor dem Build. Dependency-Scan, bevor ein verwundbares Paket im Image landet. Image-Scan vor dem Push in die Registry. Manifest-Scan, bevor kubectl apply den Cluster erreicht.
Jede Stage ist ein eigener Verantwortungs-Bucket. Sie zu einem „einen großen Scanner" zu verschmelzen ist der klassische Fehler: wenn alles Trivy ist, kümmert sich niemand um SAST. Wenn alles SonarQube ist, kümmert sich niemand um die CVE-Datenbank. Andere Teams, andere Fehlerklassen.
Stage 1: Secrets, und nicht nur im git
Die git-Basis ist gitleaks als pre-commit-Hook plus detect-secrets mit Baseline in CI. Die Baseline ist nötig: ohne sie bricht jedes legitime AKIA* im Test den Build.
Es gibt eine zweite Ebene, die fast alle vergessen — Secrets in den Image-Layern. RUN echo $TOKEN > .npmrc && ... && rm .npmrc lässt das Token im darunterliegenden immutable Layer; rm löscht nur die oberste Sicht. docker history --no-trunc oder schlicht docker save | strings | grep token holt es aus der Registry zurück. Der Fix sind BuildKit-Secrets:
RUN --mount=type=secret,id=npmrc cp /run/secrets/npmrc ~/.npmrc && npm ci
Und in der Pipeline — trivy image --scanners secret zusätzlich zu git-leveligem gitleaks. Zwei verschiedene Scanner, zwei verschiedene Angriffsflächen.
Stage 3–4: Dependency und Container sind nicht dasselbe
trivy fs scannt package-lock.json, go.sum, requirements.txt — das, was im Code lebt. trivy image scannt das gebaute Image — dort steckt alles, was das Base-Layer beibrachte (apt-get install), plus was pip install obendrauf legte. Eine CVE in der libc des Base-Images taucht in trivy fs nicht auf. Eine CVE in einer Dev-Abhängigkeit, die nie ins finale Image gelangt, taucht in trivy image nicht auf.
Der Kanon in CI:
trivy image --severity HIGH,CRITICAL --exit-code 1 --ignore-unfixed $IMAGE
trivy fs --severity HIGH,CRITICAL --exit-code 1 .
trivy config --severity HIGH,CRITICAL --exit-code 1 ./k8s/
--ignore-unfixed ist pragmatisch: solange Upstream keinen Fix herausgegeben hat, bringt das Blocken des Builds nichts. Aber damit wird .trivyignore zu technischer Schuld — jede unterdrückte CVE braucht einen why-Kommentar und ein JIRA-Ticket. Ohne diese Disziplin ist die Datei ein Jahr später eine Mülldeponie.
Stage 5 + Admission: das einzige Muster, das hält
Stage 5 in CI — trivy config / Checkov / Conftest auf Manifeste und terraform plan -json. Fängt, was im Repository liegt. Fängt nicht, was an CI vorbei deployt wurde, oder einen Tag, der überschrieben wurde — myapp:v1.2.3 in der Registry könnte heute ganz ohne Scans neu gebaut worden sein.
Deshalb läuft das zweite Gate beim Admission:
- Kyverno verifyImages + cosign — das Image wird nur zugelassen, wenn es in CI nach bestandenem Scan signiert wurde. Build ohne Signatur → admission deny.
- OPA Gatekeeper / Registry-Whitelist — Pulls nur aus vertrauenswürdigen Registries (ECR, Harbor, privates GHCR). Ein Image aus einem öffentlichen
docker.io/random/xwird abgewiesen. - ValidatingAdmissionPolicy (CEL) — seit K8s 1.36 in den API-Server eingebaut, ohne Webhook. Geeignet für einfache Policies: kein
:latest, kein privileged, requiredLabels. Komplexes (Image-Verify, Multi-Resource-Generate) bleibt bei Kyverno.
Dopplung? Mit Absicht. Der CI-Scan fängt CVEs zum Build-Zeitpunkt; Admission-Verify fängt das Deploy eines Images, das an CI vorbeigeschummelt wurde. Zwei Schichten, jede deckt die Ausfallmuster der anderen ab.
IaC: ein Pre-apply-Gate als letzte Barriere
Terraform ist eine eigene Kategorie: ein einziges terraform apply erzeugt Dutzende Ressourcen in einem Atemzug. Das Pre-apply-Gate:
terraform plan -out plan.binary
terraform show -json plan.binary > plan.json
conftest test plan.json --policy ./policies/ # OPA auf den Plan, nicht den Code
Rego-Regeln: deny cidr_blocks = ["0.0.0.0/0"] auf 22/3389; deny Action: "*" in IAM; deny s3_bucket ohne server_side_encryption_configuration. Billiger den PR zu failen als zu applyen und dann zurückzurollen.
Als strukturelles Sicherheitsnetz darunter — ein IAM Deny ohne den Tag ManagedBy=Terraform auf Account-Ebene: ein Engineer in der Konsole kann eine IaC-managed Ressource physisch nicht ändern. Drift wechselt von „möglich" zu „unmöglich".
Was Teams gern vergessen anzuschließen
exit-code 1überall. „Soft-Fail" in CI heißt „kein CI". Ein Sonar-Quality-Gate im Modus failure (nicht warning) ist Pflicht..trivyignoreim PR reviewt. Suppression ohne Begründung ist der billigste Weg, die ganze Pipeline zu entwaffnen.trivy db --skip-db-updatein CI ist ein Antipattern. Die CVE-DB aktualisiert sich ~3× pro Tag; gestern ist eine andere Datenbank.- Audit-Log auf Admission-Deny. Ohne das arbeitet das Admission-Gate stumm, und niemand im Team sieht, dass Kyverno gestern drei Deploys blockiert hat.
Der eigentliche Shift ist, DevSecOps nicht mehr als „den Scanner in CI konfigurieren" zu denken. Es sind zwei Policy-Ebenen auf dieselben Risiken: shift-left in CI plus Admission-Time-Enforce im Cluster. Eine ohne die andere ist entweder ein grünes-Häkchen-Ritual oder ein offenes Kubeconfig für jedes Image, das die Pipeline umgangen hat.