
Building a Hardened CI/CD Pipeline: Architecture, Secret Scanning, and Artifact Signing
Was bedeutet eine sichere DevOps-Pipeline? Grundlagen und DevSecOps
Der Runner war grün, das Image gepusht, und zwanzig Minuten später tauchte der API-Token im Klartext im Build-Log auf. Sowas passiert nicht wegen fehlender Tools, sondern weil Automatisierung Schlampigkeit skaliert. Wer CI/CD aufsetzt, baut im Grunde eine Remote-Code-Execution-Engine, die Tag und Nacht Code mit Root-Rechten kompiliert und deployt.
Eine sichere DevOps-Pipeline existiert nicht als statischer Zustand. Es geht um Schadensbegrenzung entlang der Kette: vom Commit über den Runner bis zur Registry. Wenn wir von DevSecOps sprechen, meinen wir nicht das nachträgliche Drüberbügeln von Scannern kurz vor dem Release, sondern deterministische Kontrollen an jedem Übergabepunkt.
Das sogenannte Shift-Left-Prinzip scheitert in der Praxis oft daran, dass Entwicklern unkonfigurierte Scanner vor die Füße geworfen werden, die bei jedem Lauf 4.000 False Positives ausspucken. Nach drei Tagen ignoriert das Team die Warnungen, nach fünf Tagen wird || true an das Build-Skript gehängt. Sicherheit von der ersten Codezeile an funktioniert nur, wenn Gates hart abbrechen, aber dabei präzise und schnell bleiben.
Für Teams bedeutet eine funktionierende, sichere DevOps-Pipeline vor allem Reproduzierbarkeit: Man weiß exakt, welcher Commit in welchem Binary steckt, welche CVEs gestern darin schlummerten und wer das Deployment autorisiert hat.
Häufige Sicherheitsrisiken und Angriffsvektoren in CI/CD-Pipelines
Die meisten Pipeline-Einbrüche benötigen keine Zero-Days. Die Einfallstore sind banal:
- Compromised Dependencies: Ein Maintainer gibt sein npm- oder PyPI-Konto auf, ein Typo-Squatting-Paket landet im Lockfile, oder ein GitHub Action Runner lädt ungepinnte Drittanbieter-Actions per
@mainnach. - Secret Leakage: Tokens in Env-Vars, die durch Debug-Outputs in Build-Logs leaken, oder Credentials, die zwar im Repository gelöscht, aber im Git-Verlauf ewig abrufbar bleiben.
- Runner-Fehlkonfigurationen: Shared Runner, die Docker-in-Docker mit gemountetem
/var/run/docker.sockbetreiben. Ein kompromittierter Test-Job bedeutet hier die vollständige Übernahme des Host-Systems. - Insecure Infrastructure as Code: Terraform-Configs oder Helm-Charts mit offenen Security Groups, privilegierten Containern (
privileged: true) oder fehlenden Ressource-Limits.
Kernkomponenten und automatisierte Security-Tests
Automatisierte Prüfungen gehören gestaffelt in die Pipeline. Nicht alles gehört in jeden Push-Event, sonst wartet das Team 45 Minuten auf Feedback.
Secret Scanning und die Git-History-Falle
Secret Scanning muss als Erstes laufen. Ich habe lange Gitleaks in CI-Pipelines laufen sehen, die trotzdem Tokens durchgewunken haben. Der Grund ist fast immer der Default von CI-Checkout-Actions:
# Schlecht: Holt nur den letzten Commit
- uses: actions/checkout@v4
with: fetch-depth: 1
Wenn ein Entwickler einen Schlüssel committet, den Fehler bemerkt, den Schlüssel im nächsten Commit löscht und erst dann pusht, scannt ein flacher Checkout (fetch-depth: 1) nur den Merge-State. Der Key ist im Arbeitsverzeichnis weg, aber im Git-Tree vorhanden. Die Pipeline läuft grün durch, und das Secret landet öffentlich im Remote-Repository.
$ gitleaks detect --source . --verbose --redact --exit-code 1
Erst mit fetch-depth: 0 durchsucht Gitleaks die gesamte Historie des Zweigs und bricht bei Treffern mit Exit-Code 1 sauber ab.
Statische Analyse und Dependency Audits (SAST & SCA)
SAST prüft den eigenen Code auf unsichere Muster (SQL-Injections, harte Krypto-Keys, fehlendes Input-Sanitizing). SCA untersucht Bibliotheken gegen bekannte CVE-Datenbanken (wie die NIST NVD). Der Sweet Spot ist hier nicht, jeden Severity-Low-Bug zu blockieren, sondern Pipelines nur bei HIGH oder CRITICAL mit bekannten Exploits (z. B. gelistet im CISA KEV-Katalog) fehlschlagen zu lassen. Alles andere landet im Backlog, sonst rebelliert das Team.
Container Security: BuildKit Secrets statt ARG
Ein Klassiker beim Bau von Container-Images: Private Abhängigkeiten erfordern Zugangsdaten (z. B. npm-Tokens oder SSH-Keys). Wer diese via ARG oder ENV übergibt, hinterlässt sie dauerhaft in den Image-Layern:
$ docker history --no-trunc ghcr.io/org/service:v1.2.3#Das Secret steht direkt im Klartext in der History-Ausgabe
Die Lösung erfordert BuildKit mit gemounteten Secrets, die nur während der Build-Ausführung im Speicher existieren und nicht im Layer landen:
# Im Dockerfile:
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm install
Zugriffssteuerung, Secrets Management und Signierung
Keine langlebigen Cloud-Keys auf Build-Servern. Wo immer möglich, gehört der Zugriff auf Cloud-Provider (AWS, GCP, Azure) über OpenID Connect (OIDC) abgewickelt. Der Runner tauscht ein kurzlebiges JSON Web Token gegen temporäre IAM-Rollen aus. Keine statischen Secrets im Repo, kein Rotationsaufwand.
Signierung von Artefakten: Der Tag-Mutation-Fehlschlag
Wir wollten Artefakte per Cosign signieren, um mit Admission Controllern (wie Kyverno) im Cluster sicherzustellen, dass nur verifizierter Code ausgeführt wird. Der erste Versuch sah so aus:
# Der falsche Ansatz: Signieren über den mutablen Tag
$ cosign sign --yes ghcr.io/org/service:v1.2.3
Das funktionierte lokal, flog uns aber in der Pipeline um die Ohren. Ein Image-Tag wie :v1.2.3 ist veränderbar. Wenn ein paralleler Build-Job denselben Tag überschreibt, signiert Cosign entweder das falsche Manifest, oder der nachgelagerte Kubernetes-Admission-Controller schlägt fehl, weil der kryptografische Digest des Deployments nicht mehr zum Payload der Signatur passt. Schlimmer noch: Ein Angreifer mit Registry-Schreibrechten kann den Tag einfach auf ein bösartiges Image umbiegen.
Signaturen müssen immer auf den unveränderlichen sha256-Digest angewendet werden:
# Der richtige Ansatz: Signieren über den Immutable Digest
$ cosign sign --yes ghcr.io/org/service@sha256:4f3b2a1c8e...
Konfigurationsbeispiel: GitHub Actions mit Keyless Signing
Damit Keyless Signing mit Sigstore funktioniert, benötigt der GitHub Actions Job zwingend das OIDC-Token-Schreibrecht (id-token: write). Fehlt diese Permission, schlägt Cosign mit einem nichtssagenden 400-Fehler oder einer Fehlermeldung bezüglich fehlender Ambient Credentials fehl.
name: build-and-signon: [push]
permissions: contents: read packages: write id-token: write
jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0
- name: Secret Scan run: gitleaks detect --verbose --redact
- uses: sigstore/[email protected]
- uses: docker/build-push-action@v5 id: build with: push: true tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
- name: Sign Digest run: | cosign sign --yes ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
Die Verifizierung in der Staging-Umgebung oder im Admission Controller prüft dann exakt die Identität des CI-Workflows:
$ cosign verify \
--certificate-identity https://github.com/org/service/.github/workflows/build.yml@refs/heads/main \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ ghcr.io/org/service:v1.2.3
Ein Nebeneffekt strenger Registries: Ist "Tag Immutability" aktiviert, schlagen ältere Cosign-Versionen oft fehl, weil sie versuchen, Signaturen als .sig-Tags hochzuladen. Neuere Setups benötigen OCI 1.1 Image Referrers oder explizite Ausnahmen für Signatur-Tags im Registry-Regelwerk.
Audit-Trails und Log-Aufbewahrung
Wenn etwas schiefgeht, müssen die Logs existieren. Wer globale Infrastruktur oder Services in regulierten Regionen betreibt, stolpert schnell über handfeste Compliance-Vorgaben. Beispielsweise verlangen die CERT-In-Richtlinien (April 2022, Section 70B des IT Act), dass ICT-Systemprotokolle – einschließlich CI/CD-Zugriffen, Build-Audit-Trails und Registry-Authentifizierungslogs – für mindestens 180 Tage rollierend innerhalb der zuständigen Jurisdiktion archiviert werden müssen, synchronisiert über offizielle NTP-Quellen (wie samay1.nic.in und samay2.nic.in).
Selbst ohne gesetzliche Pflicht: Wer seine Runner-Logs nach 7 Tagen löscht, kann bei einem Vorfall die Supply Chain nicht mehr zurückverfolgen. Build-Metadaten gehören zentralisiert und fälschungssicher exportiert.
Schritt-für-Schritt: Die Pipeline abhärten
- Pipelines isolieren: Ephemere Runner verwenden. Keine persistenten Docker-Build-Caches auf geteilten Maschinen, auf denen ungetesteter Branch-Code ausgeführt wird.
- Pinning erzwingen: Aktionen und Pipeline-Module nicht per Branch-Namen (
@main) referenzieren, sondern per SHA (z. B.uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11). - Gates staffeln: Secret-Scans brechen sofort ab. Linting und Unit-Tests parallel. SCA/SAST als Pflicht vor dem Merge. Container-Scans und Signierung erst beim Image-Push.
- Admission Enforcement: Im Kubernetes-Cluster blockiert ein Admission-Controller (z. B. Kyverno) unsignierte Images rigoros. Was nicht kryptografisch nachweisbar aus der Pipeline stammt, startet nicht.
Aktuelle offene Baustelle: Wir kämpfen noch mit dem Overhead von in-toto Attestations für SBOMs (Software Bill of Materials). Das Erzeugen via Syft und Signieren via Cosign funktioniert im CI-Job, aber die Validierung im Admission Controller verdoppelt derzeit die Deployment-Latenz bei großen Images mit tausenden Packages. Ob sich dieser Tradeoff im Tagesgeschäft rechnet oder ob wir Attestation-Checks asynchron nach dem Deployment laufen lassen, ist noch nicht ausgemacht.
Explore Topics
Cybersecurity Tools for Small Teams
SIEM, secure terminal access, and hands-on training — built for startups and individuals.
Stay Ahead of Threats
Get the latest cybersecurity insights, tutorials, and threat intelligence delivered to your inbox.
Discussion
Enjoyed this article?