All articles

CI/CD pipeline security: Controls from source to deployment

The Chainguard Team
Software Supply Chain
Key Takeaways
  • Map your CI/CD chain of custody: trace one artifact from source commit to deployment to find gaps in approval and evidence.

  • Pin third-party actions to full commit SHAs, not mutable tags, so an attacker can't swap in new code behind a trusted label.

  • Bind signatures, SBOMs, and provenance to the artifact digest, then enforce that evidence at deployment with admission policy.

A good litmus test for secure CI/CD pipelines is whether you can answer these three questions at every handoff:

  • Which source change was approved, and by whom?

  • Which build system and inputs produced the artifact?

  • Which deployment policy authorized that exact artifact?

Those three questions define the chain of custody your pipeline needs to preserve. Every production artifact should be traceable to its source commit, build runner, and the deployment decision that authorized it. That requires controls across threat modeling, source control, runners, dependencies, workflow actions, artifact publishing, and deployment, with each stage preserving evidence for the next handoff.

Mapping the CI/CD chain of custody

Attackers exploit the trust boundaries where code, credentials, dependencies, and artifacts pass between systems en route from source control to production. Those systems retain separate permissions and audit records, leaving the custody record fragmented. A CI/CD threat model maps the systems, identities, and inputs involved at each boundary, exposing gaps where a change lacks a recorded approver, picks up an unverified input, or reaches production without evidence.

To make the review manageable, start with a single production artifact. Tracing its chain of custody from the source commit that produced it to the workload running in production gives you a concrete way to find missing approvals, unverified inputs, and gaps in deployment evidence. Along that path, identify the source repositories and workflow files, build runners, dependency and artifact registries, build caches, deployment credentials, and production policy gates. At each boundary, record what moves, who authorizes it, and what evidence must survive.

NIST SP 800-204D describes CI/CD as taking software through stages such as build, test, package, and deploy. Those stages create handoffs, and each handoff is where custody can be broken. The table below maps the risk, controls, and evidence at every step, from source control through deployment:

Control point

Primary risk

Controls

Evidence to retain

Source control

Unauthorized or unreviewed code and workflow changes; spoofed authorship

Branch protection or rulesets, required code-owner reviews, verified commit signatures

Pull-request approvals, required check results, verified signature status

Build environment

Credential theft, persistent runner compromise, unrestricted network access

Ephemeral runners, least-privilege tokens, egress limits

Runner identity, job permissions, network logs

Dependencies

Unapproved, known malicious, or known vulnerable packages; mutable versions; dependency confusion

Lockfiles and integrity checks, package-source and namespace controls, approved registry policy, checks for known malware and vulnerabilities, provenance verification where available

Lockfile diff, resolved version, source, and integrity hash or digest; registry policy decision; malware and vulnerability results; verified provenance

Workflow actions

Mutable action references, untrusted-input injection, overprivileged third-party code

Full-length commit SHA pinning, required workflow review, least-privilege permissions, safe handling of untrusted input

Pinned commit SHA, reviewed workflow diff, effective job permissions

Test and verification

Skipped, altered, or falsified test and scan results

Required test and security jobs, protected workflow definitions, fail-closed gates

Test and scan results, workflow revision, runner or job identity

Artifacts

Artifact substitution or tampering; unknown contents or build origin

Digest-bound signing and verification, Software Bill of Materials (SBOM) generation, signed provenance attestations

Artifact digest and signature verification result, SBOM, verified Supply-chain Levels for Software Artifacts (SLSA) provenance

Deployment

Bypassed release policy or deployment of an unapproved artifact

Policy gates that verify the digest, signer, and required attestations; admission control where applicable

Deployed digest, policy decision and version, deployment identity

The exercise should produce a short list of custody gaps you can close before an untrusted artifact reaches production. If a production image is built on a long-lived self-hosted runner, uses a mutable base-image tag, is pushed without a signature, and reaches the cluster without verification, it has four separate custody breaks you need to account for. Without a verifiable record of the image’s inputs, approvals, and origin, your deployment policy cannot reliably distinguish the approved image from one altered or substituted along the way. If an incident occurs, those same gaps slow efforts to identify the affected release and scope the exposure.

Locking down source control

A single merged change to .github/workflows/ can rewrite what your pipeline does on the very next push. Source control is where the chain begins, because the pipeline will eventually do whatever the repository tells it to do. The default branch is a control point in the chain of custody. It's where code and workflow definitions become trusted input to CI. Require reviews, passing status checks, and block direct pushes so the code and workflow definitions that trigger CI have been reviewed and validated before the pipeline trusts them. Apply the same discipline to workflow files, since .github/workflows/* controls the automation that runs after the merge.

That scrutiny is necessary because workflow files define the permissions, third-party code, and deployment actions CI will execute. An unsafe use of pull_request_target, for example, can expose repository secrets or write permissions to untrusted pull request code. Recent actions/checkout versions refuse to fetch fork pull request code under pull_request_target by default, but workflows pinned to older commit SHAs do not inherit that protection. OpenSSF Scorecard checks whether your repositories enforce branch protection, safe workflow patterns, code review, least-privilege token permissions, packaging, and signed releases. Use it to surface weak repositories and prioritize fixes to prevent unsafe changes from entering CI.

Start by locking down the merge path for branches that feed production and the files that define CI behavior. In your source-control platform, configure these branch rules and review requirements:

  1. Require pull requests for the default branch and block direct pushes.

  2. Require status checks from trusted CI providers before merge.

  3. Require signed commits or signed tags for release branches where your platform supports it.

  4. Add CODEOWNERS for workflow files, build scripts, deployment manifests, and package manager configuration.

  5. Require security review for changes to secrets, OpenID Connect trust policies, publishing jobs, and admission policies.

A signed commit proves only that the commit came from a trusted identity. It says nothing about whether the code is safe, so treat signing as an identity control that hands the next stage a cleaner starting point. The identity claim behind a signed commit is reliable only when the signing credential belongs to an authorized developer or release service, its private key is protected from misuse, and access is revoked or rotated after a compromise, a role change, or a departure. Use short-lived commit-signing certificates where they are supported, and where long-lived keys are unavoidable, pair them with a rotation policy and a revocation procedure.

Making runners disposable

A self-hosted runner that persists between jobs can carry a compromise forward into later builds. Because runners can access source code, dependencies, build outputs, and often publishing or deploy credentials, one infected runner can taint more than a single pipeline run. GitHub's secure use reference notes that GitHub-hosted runners execute jobs in ephemeral, clean, isolated virtual machines, while self-hosted runners carry no such guarantee and can be persistently compromised by untrusted workflow code. Run untrusted pull request work on hosted ephemeral runners whenever you can, so each job starts from a clean environment. If you need a self-hosted runner for network reach, specialized hardware, cost, or compliance, treat it as production infrastructure and apply the same isolation, access controls, and monitoring you would use for a production workload.

Start with controls that limit runner persistence, scope access, and preserve incident evidence:

  • Register runners just in time or with an ephemeral mode where your platform supports it.

  • Destroy the machine or container after one job, then rebuild from a known image.

  • Put runners in groups scoped to specific repositories, teams, or environments.

  • Block untrusted fork workflows from landing on privileged runners.

  • At the firewall or security-group level, allowlist egress only to required registries, logging endpoints, and cloud APIs.

  • Keep runner logs outside the runner to prevent cleanup from erasing incident evidence.

A build job that can reach the whole internet can fetch an unreviewed script, exfiltrate a token, or beacon back to an attacker-controlled server before your scanner ever sees an artifact. Scoping egress to the few hosts each job needs removes those paths, so a compromised job has nowhere to send what it steals.

Scoping build credentials

Two controls, read-only default tokens and short-lived cloud access, cover a large portion of credential risk in CI.

Default workflow tokens to read-only.

GitHub recommends granting the GITHUB_TOKEN the minimum required permissions, including read access to repository contents by default, with job-level increases only where a step requires them. New repositories have defaulted to the restricted read-only setting for contents and packages since February 2023, but repositories created before then may still be set to read/write, so check the setting rather than assuming it. A read-only default means a compromised step or action cannot push commits, publish packages, or move releases without an explicit, reviewable permission bump.

Set the workflow-wide default first, then declare the full permission set each job needs, since job-level permissions replace the workflow default rather than adding to it:

# Set read-only defaults for all jobs in this workflow.
permissions:
  contents: read

jobs:
  build:
    permissions:
      # These REPLACE the workflow default, so list all you need.
      contents: read
      packages: write

Use OpenID Connect (OIDC) for cloud access instead of long-lived keys.

OIDC lets your cloud provider issue a short-lived credential only to an approved CI workload, such as a deployment job from a specific repository, branch, workflow, or environment. Configure the provider's trust policy around those claims so CI does not need a long-lived cloud key.

Treating dependencies as inputs

Dependency installation is a security boundary. Without lockfiles, approved package sources, and a policy for what may enter the build, the resolver can install an unintended or compromised dependency before tests run.

Start by making dependency resolution deterministic and reviewable. Commit lockfiles for ecosystems that support them, fail builds when a lockfile drifts without review, and split dependency-update pull requests from feature work so a reviewer can assess the supply-chain change on its own. That reviewer should see which package changed, which source it came from, and why the update is being added to the build.

Trusted registries come next. Public registries are useful, but they were not designed with your organization's policies in mind, so you should route builds through a private mirror, a proxy registry, or an artifact manager. Additional policy gates allow you to enforce package age, license, and CVE rules before an upstream dependency reaches your CI/CD runner.

For a new open source dependency, use OpenSSF Scorecard to assess how securely the project maintains its repository and releases, not to prove that a specific package version is safe. Its results can flag projects that need deeper review alongside your vulnerability, provenance, and source checks before the dependency enters the build.

A practical dependency gate can be small:

Block merge when:
- lockfile changes without package manifest changes
- package source changes from an approved registry to a public registry
- new dependency fails minimum Scorecard threshold
- dependency review finds a known high-severity CVE with a fix available
- install scripts are added without security review

Record the dependency decision as build evidence. Include the lockfile diff, each package’s approved registry source, and the dependency-review or Scorecard result that approved it in the build’s provenance so later policies can verify that the artifact used only approved dependencies.

For Chainguard customers, we provide multiple layers of defense against attacks. Trusted inputs become a default rather than a review habit. With Chainguard Libraries, you get thousands of Python, Java, and JavaScript packages rebuilt from verified source in a SLSA Level 3 Factory. For packages Chainguard has not yet built, the Chainguard Repository provides the ability to fall back to the upstream version, with a cooldown (seven days by default) that holds delivery until the package has been public long enough for known-bad releases to surface. And every package, whether built from source or served from upstream, runs through a malware scanner that provides binary analysis and sandbox detonation. Together, these controls make it harder for malicious packages to reach your builds, whether that's a new package published by a worm, as in the Mini Shai-Hulud, or a backdoored version of a package you already trust, as in the axios compromise. Engineers keep running pip install, npm install, and mvn install, and the dependency review question shifts from whether a package is safe to whether the build consumed the version you approved.

"Most malicious releases get found and pulled within hours or days. The problem isn't that the ecosystem is slow to notice. It's just that your build already installed the thing before anyone noticed. Holding a new version back for a week turns somebody else's incident into your prevention. You don't have to be the one who catches it." — Alex Burrage, Director or Product Security at Chainguard

Hardening workflow actions

The actions in your workflows are dependencies too, but with commit access and secrets access, and they usually look like two harmless lines of YAML. GitHub warns that a compromised action can read repository secrets and use the GITHUB_TOKEN to write to the repository. Treat a third-party action as executable code in your CI environment. Pin it to an immutable commit, review updates, and restrict the job’s token and secret access.

Pinning actions to full-length commit SHAs

GitHub's secure use reference recommends pinning each action to the full commit SHA you reviewed, rather than to a tag. Tags can later point to different code after a maintainer update or account compromise, so a future workflow run could execute code you never approved. The workflow line below shows the change required. Replace the tag after @ with the action’s audited 40-character commit SHA.

steps:
  # Use the audited full-length commit SHA, not a tag.
  - uses: actions/checkout@FULL_LENGTH_COMMIT_SHA

GitHub has since shipped immutable releases, generally available in October 2025, which lock a release's tag and assets after publication, and has said Actions dependency lockfiles are on the way. Neither replaces SHA pinning yet. Immutable releases require you to opt in per repository; there is no policy to require that the actions you consume use them, and a locked tag guarantees the bytes have not changed, not that the maintainer publishing them is uncompromised. GitHub still describes a full-length commit SHA as the only way to consume an action as an immutable release.

Pinning actions creates an update task because someone must review and adopt newer action revisions. In GitHub, Dependabot is an update bot that opens pull requests for those revisions, and it updates both the pinned SHA and the version comment next to it. Confirm the new SHA corresponds to a tagged release rather than a branch tip before merging, since Dependabot has known bugs that can pin to an untagged commit or move a pin backward. That is also the argument against auto-merging action updates.

A CODEOWNERS file assigns people or teams to review specified paths, such as .github/workflows/**. That file identifies the right reviewers, but it does not block a pull request on its own. To make review mandatory, require code-owner approval in your branch rules so that workflow updates cannot be merged without CI/CD review. This keeps action versions current without allowing unreviewed changes into the pipeline. This stage leaves behind three artifacts for the next handoff, the pinned SHA in the workflow file, the reviewed pull request that updated it, and, where the action publishes one, the action's own provenance attestation.

Hardening how workflows handle untrusted input

Avoid interpolating pull request titles, branch names, issue bodies, or commit messages directly into shell scripts. GitHub recommends passing context values into intermediate environment variables or purpose-built actions so untrusted strings never become shell syntax.

Chainguard Actions pushes this stage one level earlier. Every Action is hardened and continuously scanned for vulnerable pipeline configurations, with protections covering tag hijacking, dependency confusion, pull_request_target abuse, and secret exfiltration through workflow logs, and each one ships with an SBOM and provenance attestation. That does not remove your obligation to review workflow permissions, but it gives the action itself a stronger custody record.

Publishing evidence with artifacts

Even with trusted inputs and hardened actions, you still end up with an unverified artifact. Before publishing the release artifact, attach evidence to its digest, including the passing test run and a clean vulnerability scan, so downstream systems can verify it alongside the signature. By the time an image, package, binary, or chart leaves CI, the next stage should be able to validate what built it, which source revision it came from, what it contains, and that it has not been modified.

The release stage needs three pieces of evidence:

  • A signature that ties the artifact digest to an identity.

  • An SBOM that lists the components inside the artifact.

  • Provenance or attestation that describes the build system, source, parameters, and output.

SLSA v1.2 says attestations should be bound to artifacts, not releases. A release can contain separate images or binaries for different platforms and architectures, each with its own digest. Release-level evidence cannot prove which of those artifacts your deployment system will run. That’s why you should bind the signature, SBOM, and provenance to the deployed digest so policy can verify the exact artifact and reject a substituted one.

Sign container images by digest, not by tag, since tags are human-friendly pointers while the digest is the immutable thing your admission policy can verify later.

# Replace the digest with the immutable digest your build published.
IMAGE="registry.example.com/app@sha256:IMAGE_DIGEST"
cosign sign --yes "$IMAGE"
cosign attest --yes --predicate sbom.spdx.json --type spdxjson "$IMAGE"

# slsaprovenance = SLSA v0.2. Use slsaprovenance1 for SLSA v1.0+.
cosign attest --yes --predicate provenance.json --type slsaprovenance "$IMAGE"

Cosign 3.0 and later write these attestations in the new Sigstore bundle format by default, so confirm your verification step and admission controller understand bundles before you roll this out. Pin the Cosign version your pipeline installs, for the same reason you pin actions.

Chainguard Containers already follows this evidence-first model. Each image is continuously rebuilt from verified source, and Chainguard signs all container images and their attestations, including SBOMs, so teams can verify image authenticity, contents, and build provenance with Cosign. If your application uses a Chainguard base image, you can verify the base image's evidence before building, and then publish evidence for your own application image.

With evidence bound to the digest, the deployment system can stop trusting a registry tag just because someone named it prod and instead require a signed digest, an SBOM, and provenance that matches the expected repository, workflow, branch, and builder identity.

Gating deployment with policy

An unsigned image will deploy exactly as readily as a signed one unless you enforce signature checks at admission. Kubernetes admission controllers intercept API requests after authentication and authorization but before persistence, which makes them a natural place to reject workloads that do not meet your artifact policy.

Start with a narrow deploy policy that you can explain in one sentence:

Only admit images that are pinned by digest, signed by an approved identity, built from an approved repository, and accompanied by an SBOM and provenance.

Then, encode that policy in the enforcement layer your platform already uses. For Kubernetes, Sigstore policy-controller is purpose-built for verifying Cosign signatures and attestations, Kyverno covers the same ground with a broader policy language that extends to resource configuration, and Gatekeeper works well if you are already running OPA across your cluster, though, unlike Kyverno, it has no built-in image signature verification; checking Cosign signatures from Gatekeeper requires wiring up an external data provider such as Ratify.

If your cloud provider offers a managed admission feature, that is worth evaluating before adding a self-managed component. For GitOps, apply the same requirements to the controller path so that a manual manifest change cannot bypass the release evidence. For VM or serverless deployments, enforce at the deploy service, artifact promotion gate, or environment protection rule.

Roll out admission policy in stages rather than rejecting every noncompliant deployment on day one. Existing pipelines may not yet produce all the signals the policy expects, such as a digest pin, signature, SBOM, or provenance. Use the following four phases to identify noncompliant deployments, give service owners time to remediate them or request an exception, and then enforce the policy:

  1. Audit mode: log which workloads would fail signature, digest, SBOM, or provenance checks.

  2. Warning mode: notify service owners and block only new high-risk patterns, such as mutable tags in production namespaces.

  3. Enforcement mode: reject unsigned or unverifiable artifacts in production.

  4. Exception mode: require time-bound, reviewed exceptions with an owner and expiration date.

Beyond the baseline policy, require the provenance itself to identify the approved repository, workflow, branch, and builder, and reject any artifact with missing, invalid, or inconsistent evidence.

Evaluate your CI/CD pipeline security in 30 minutes

Securing your CI/CD pipeline does not require turning every release into a paperwork exercise. Start with one service, trace the chain of custody from pull request to production, and document the controls, evidence, and policies at each handoff.

Here's a 30-minute exercise for this week. Pick one production image and fill in this custody record:

  • Commit that built it.

  • Workflow that ran.

  • Runner that executed the job.

  • Dependencies that entered the build.

  • SBOM and provenance attached to the digest.

  • Admission policy that verified it before deployment.

The completed record gives you the evidence to trace a production artifact to its approved change, the build and inputs that produced it, and the policy that authorized its deployment.

If the answers require forensic digging, Chainguard can help replace brittle, reactive steps with secure-by-default open source, hardened CI/CD actions, and verifiable artifacts that carry their own evidence forward. Contact Chainguard today and see how we can help your team make trusted inputs and verifiable artifacts the default in every pipeline.

Share this article
Execute commandCG System prompt

$ chainguard learn --more

Contact us