CI/CD pipelines are the single most productive target for a modern attacker. One compromise gets you source code, deployment credentials, production access, and a pre-authorized path into every environment the pipeline touches. The evidence is on the record: Codecov's bash uploader breach in April 2021 leaked credentials from thousands of downstream repositories, CircleCI's January 2023 incident exfiltrated customer OAuth tokens and session cookies from engineering laptops that had authenticated to CircleCI, and the September 2024 compromise of the tj-actions/changed-files GitHub Action — which was used by 23,000+ repositories — exposed secrets from any workflow that ran the poisoned version. Zero trust for CI/CD isn't a slogan; it's a specific set of controls that treats every pipeline execution as untrusted until it proves otherwise.
Why are CI/CD pipelines such a high-value target?
CI/CD pipelines are high-value because they accumulate privileges no other system in the organization holds. A production deployment pipeline has write access to production artifact registries, cloud accounts, Kubernetes clusters, and secret stores. A release pipeline can sign artifacts, publish packages, and push to customer environments. A build pipeline has read access to every repository in the organization. An attacker who lands on a build runner for five minutes can steal long-lived credentials, inject code into the next release, or plant backdoors that persist across rebuilds.
The attack surface is also unusually broad. A pipeline pulls from Git, runs third-party actions, fetches package dependencies, mounts secrets, and executes arbitrary code written by any engineer who can land a commit. Every one of those is a code-execution vector. The 2022 PHP Packagist incident, the 2023 CircleCI compromise, and the 2024 tj-actions/changed-files supply chain attack all exploited one of these vectors; none required exploiting a novel vulnerability. They just required finding a pipeline that trusted its inputs.
What does OIDC federation replace and why does it matter?
OIDC federation replaces long-lived cloud credentials in CI secrets with short-lived tokens minted per workflow run. The pattern works like this: the CI platform (GitHub Actions, GitLab CI, CircleCI, Buildkite) issues an OIDC token describing the workflow identity — repository, ref, actor, workflow path. The cloud provider (AWS, GCP, Azure) validates that token against a trust policy and, if the policy matches, issues a short-lived credential scoped to the specific role the policy permits. No AWS access key or GCP service account JSON ever sits in the CI secret store.
The practical consequence: a stolen GitHub token from a compromised runner or action buys the attacker nothing persistent. They might be able to run a malicious step once, but they can't exfiltrate a credential that lasts beyond the workflow run. The 2024 tj-actions/changed-files attack extracted secrets, but for the portion of affected repositories that used OIDC federation exclusively, the exfiltrated tokens were expired by the time the attackers could process them at scale. OIDC federation is now the default pattern for AWS IAM Roles Anywhere, GCP Workload Identity Federation, and Azure workload identities; teams that are still storing AWS_SECRET_ACCESS_KEY in GitHub Secrets in 2026 are accepting an obsolete risk.
How should you pin and audit third-party actions?
Pin every third-party action by full commit SHA, not tag, and maintain an allow-list of actions permitted in your repositories. The tj-actions/changed-files compromise is the textbook case: the attacker pushed a malicious commit, retagged existing tags (v35, v41, v42) to point at the malicious commit, and every workflow pulling by tag silently upgraded to the backdoored version within hours. Workflows pinned to a specific SHA (tj-actions/changed-files@a284dc1814e3fc707dfb7c35a2e1b0b1d2d8df9c) never moved. Dependabot can still update SHAs on a scheduled cadence, but the update goes through PR review instead of silently at runtime.
GitHub's organization-level allowed-actions setting restricts which actions workflows can reference, and should be set to selected with an explicit list. The list should include GitHub-created actions (actions/*), your organization's own actions, and a curated set of vetted third-party actions reviewed by the security team. Anything outside the list fails at parse time before the workflow runs. GitLab and Jenkins have analogous controls (shared pipeline library allow-lists, Jenkins approved signatures). The policy should be enforced at the organization level, not left to individual repositories.
Alongside pinning, audit what actions actually run. actionlint lints workflow files locally; zizmor, released in 2024, statically analyzes GitHub workflows for dangerous patterns like pull_request_target combined with checkout of the PR ref, unpinned actions, and secrets exposure in logs. Run both as required checks on any workflow file change.
What does a minimal hardened GitHub Actions workflow look like?
A minimal hardened workflow uses OIDC for cloud auth, pins actions by SHA, sets restrictive permissions at the job level, scopes environments with required reviewers, and never passes secrets into third-party action inputs. Here's the shape:
name: Release
on:
push:
tags: ['v*']
permissions: {} # default to zero
jobs:
release:
runs-on: ubuntu-24.04
environment:
name: production
url: https://app.example.com
permissions:
contents: read
id-token: write # OIDC token for AWS
attestations: write # SLSA provenance
packages: write # GHCR push
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
with:
role-to-assume: arn:aws:iam::111122223333:role/ci-release
aws-region: us-east-1
- run: ./scripts/build.sh
- uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4
with:
subject-path: 'dist/*'
Notice what's absent: no AWS_ACCESS_KEY_ID, no secrets.DEPLOY_TOKEN, no ${{ github.event.pull_request.title }} interpolated into a shell step. The permissions: {} block at the top defaults every job to zero permissions and each job declares only what it needs. The environment: production gate routes the deployment through a protected environment that can require a manual approver before the job continues. persist-credentials: false on checkout prevents the Git credential from leaking into later steps.
Why do branch protection and required reviews matter this much?
Branch protection and required reviews matter because the signing and deployment identity your workflow holds is only as trustworthy as the commit that triggered it. If any engineer can push directly to main and trigger a production release, you haven't built zero trust — you've built single-engineer trust. Zero-trust pipelines require that the commit itself came through a controlled process: protected branch, required PR review from a different person, CODEOWNERS approval for sensitive paths, and signed commits where available.
GitHub's rulesets (GA in 2024) supersede classic branch protection and support layered rules across patterns, bypass actors, and required status checks. For a production release workflow, the ruleset should require at least one review from a non-author CODEOWNER, require signed commits, require all status checks to pass, and block force pushes. Bypass should be restricted to specific break-glass accounts with audited access. The controls stack: OIDC federation prevents credential theft, pinned SHAs prevent action compromise, environment gates prevent unauthorized deploys, and branch protection prevents unauthorized source. Remove any layer and the others can be bypassed.
What should secret rotation and environment scoping look like?
Secrets should be scoped to specific environments, rotated on a schedule that doesn't depend on human memory, and audited for usage so dormant secrets can be deprovisioned. Environment scoping in GitHub Actions (and the equivalent in GitLab and CircleCI) means a secret is only available to workflows targeting that environment, not to every workflow in the repository. A PRODUCTION_DB_PASSWORD scoped to the production environment cannot be read by a preview workflow or a PR workflow, which closes the most common path for secret exfiltration.
Rotation should be automated wherever possible — OIDC federation eliminates the need for most long-lived secrets entirely, and for the secrets that remain (signing keys, webhook secrets, API keys for services that don't support OIDC), rotation should run on a schedule via Vault, AWS Secrets Manager, or GCP Secret Manager. The key is that no human needs to remember to rotate, and no dashboard requires manual action. Audit logs from both the secret manager and the CI platform should feed into a SIEM, and any secret that hasn't been read in 90 days should be a candidate for removal.
How Safeguard.sh Helps
Safeguard.sh treats your CI/CD configuration as a first-class asset in the supply chain graph, not as infrastructure someone else worries about. Our SBOM generation and ingestion module catalogs every third-party action, every base image, and every package your pipelines pull, mapping them to the workflows and repositories that use them. Reachability analysis cuts 60-80% of CVE noise by focusing on vulnerabilities in code paths your pipelines and applications actually execute, so an action with a critical CVE in an unused subcommand doesn't drown out the one that touches your deploy credentials. Griffin AI autonomous remediation can open PRs to repin actions to safe SHAs, migrate secrets to OIDC federation, and harden workflow permissions — often the same day a compromise like tj-actions/changed-files is disclosed. The TPRM module extends the same analysis to vendor pipelines, so when a third party ships artifacts built on unpinned or unscoped workflows, the risk surfaces in your vendor risk review. With 100-level dependency depth scanning and container self-healing, the path from a compromised upstream action to a breached production environment closes before the pipeline finishes its next run.