JustAppSec

CI/CD Pipeline Security

Securing the path from commit to production — runners, permissions, and gates.

0:00

Your CI/CD pipeline has more access than almost any human in your organisation. It reads source code, stores secrets, builds artefacts, and deploys to production. If an attacker compromises it, they own everything downstream. This lesson covers how to secure the path from commit to production.

The pipeline as a target

A typical CI/CD pipeline:

  1. Developer pushes code to a repository
  2. CI runner checks out the code
  3. CI runs tests, linting, security scans
  4. CI builds artefacts (Docker images, binaries, packages)
  5. CD deploys to staging, then production

At every step, the pipeline has access to:

  • Source code (including private repositories)
  • Secrets (database credentials, API keys, signing keys)
  • Build infrastructure (runners, registries, cloud accounts)
  • Production environments (deployment credentials)

Compromise any step and the attacker can inject code, steal secrets, or deploy a backdoor.

Threat model

Compromised dependency

A malicious package runs code during npm install or pip install in your CI pipeline. The install script reads environment variables (which often contain secrets) and exfiltrates them.

Malicious pull request

An external contributor opens a PR that modifies the CI config or build scripts. If your CI runs on untrusted PRs with access to secrets, the attacker extracts them.

Stolen CI credentials

An attacker obtains CI runner credentials (e.g., a GitHub Actions token, a Jenkins API key) and triggers builds with modified code or config.

Supply chain injection

An attacker compromises the build process itself — modifying the build script, replacing a compiler, or tampering with the build cache.

Securing runners

Use ephemeral runners

Each CI job should run on a fresh, clean environment that is destroyed after the job completes. This prevents:

  • Secrets leaking between jobs
  • Persistent malware on shared runners
  • Cache poisoning

GitHub Actions hosted runners, GitLab SaaS runners, and containerised Jenkins agents are ephemeral by default. Self-hosted runners need explicit configuration to achieve this.

Restrict self-hosted runner access

If you must use self-hosted runners:

  • Isolate them in a dedicated network segment
  • Do not reuse runners across security boundaries (public repos vs. private repos)
  • Run CI jobs in containers, not directly on the host
  • Regularly rotate runner credentials

Limit runner permissions

CI runners should have the minimum cloud IAM permissions required:

# GitHub Actions — use OpenID Connect instead of long-lived keys
permissions:
  id-token: write
  contents: read

Use short-lived credentials via OIDC federation rather than storing long-lived cloud keys as secrets.

Protecting secrets in CI

Scoped secrets

Restrict which jobs and workflows can access which secrets:

  • Per-environment secrets — production deployment secrets should not be available to test jobs
  • Per-branch restrictions — only the main branch should trigger deployments with production secrets
  • Per-job scoping — a linting job does not need database credentials

Pull request restrictions

Never expose secrets to pull request builds from forked repositories. On GitHub Actions:

# Secrets are not available to pull_request events from forks by default
# Do NOT use pull_request_target with checkout of PR code — this bypasses the restriction
on:
  pull_request:
    # Secrets NOT available for forked PRs ✓

The pull_request_target event runs in the context of the base branch and has access to secrets. If combined with checking out the PR's code, an attacker's code runs with your secrets. This is one of the most dangerous GitHub Actions anti-patterns.

Masked output

CI platforms can mask secret values in logs. Enable this for all secrets:

# GitHub Actions — secrets are masked automatically
# But if you derive a new value from a secret, mask it explicitly
echo "::add-mask::$DERIVED_VALUE"

Pipeline configuration as code

Protect the CI config

Your .github/workflows/, .gitlab-ci.yml, or Jenkinsfile is a high-value target. Modifications to these files should:

  • Require code review approval
  • Be protected by branch protection rules
  • Trigger alerts when changed

Pin action versions

Do not use mutable tags for CI actions:

# BAD — the tag can be moved to point to different code
- uses: actions/checkout@v4

# GOOD — pinned to a specific commit SHA
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

Tags and branches can be force-pushed. Commit SHAs are immutable.

Limit workflow permissions

Follow least privilege for the GITHUB_TOKEN:

permissions:
  contents: read
  pull-requests: write
  # Only the permissions this workflow actually needs

The default GITHUB_TOKEN often has more permissions than necessary. Restrict it explicitly.

Build integrity

Reproducible builds

The same source code should produce the same artefact every time. This makes it possible to verify that a deployed artefact was built from the expected source code.

Factors that break reproducibility:

  • Timestamps embedded in artefacts
  • Non-deterministic dependency resolution (use lock files)
  • Build tools that introduce randomness

Build provenance

Record what was built, from which source, by which pipeline:

  • SLSA (Supply Chain Levels for Software Artefacts) defines levels of build integrity, from basic provenance to hermetic builds
  • Sigstore provides tools for signing and verifying artefacts
  • in-toto provides a framework for verifying each step in the supply chain

At minimum, log: the source commit, the builder identity, the build timestamp, and the artefact hash.

Deployment gates

Require approval for production

Automated deployment to staging is fine. Deployment to production should include a human approval gate:

# GitHub Actions environment with required reviewers
environment:
  name: production
  url: https://myapp.com

Canary and progressive deployments

Deploy to a small percentage of production first. Monitor for errors, latency spikes, and security anomalies before rolling out to 100%.

Rollback capability

Ensure you can roll back any deployment within minutes. If a compromised build reaches production, the speed of your rollback determines the blast radius.

Summary

CI/CD pipelines are high-value targets because they have broad access to secrets, source code, and production infrastructure. Use ephemeral runners, scope secrets to specific environments and branches, never expose secrets to untrusted pull requests, pin action versions to commit SHAs, and restrict workflow permissions to the minimum needed. Record build provenance, require approval for production deployments, and ensure you can roll back quickly. Your pipeline should be as hardened as your production infrastructure.


This training content is AI-assisted and reviewed by our team, but issues may be missed and best practices evolve rapidly. Send corrections to [email protected].