Skip to main content

Delivery and supply chain · For Platform engineers, DevSecOps, CTOs

Securing CI/CD and the paths into production

Whoever controls your pipeline controls production. How to separate building untrusted code from holding deploy power, replace stored secrets with federation, and make the artifact the unit of trust.

Your pipeline can change production without anyone touching a server. That makes it part of production, whatever the org chart says.

Your pipeline is production's front door

A deployment pipeline is a machine that turns text written by many people into code running with production's privileges. If an attacker can influence what it builds, which dependencies it pulls or which credentials it holds, they do not need to break into production. The pipeline will deliver their change for them.

OWASP's Top 10 CI/CD Security Risks reads like a list of ways this happens: insufficient flow control, so code reaches production without the intended review; inadequate identity and access management across many systems; dependency chain abuse; poisoned pipeline execution, where an attacker changes the build steps through a pull request; and poor credential hygiene. The common thread is that teams secure production carefully and treat the path into it as tooling.

Map the path from commit to running code

Draw the path once, with every system and every credential on it. Include the paths that exist for outside contributions and for emergencies, not only the happy path.

Two properties matter in this picture. The jobs that run code you do not fully trust never hold credentials that can change production. And the thing that reaches production is a specific, signed artifact, identified by digest, rather than whatever a later job happens to rebuild.

From commit to production with deploy power isolatedCONTRIBUTORSSOURCE CONTROLBUILD AND RELEASEPRODUCTIONMaintainerSSO, signedcommitsOutsidecontributoruntrusted codeProtectedbranchreview and checksFork PR checksno secrets,read-onlyBuild jobephemeral, nodeploy rightsArtifactregistrysigned, withprovenanceDeploy jobenvironmentapprovalProductionclusteradmits signedimagesmergepull requeston mergepush digestsamedigestshort-lived role
From commit to production with deploy power isolated. Untrusted code from forks is built without secrets. Merged code is built once in an ephemeral job, published by digest with provenance, and the same digest is deployed by a separate job that alone can obtain a short-lived production role.
Read the diagram as text
  • Contributors: Maintainer, Outside contributor
  • Source control: Protected branch, Fork PR checks
  • Build and release: Build job, Artifact registry
  • Production: Deploy job, Production cluster
  1. Maintainer → Protected branch: merge
  2. Outside contributor → Fork PR checks: pull request
  3. Protected branch → Build job: on merge
  4. Build job → Artifact registry: push digest
  5. Artifact registry → Deploy job: same digest
  6. Deploy job → Production cluster: short-lived role

Separate building untrusted code from holding deploy power

Every build runs code: your source, its build scripts, its dependencies' install hooks, the test suite. Treat every build as executing untrusted code, because some of it is.

  • Split build and deploy into different jobs with different identities. The build job produces an artifact and has no route to production; the deploy job runs no project code and can obtain production access.
  • Never give pull requests from forks access to secrets. Workflows that check out a contributor's code in a privileged context are the classic route to poisoned pipeline execution.
  • Default job tokens to read-only and grant write scopes per job, only where needed.
  • Use ephemeral runners for anything that touches production, so one job cannot leave something behind for the next.
  • Protect deployment environments with required reviewers or checks, so a merged change still meets a gate before production.

Replace stored secrets with federation

Static cloud keys in CI variables are the most common credential failure in pipelines. Most CI platforms can now issue a signed OIDC token per run, which the cloud provider exchanges for a short-lived session. Done well, nothing long-lived is stored anywhere.

Done badly, federation recreates the problem in a new form. The trust policy is where the security lives: it should name the exact repository, the branch or deployment environment, and the expected audience. A policy that accepts any repository in your organisation, or any branch, hands production to anyone who can open a pull request. Review these policies with the same care as firewall rules, and keep the resulting role's permissions to what deployment needs.

Make the artifact the unit of trust

Build once, then promote the same artifact through environments by its digest. Rebuilding at each stage means production may run something nobody tested.

Attach evidence to the artifact as it is built. Provenance records which source revision, build definition and builder produced it; the SLSA framework describes increasing levels of confidence in that record, from documented provenance to a hardened, isolated build platform. Sign the artifact and its provenance, and have the deployment target verify the signature before admitting it. A software bill of materials attached to the same digest tells you later what was inside when a new vulnerability is published.

Guard the pipeline definition itself

The pipeline's own configuration is code that runs with the pipeline's privileges. Protect it accordingly:

  • Require code-owner review for changes to workflow files, deployment scripts and environment settings.
  • Pin third-party actions and build images by commit or digest rather than by a mutable tag.
  • Restrict who can change branch protection, environment rules and federation trust; these settings define the whole path.
  • Keep the CI platform's audit log and alert on changes to protections and secrets.

Close the side doors: caches, dependencies and emergencies

The main path is rarely where pipelines are broken. Look at the side doors as carefully.

Caches survive between jobs. If an untrusted job can write to a cache that a privileged job later restores, the privileged job runs the attacker's files. Key caches by branch and trust level, and never let pull-request jobs write to caches that release jobs read.

Dependencies are fetched by the build with the build's network access. Resolve them from lock files, through a registry you control or a proxy that records what was fetched, and block install scripts where your ecosystem allows. Our guide to supply-chain boundaries goes further.

Emergency paths exist in every organisation: a script that deploys from a laptop, a console role for hotfixes, a manual override on a protected branch. They are legitimate, and they are exactly what an attacker will look for. Make each one explicit, give it its own identity with just-in-time approval, and alert whenever it is used. An emergency path nobody monitors is the real path to production.

Finally, watch the pipeline as you watch production. Changes to branch protection, new federation trust policies, new secrets, unusual deploy times and deployments that did not come from the release job are all worth an alert.

A short checklist

  1. A drawing of every path to production, including forks and emergency deploys.
  2. Build and deploy in separate jobs with separate identities.
  3. No secrets available to code from forks.
  4. OIDC federation with exact repository and environment conditions; no stored cloud keys.
  5. Read-only job tokens by default.
  6. One build per change, promoted by digest, with signed provenance.
  7. Signature verification at deployment.
  8. Code-owner review on pipeline definitions and pinned third-party steps.

Sources

  1. Top 10 CI/CD Security Risks

    OWASP

  2. CI/CD Security Cheat Sheet

    OWASP

  3. Strategies for the Integration of Software Supply Chain Security in DevSecOps CI/CD Pipelines (SP 800-204D)

    NIST

  4. SLSA specification v1.0

    OpenSSF

  5. OpenID Connect in GitHub Actions

    GitHub Docs

  6. Secure use reference for GitHub Actions

    GitHub Docs

  7. Sigstore documentation

    Sigstore

Foundations · 5 min read

How to build a secure architecture from day one

The decisions that make a new system defensible before its first incident: five facts to write down, a skeleton with its boundaries drawn in, and defaults that hold without heroics.

  • secure by design
  • trust boundaries
  • least privilege