Search…
CI/CD Pipelines · Part 8

Pipeline security and supply chain

In this series (10 parts)
  1. What CI/CD actually means
  2. Pipeline anatomy and design
  3. GitHub Actions in depth
  4. GitLab CI/CD in depth
  5. Jenkins fundamentals
  6. Testing in CI pipelines
  7. Artifact management
  8. Pipeline security and supply chain
  9. Progressive delivery
  10. Self-hosted runners and pipeline scaling

Prerequisite: Artifact management.

A CI/CD pipeline has write access to production. It pulls dependencies from the internet, runs arbitrary build scripts, and pushes artifacts to registries. If an attacker compromises any step, they own your deployment. The SolarWinds and event-stream incidents proved this is not theoretical.

This article adds security gates to every stage of your pipeline. See also: supply chain security for a broader treatment.


Security gates in the pipeline

graph LR
  CODE["Source Code"] --> DEP["Dependency Scan<br/>Dependabot / Renovate"]
  DEP --> SAST["SAST<br/>Semgrep"]
  SAST --> BUILD["Build"]
  BUILD --> IMG["Image Scan<br/>Trivy"]
  IMG --> SBOM["SBOM Generation<br/>Syft"]
  SBOM --> SIGN["Sign + Attest<br/>Cosign / SLSA"]
  SIGN --> DEPLOY["Deploy"]

  style CODE fill:#64748b,color:#fff
  style DEP fill:#3b82f6,color:#fff
  style SAST fill:#8b5cf6,color:#fff
  style BUILD fill:#f59e0b,color:#000
  style IMG fill:#ef4444,color:#fff
  style SBOM fill:#ec4899,color:#fff
  style SIGN fill:#22c55e,color:#000
  style DEPLOY fill:#10b981,color:#000

Security gates span the entire pipeline. Each gate can block the deployment if it finds a critical issue.


Dependency scanning

Your application is mostly third-party dependencies. Scanning them for known vulnerabilities is the lowest-effort, highest-impact security measure you can add.

Dependabot (GitHub native)

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: npm
    directory: "/"
    schedule:
      interval: weekly
    open-pull-requests-limit: 10

  - package-ecosystem: github-actions
    directory: "/"
    schedule:
      interval: weekly

Dependabot opens PRs when it finds patched versions. Pin GitHub Actions to full commit SHAs, not mutable tags:

# Bad: mutable tag
- uses: actions/checkout@v4

# Good: pinned to SHA
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11

Renovate (self-hosted option)

Renovate offers more control: grouping updates, auto-merge for patches, and custom regex managers.

{
  "extends": ["config:recommended"],
  "packageRules": [
    {
      "matchUpdateTypes": ["patch"],
      "automerge": true
    }
  ],
  "vulnerabilityAlerts": {
    "enabled": true,
    "labels": ["security"]
  }
}

Static Application Security Testing (SAST)

SAST tools analyze source code without executing it. They find SQL injection, path traversal, hardcoded secrets, and insecure cryptographic usage. Semgrep is open-source, fast, and supports custom rules.

  sast:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Semgrep
        uses: semgrep/semgrep-action@v1
        with:
          config: >-
            p/default
            p/owasp-top-ten
            p/nodejs
          generateSarif: true

      - name: Upload SARIF
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: semgrep.sarif

The SARIF upload integrates findings into the GitHub Security tab. Developers see vulnerabilities inline on their pull requests.

Custom Semgrep rules

When your codebase has specific patterns to ban, write a custom rule:

# .semgrep/no-raw-sql.yml
rules:
  - id: no-raw-sql-queries
    patterns:
      - pattern: db.query($SQL, ...)
      - pattern-not: db.query($SQL, $PARAMS, ...)
    message: >
      Raw SQL without parameterized inputs. Use parameterized queries
      to prevent SQL injection.
    severity: ERROR
    languages: [javascript, typescript]

Container image scanning

Your container image inherits every vulnerability in its base image. Alpine has fewer packages and therefore fewer CVEs than Ubuntu, but it still needs scanning.

Trivy

  image-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Scan with Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: myapp:${{ github.sha }}
          format: table
          exit-code: 1
          severity: CRITICAL,HIGH
          ignore-unfixed: true

Setting exit-code: 1 fails the pipeline on critical or high vulnerabilities. The ignore-unfixed flag skips vulnerabilities that have no available patch, reducing noise.

Slim base images

Reduce your attack surface by choosing minimal base images:

# Multi-stage build: build in full image, run in distroless
FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build

FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
CMD ["dist/index.js"]

Distroless images contain only your application and its runtime. No shell, no package manager, no curl. An attacker who gains code execution inside the container has far fewer tools available.


SBOM generation

A Software Bill of Materials (SBOM) lists every component in your artifact. When a new CVE drops, you check your SBOMs to see if you are affected. Without an SBOM, you are guessing.

      - name: Generate SBOM
        uses: anchore/sbom-action@v0
        with:
          image: myapp:${{ github.sha }}
          format: spdx-json
          output-file: sbom.spdx.json

      - name: Attest SBOM
        uses: actions/attest-sbom@v2
        with:
          subject-name: ghcr.io/myorg/myapp
          subject-digest: ${{ steps.build.outputs.digest }}
          sbom-path: sbom.spdx.json
          push-to-registry: true

The attestation links the SBOM to a specific image digest. Anyone pulling the image can verify what is inside it.


Signing builds with Sigstore

Sigstore provides three components:

  • Cosign: signs and verifies container images and other artifacts.
  • Fulcio: issues short-lived certificates tied to OIDC identities (like a GitHub Actions workflow).
  • Rekor: a transparency log that records all signing events.
      - name: Install Cosign
        uses: sigstore/cosign-installer@v3

      - name: Sign the image
        run: cosign sign --yes ghcr.io/myorg/myapp@${{ steps.build.outputs.digest }}
        env:
          COSIGN_EXPERIMENTAL: "true"

No keys to manage. The GitHub Actions OIDC token proves the identity of the workflow. The signature is recorded in Rekor. Anyone can verify it.

Admission control

Kubernetes can enforce that only signed images run in your cluster:

# Kyverno policy
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-image-signature
spec:
  validationFailureAction: Enforce
  rules:
    - name: verify-cosign-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "ghcr.io/myorg/*"
          attestors:
            - entries:
                - keyless:
                    issuer: "https://token.actions.githubusercontent.com"
                    subject: "https://github.com/myorg/*"

If someone pushes an unsigned image or an image signed by an unexpected identity, Kubernetes rejects the pod.


The SLSA framework

Supply-chain Levels for Software Artifacts (SLSA, pronounced “salsa”) defines four levels of supply chain security:

LevelRequirement
SLSA 1Build process is documented and produces provenance
SLSA 2Build runs on a hosted service with signed provenance
SLSA 3Build environment is hardened and provenance is non-forgeable
SLSA 4Hermetic, reproducible build with two-party review

Most teams should target SLSA 2 as a practical starting point. GitHub Actions natively supports SLSA provenance generation.

      - name: SLSA Provenance
        uses: actions/attest-build-provenance@v2
        with:
          subject-name: ghcr.io/myorg/myapp
          subject-digest: ${{ steps.build.outputs.digest }}
          push-to-registry: true

The provenance attestation records the repository, workflow file, commit SHA, builder identity, and build parameters. This creates an auditable chain from source to artifact.


Layered defense

No single tool catches everything. Layer your defenses:

  1. Dependency scanning catches known CVEs in libraries before they enter your build.
  2. SAST finds vulnerabilities in your own code.
  3. Image scanning catches vulnerabilities in base images and OS packages.
  4. SBOM lets you respond to new CVEs after deployment.
  5. Signing and provenance ensure the artifact in production is the one your pipeline built.

Each layer is imperfect. Together they make an attacker’s job significantly harder.


What comes next

A secure pipeline delivers artifacts safely. But delivering everything to everyone at once is risky. Progressive delivery introduces feature flags, canary deployments, and automated rollback triggers to reduce blast radius.

Start typing to search across all content
navigate Enter open Esc close