DevSecOps & Supply Chain Security

Project: A Secure Delivery Pipeline

18 min Lesson 10 of 28

Project: A Secure Delivery Pipeline

The ten lessons of this tutorial each addressed a distinct security layer — shift-left culture, threat modeling, SAST, SCA, DAST, container and IaC scanning, SBOMs, signing, and secrets management. This capstone lesson assembles them into a single, production-grade pipeline design, explains how the gates interlock, and shows how attestation chains connect every step into an auditable provenance record that survives a supply-chain investigation.

The model here reflects how companies like Google, GitHub, and Shopify actually structure DevSecOps: security is not a scan that blocks a PR — it is a chain of signed claims about software that accumulates from the first commit to the running workload. If any link in that chain is missing or invalid, the deployment gate refuses the artifact.

The Five-Gate Model

A mature secure delivery pipeline organizes controls into five ordered gates. Each gate either produces a signed attestation (a cryptographic claim about the artifact) or consumes attestations from previous gates. Nothing advances without the gate's attestation being present and valid.

  • Gate 1 — Code Integrity: Gitleaks secrets scan + Semgrep/CodeQL SAST + dependency license check. Runs on every PR commit in parallel. Produces a code-scan attestation signed with the CI OIDC identity.
  • Gate 2 — Dependency Trust: Trivy/Grype SCA against OSV + CVE databases, Syft SBOM generation attached to the PR. Blocks on any critical CVE with a fix available. Produces a sbom attestation and a vuln-scan attestation.
  • Gate 3 — Build Integrity: Reproducible build in an ephemeral, network-isolated runner. IaC scan (Checkov/tfsec) on Terraform/Helm changes. Container image built and signed with Cosign (keyless, Sigstore). SLSA provenance attestation generated by the builder. Image pushed to registry only after all attestations are attached.
  • Gate 4 — Pre-Deploy Verification: Kyverno or OPA/Gatekeeper policy admission checks the image for all required attestations before allowing it into any Kubernetes namespace. DAST (OWASP ZAP API scan) runs against the staging environment after deploy. Gate passes only when DAST finds no high/critical findings.
  • Gate 5 — Runtime Continuous Assurance: Falco runtime security rules watch for anomalous process behavior. Vault dynamic secrets rotate credentials before each deployment. Dependency-Track monitors the production SBOM for newly published CVEs and pages on-call when a critical emerges.
Five-Gate Secure Delivery Pipeline with Attestation Chain Gate 1 Code Integrity Gitleaks · Semgrep CodeQL · License ⟶ code-scan attest Gate 2 Dependency Trust Trivy SCA · Syft SBOM OSV · Grype ⟶ sbom + vuln attest Gate 3 Build Integrity Reproducible Build Cosign · SLSA L3 ⟶ provenance attest Gate 4 Pre-Deploy Verify Kyverno attest check ZAP DAST · OPA ⟶ deploy-approval Gate 5 Runtime Assurance Falco · Vault Rotate Dep-Track · Alerts ⟶ continuous CVE watch Attestation Chain — each gate signs a claim; Gate 4 verifies ALL prior attestations exist on the image before deploy Trigger: PR commit Trigger: PR commit Trigger: merge to main Trigger: CD deploy job Trigger: continuous Any gate failure → artifact is quarantined; deployment is blocked; on-call paged if severity ≥ HIGH Vault Dynamic Secrets OCI Registry Image + Cosign Attestations Dependency-Track SBOM + CVE Feed
The five-gate secure delivery pipeline: gates 1-2 run on every PR, gate 3 builds and signs on merge, gate 4 verifies all attestations before deploying to Kubernetes, gate 5 watches the runtime continuously.

Wiring It Together: The GitHub Actions Workflow

The following skeleton shows how all five gates map to a real GitHub Actions workflow. Jobs within the same stage run in parallel. The build-and-sign job uses needs: to enforce gate ordering — it will not run unless gates 1 and 2 both pass.

# .github/workflows/secure-pipeline.yml name: Secure Delivery Pipeline on: push: branches: [main] pull_request: permissions: contents: read id-token: write # Required for keyless Cosign + SLSA provenance packages: write # Push to GHCR security-events: write jobs: # ── GATE 1: Code Integrity ────────────────────────────────────────────── secrets-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: { fetch-depth: 0 } - uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} sast: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: github/codeql-action/init@v3 with: { languages: python, javascript } - uses: github/codeql-action/autobuild@v3 - uses: github/codeql-action/analyze@v3 with: { category: /language:python } # ── GATE 2: Dependency Trust ──────────────────────────────────────────── sca-and-sbom: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Trivy SCA — fail on CRITICAL with fix uses: aquasecurity/trivy-action@master with: scan-type: fs exit-code: "1" severity: CRITICAL ignore-unfixed: true - name: Generate SBOM with Syft uses: anchore/sbom-action@v0 with: format: cyclonedx-json output-file: sbom.cdx.json - uses: actions/upload-artifact@v4 with: { name: sbom, path: sbom.cdx.json } # ── GATE 3: Build Integrity ───────────────────────────────────────────── build-and-sign: needs: [secrets-scan, sast, sca-and-sbom] runs-on: ubuntu-latest outputs: image-digest: ${{ steps.push.outputs.digest }} steps: - uses: actions/checkout@v4 - name: Set up QEMU & Buildx uses: docker/setup-buildx-action@v3 - name: Login to GHCR uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build & push (reproducible, no-cache, provenance=true) id: push uses: docker/build-push-action@v6 with: push: true tags: ghcr.io/${{ github.repository }}:${{ github.sha }} cache-from: type=gha cache-to: type=gha,mode=max sbom: true provenance: mode=max # Generates SLSA provenance in-toto attestation - name: Install Cosign uses: sigstore/cosign-installer@v3 - name: Sign image (keyless — uses OIDC token) run: | cosign sign --yes \ ghcr.io/${{ github.repository }}@${{ steps.push.outputs.digest }} - name: Attach SBOM attestation run: | cosign attest --yes \ --predicate sbom.cdx.json \ --type cyclonedx \ ghcr.io/${{ github.repository }}@${{ steps.push.outputs.digest }} # ── GATE 4: Pre-Deploy Verification (staging) ─────────────────────────── pre-deploy-verify: needs: [build-and-sign] runs-on: ubuntu-latest steps: - name: Verify Cosign signature & attestations run: | cosign verify \ --certificate-identity-regexp \ "https://github.com/${{ github.repository }}/.github/workflows/.*" \ --certificate-oidc-issuer \ "https://token.actions.githubusercontent.com" \ ghcr.io/${{ github.repository }}@${{ needs.build-and-sign.outputs.image-digest }} - name: Deploy to staging run: kubectl set image deployment/myapp myapp=ghcr.io/${{ github.repository }}@${{ needs.build-and-sign.outputs.image-digest }} -n staging - name: DAST — ZAP API Scan uses: zaproxy/action-api-scan@v0.9.0 with: target: https://staging.myapp.example.com/openapi.json fail_action: true rules_file_name: .zap/rules.tsv
Why pass the digest, not the tag? Tags are mutable — :latest can point to a different image tomorrow. A digest (sha256:abc…) is an immutable reference to a specific image layer hash. Every step in the pipeline after the build job references the digest, not the tag, so there is no window for a tag-swap attack (where an attacker replaces the registry image between your signing step and your deploy step).

The Kyverno Admission Policy: Closing the Loop

Gate 4 is only meaningful if Kubernetes enforces it. Without an admission controller, a developer could push a manually built image that bypassed every scan and deploy it directly with kubectl. A Kyverno ClusterPolicy requires that every Pod image has a valid Cosign signature from your CI OIDC identity before the API server accepts the Pod spec.

# kyverno/require-image-signature.yaml apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: require-signed-images annotations: policies.kyverno.io/description: > All images must be signed by the GitHub Actions OIDC identity using keyless Cosign (Sigstore). Unsigned images are rejected at admission time — they never start. spec: validationFailureAction: Enforce background: false rules: - name: check-image-signature match: any: - resources: kinds: [Pod] namespaces: [production, staging] verifyImages: - imageReferences: - "ghcr.io/myorg/*" attestors: - entries: - keyless: subject: "https://github.com/myorg/myapp/.github/workflows/secure-pipeline.yml@refs/heads/main" issuer: "https://token.actions.githubusercontent.com" rekor: url: https://rekor.sigstore.dev attestations: - predicateType: https://cyclonedx.org/bom conditions: - all: - key: "{{ metadata.component.version }}" operator: NotEquals value: ""

Failure Modes and Escape Hatches

Production pipelines must handle emergencies without completely abandoning security. Three common failure modes and their mitigations:

  • Supply-chain scanner outage: If the Trivy database server is unreachable, fail open (allow the build) but page security on-call and set a 24-hour deadline for a manual scan. Do not let a vendor outage become a P0 deploy blocker — but do not silently skip the scan either. Record the skip as a signed exception attestation.
  • Zero-day in a transitive dependency with no fix: The correct response is not to suppress the finding. Use a .trivyignore file with an expiry date and a Jira ticket reference. Kyverno can be configured to accept an image with a vuln-exception attestation that includes the ticket ID and expiry, signed by a security engineer's key.
  • Emergency hotfix to production: Define a "break-glass" procedure: a GitHub environment with required reviewers (two senior engineers + one security engineer), a mandatory post-incident review, and an automated Jira ticket. The Kyverno policy can be configured to allow images signed by a break-glass key with a 2-hour TTL. Every break-glass event is logged to an immutable audit trail (CloudTrail / Rekor).
Start with audit mode, then enforce. When rolling out Kyverno admission policies to an existing cluster, set validationFailureAction: Audit first. This logs policy violations without blocking deployments. Run in audit mode for two weeks, fix all violations, then flip to Enforce. Flipping directly to Enforce on a cluster with unsigned legacy images will cause an immediate outage.
Attestation pinning is not enough alone. Cosign signatures and SLSA provenance prove the image was built by your CI system. They do not prove the image is free of vulnerabilities that were published after the build. This is why Gate 5 (Dependency-Track + SBOM continuous monitoring) is mandatory — an image that was clean on build day can become critical-vulnerable the next morning when a new CVE is published against one of its packages. The SBOM gives you the inventory to know instantly which running workloads are affected.

Measuring the Pipeline: Security KPIs

A pipeline you cannot measure is a pipeline you cannot improve. The four KPIs that matter at big-tech scale are: Mean Time to Detect (MTTD) — how long from a vulnerability being published until your Dependency-Track alert fires; Mean Time to Remediate (MTTR) — how long from alert to a fixed image running in production; Gate Escape Rate — the percentage of CVEs that reached production without being caught at gates 1-4 (target: zero critical/high escapes); and Pipeline Coverage — the percentage of production images that have a complete, valid attestation chain (target: 100%). Track these in a Grafana dashboard fed from Dependency-Track's API and your audit log pipeline.