how to secure a CI pipeline from supply-chain attacks
A hardening skill for CI pipelines against supply-chain attacks: pinning actions by SHA, least-privilege tokens, OIDC instead of stored credentials, separating build from publish, and provenance. Use when securing GitHub Actions or any CI, or after a compromised-dependency incident. Triggers: 'secure CI pipeline', 'pin github actions SHA', 'CI supply chain'. Not for: build speed fixes, SAST selection.
how to secure a CI pipeline from supply-chain attacks
TL;DR
Treat your CI config as an attack surface: pin every third-party action to a full commit SHA, run jobs with least-privilege tokens, use OIDC instead of stored cloud credentials, and keep the job that builds separate from the job that publishes. Most supply-chain incidents start with a compromised action or a leaked token, and these four controls blunt both.
how to secure a CI pipeline from supply-chain attacksUse this when
- You are hardening GitHub Actions or another CI system
- A compromised dependency or action makes you audit the pipeline
- You need a supply-chain checklist for a security review
- You are writing CI config for releases and publishing
Not for this skill when
- The problem is slow or flaky builds (performance, not security)
- You only need artifact signing theory
- You are choosing a SAST scanner (different decision)
Steps
1. Pin every third-party action to a full commit SHA
Tags like v4 are mutable; a compromised tag poisons every pipeline that references it. Pin the SHA and keep the tag as a comment so humans can still read the version.
grep -rn "uses:" .github/workflows/ | grep -v "@" | head; grep -rhn "uses: .*@[0-9a-f]\{40\}" .github/workflows/ | wc -lExpected: the first command shows any unpinned uses lines (fix those first); the second counts SHA-pinned references. Convert each uses: actions/checkout@v4 to uses: actions/checkout@[full-sha] # v4, verifying the SHA from the action's own repo.
2. Give the pipeline token the least privilege it needs
The default token used to be effectively admin. Set restrictive permissions at the top of each workflow and grant more only where a job needs it.
permissions:
contents: readExpected: every workflow declares its permissions explicitly; jobs that need more (releases needing contents write, for example) declare it on the job, not the workflow. Audit with the API or the workflow linter of your choice; undeclared permissions are the finding.
3. Replace long-lived cloud credentials with OIDC
Stored access keys in CI secrets get exfiltrated and live forever. OIDC lets the CI job mint short-lived credentials bound to the exact repo, branch, and workflow.
grep -rn "aws-access-key-id\|azure-credentials\|gcp-credentials" .github/workflows/ | headExpected: no matches after migration. Configure the cloud trust policy to allow only your repo's workflows, then delete the stored keys. A leaked OIDC token expires in minutes and only works for that workflow; a leaked static key works until someone notices.
4. Separate build from publish and gate releases
The job that compiles code should not hold publishing credentials. Split workflows: build and test on every PR, publish only on tagged releases after required reviews, with the publish job holding the narrow deploy credential.
grep -rln "publish\|release" .github/workflows/ | xargs grep -l "pull_request"Expected: ideally no output, meaning publish workflows dont trigger on PRs. A PR-triggered job with publish credentials is a privilege-escalation path: anyone who can open a PR can run it.
5. Add secret scanning, dependency scanning, and provenance
Enable push protection so secrets never land in the repo, run a dependency scanner in CI on every PR, and generate build provenance (SLSA-style attestations) so consumers can verify what built the artifact.
gitleaks detect --source . --no-git -v && osv-scanner --recursive ./Expected: both clean. Wire them as required checks so a PR introducing a secret or a known-vulnerable dependency cant merge without someone explicitly overriding.
Variant: github actions security best practices
The checklist version: SHA pinning, least-privilege permissions, OIDC, no publish on PR, Dependabot for actions updates, required reviews on workflow changes (workflow files are code that runs with your secrets).
Variant: compromised github action what to do
Incident version: pin everything to known-good SHAs immediately, rotate every secret the workflows could access, check run logs for exfiltration during the compromise window, and treat artifacts built in that window as untrusted until rebuilt.
Variant: securing gitlab or circleci pipelines
Same principles, different knobs: pin orbs and images by digest, scope project tokens, use OIDC (both support it), separate protected-branch deploy jobs. The attack surface is the same shape everywhere.
Why this happens
CI runs attacker-influenced code (your PRs, your dependencies, third-party actions) with your most powerful credentials. Every convenience (mutable tags, broad tokens, stored keys, publish-on-PR) trades security for ergonomics, and attackers shop exactly those conveniences. The controls above dont slow the pipeline down; they just stop it from being a confused deputy.
Edge cases and pitfalls
- SHA pinning without Dependabot: pinned SHAs go stale and miss security fixes; run Dependabot or Renovate on actions so pins stay fresh.
- Reusable workflows: a pinned caller can still pull a floating callee; pin the whole chain or vendor the shared workflows.
- OIDC trust too broad: allowing
repo:*in the trust policy defeats the purpose; scope to the exact repo and ref patterns. - Secrets in fork PRs: secrets arent available to fork PRs by default; keep it that way and use label-gated workflows if you need CI secrets for external contributors.
- Cache poisoning: poisoned caches persist across runs; include lockfile hashes in cache keys and treat cache writes as privileged.
- Self-hosted runners: a compromised job owns the runner; use ephemeral runners or you inherit every previous job's leftovers.
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_mCV9j2jXlQ26RMugxIRrKQ
Maintainer review
No maintainer verification is recorded for this version.
This records the version a maintainer checked. It does not assert that the version is the latest upstream release.