How to secure your CI/CD pipeline
But on March 14, 2025, an attacker redirected every version tag of the tj-actions/changed-files incident from v1 through v45.0.7 to a malicious commit. The action was used in more than 23,000 repositories. Its payload scanned runner memory and printed secrets into workflow logs.
Scanning for CVEs won’t catch malicious packages with zero CVEs. Those packages can compromise the systems that build, test, or release your software. Treating YAML as harmless configuration is a category error. A pipeline is production infrastructure, whether or not your organization treats its YAML as security-critical.
The short answer: pin third-party actions, set explicit permissions, replace long-lived cloud secrets with narrowly trusted OIDC, lock dependencies and generate SBOMs, then sign and verify release artifacts.
So this guide uses NIST’s Secure Software Development Framework (SSDF) as a risk-based map. It follows trust from source to build to artifact to deployment, then gives you an ownership model and a five-control order of operations.

In this article
- Attackers can enter before a CVE exists
- Use SSDF to choose controls, not to fill a spreadsheet
- Protect the source and workflow before you scan code
- Make every build resolve the code you intended
- Use OIDC for cloud access, with a narrow trust policy
- Treat the artifact as evidence you can verify
- Deployment must verify the approved artifact
- If you can do only five things, do them in this order
- Give the platform team ownership and security an exception path
- AI-assisted code still enters the same trust chain
- Keep the four trust transitions measurable
Attackers can enter before a CVE exists
Start somewhere other than the CVE report. It tells you about known flaws in components. It cannot establish trust in your workflow, runner, credentials, or release process.
I wouldn’t use Phoenix Security’s report of 59 campaigns and 657 malicious packages as a forecast. Its useful point is narrower: malicious inputs can be clean of known CVEs.
And assume an attacker can alter an upstream action, submit fork code, compromise a dependency publisher, or change CI configuration. Stop that code before it gains the next credential or release privilege.
Three routes deserve separate treatment:
- Pipeline injection: workflow code, reusable workflows, action references, UI configuration, or environment variables cause the runner to execute attacker-controlled instructions.
- Dependency compromise: a malicious package enters through a compromised publisher, dependency confusion, or a version range that resolves differently later.
- Developer-environment compromise: stolen credentials or altered source originate from a workstation, editor, local tool, or token before code reaches the repository.
These routes meet at the runner. It runs commands and receives credentials. It resolves dependencies and produces artifacts. Export dashboard configuration, review environment-variable changes, and expire runner credentials; committed YAML is not the whole pipeline.
Your minimum pipeline audit record should capture execution context. Record the commands run, secrets accessed, and artifacts modified. Include the workflow identity, commit, runner, and deployment decision. Repository events alone leave too much missing.
Use SSDF to choose controls, not to fill a spreadsheet
NIST published SP 800-218, SSDF v1.1 in February 2022. Its four practice groups are:
- Prepare the Organization (PO): establish people, policies, processes, and secure development environments.
- Protect the Software (PS): protect source code, dependencies, systems, and credentials.
- Produce Well-Secured Software (PW): build, test, verify, and release software securely.
- Respond to Vulnerabilities (RV): identify, assess, remediate, and communicate vulnerabilities.
NIST describes SSDF as a planning basis rather than a compliance worksheet:
“The SSDF provides a basis for planning and implementing a risk-based approach rather than a checklist to follow.”
That distinction determines how you use the framework here. PO covers ownership and environment preparation, including exceptions. PS covers source, actions, dependencies, runners, and credentials. PW covers build and release evidence. RV covers impact analysis, remediation, and recovery.
NIST’s SSDF v1.2 announcement calls version 1.2 an initial public draft released on December 17, 2025. Treat v1.2 as draft guidance here; build your baseline on the final v1.1 publication. NIST also announced live DevSecOps guidelines in March 2026 that had previously been open for comment.
Use the framework as a map, then build guardrails into your pipeline.
Protect the source and workflow before you scan code
Require review for workflow files, deployment configuration, reusable workflows, and any file that can change execution or release behavior. Signed commits can help establish authorship, though they bring key-management work. Prioritize them after permissions and action references unless your organization already operates signing well.
Set GitHub Actions permissions explicitly:
permissions:
contents: read
Use {} when a job needs no repository permissions. Grant a write scope only to the job that needs it. A job that comments on pull requests might receive pull-requests: write. A test job should receive neither by default.
On GitHub, a GITHUB_TOKEN limited to contents: read cannot push repository contents or create releases through that token. Audit the job for other credentials and inherited access.
Be careful with pull_request_target. It runs in the base repository’s context and may receive its permissions and secrets. Checking out a fork’s head commit and running its scripts gives the fork author’s code trusted access.
Run fork code in a restricted environment without repository secrets. If a privileged job must consume its output, treat artifacts as untrusted bytes: validate their format before privileged processing, never execute embedded scripts, and never use them to construct privileged commands.
In SSDF terms, this is PS because the controls protect source and execution inputs before production begins. PO supplies the review policy, permission defaults, and owner for exceptions.
Buying a larger scanner before controlling these paths is backwards.
Make every build resolve the code you intended
Pin third-party actions to reviewed commit SHAs:
# Illustrative abbreviated SHA; production requires the verified full SHA
- uses: tj-actions/changed-files@0e58ed8
A production workflow should use the complete 40-character commit SHA:
- uses: tj-actions/changed-files@<verified-full-40-character-sha>
The abbreviated 0e58ed8 value is useful for explaining the difference between a mutable tag and a commit reference. It isn’t a complete production pin.
A tag such as @v45 is a mutable pointer. As Safeguard’s technical write-up puts it:
“A git tag is just a mutable pointer, and anyone with push access to the upstream repository — including an attacker who has compromised a maintainer’s account or npm-style publish token — can move it to a different commit at any time.”
Pinning third-party actions to immutable commit SHAs is one of the least glamorous and highest-value controls in CI/CD security. A floating tag is a trust decision you’re making on every run.
SHA pins create maintenance work. Use Dependabot or Renovate to propose reviewed updates. Inspect the commit change, release notes, and permission changes before merging. Returning to floating tags for convenience defeats the control.
Dependencies need the same discipline. Commit package-lock.json and poetry.lock. Commit go.sum too. Verify package signatures where the ecosystem supports them. Monitor unexpected dependency-tree changes. Don’t treat a range such as npm’s ^1.2.3 as sufficient control. Use the package manager’s lockfile-enforcing install mode in CI, and review every lockfile change.
In SSDF terms, SHA pinning and dependency verification protect software inputs under PS. The resulting, repeatable build supports PW.
Use OIDC for cloud access, with a narrow trust policy
Stored cloud credentials may remain valid until someone rotates or revokes them. OpenID Connect (OIDC) changes how the workflow authenticates: the job exchanges a GitHub-issued JWT for temporary provider credentials. The JWT does not itself grant cloud access.
A job might declare:
permissions:
contents: read
id-token: write
The cloud provider then evaluates a trust policy. Restrict that policy by repository and branch. Add the deployment environment or equivalent claims where relevant. A token from an arbitrary repository should never assume your production role.
The credentials typically expire in roughly an hour, though the exact lifetime depends on the provider and configuration. OIDC is preferable to long-lived cloud secrets, but it is not magic: a badly scoped trust policy still turns temporary credentials into powerful credentials.
Apply least privilege twice. Limit the CI job’s permissions, then limit the cloud role’s actions and resources. Review both when a workflow changes.
Masking isn’t an access control. A compromised step can still attempt to exfiltrate any secret available to its process, and exact-string masking may not catch encoded, split, reformatted, or non-log output.
In SSDF terms, this is PS because it protects credentials and the systems that use them. PO determines who reviews the trust policy and how exceptions expire.
Treat the artifact as evidence you can verify
A defensible release carries evidence about its output. Generate a CycloneDX or SPDX software bill of materials (SBOM) in every build. An SBOM records components, including transitive dependencies, so responders can identify affected production artifacts after a vulnerability disclosure.
NIST SSDF v1.1 added PS.3.2 for provenance data. Provenance links an artifact to its source commit, workflow, and build environment. A signature establishes integrity only within a trusted key and policy boundary.
| Control | Question answered | Recommended response |
|---|---|---|
| SBOM | Where is this dependency deployed? | Quarantine or rebuild affected artifacts. |
| Signature | Was this artifact produced by an authorized process? | Reject an unverifiable artifact. |
| Provenance | What source and build produced it? | Investigate or rebuild from trusted inputs. |
These controls are Blackhawk’s operational mapping of PW and RV: produce release evidence, then use it to assess impact and respond.
Deployment must verify the approved artifact
Build-time checks lose their value if deployment can select an arbitrary output. Enforce deployment rules as code.
A useful policy requires a signed artifact and an approved source repository and branch. It also checks the workflow identity and required checks. Store the digest and source commit as signed metadata. Include the workflow identity and check results in an immutable release record that deployment policy can read.
Use this sequence:
- Build the artifact and generate its SBOM and provenance.
- Sign it through the authorized release process.
- Require policy checks before promotion.
- Verify the signature, digest, provenance, and required checks at deployment.
- Confirm that the running artifact matches the approved artifact.
- Record the workflow identity, commit, and action SHAs. Record the runner identity and credential claims. Add the artifact digest, commands executed, secret access, artifact modifications, and deployment decision.
Separate build and deployment privileges where possible; a test job shouldn’t automatically possess production authority. See how to secure SaaS applications for the cloud-side controls.
This is the PW-to-RV handoff: release evidence becomes deployment feedback and incident evidence.
If you can do only five things, do them in this order
This ordering is our operational judgment, not a measured universal ranking. Runner isolation, production architecture, and release design can move an item upward in your environment. Your blast radius depends on your environment; measure it before declaring the work complete.
| Priority | Control | Why it comes here | SSDF intent |
|---|---|---|---|
| 1 | Pin third-party actions to reviewed commit SHAs and automate update pull requests. | It controls what code every job executes before other defenses can help. | PS / PW |
| 2 | Set explicit least-privilege workflow and cloud permissions. | It limits the damage when a step or action is compromised. | PS |
| 3 | Replace long-lived cloud credentials with narrowly trusted OIDC. | It reduces credential lifetime after permissions have been constrained. | PS / PO |
| 4 | Lock and verify dependencies, then generate an SBOM for every build. | It makes build inputs traceable and gives response teams an inventory. | PS / PW / RV |
| 5 | Sign and verify release artifacts, with digest and provenance policy plus execution logs. | It controls the final transition from build output to deployment. | PW / RV |
The fifth row is one release-evidence workstream with separate implementation tasks. Don’t mistake grouped planning for one giant gate.
SAST analyzes source code, SCA analyzes dependencies, and secret scanning searches source and history for credential-like material. Add container scanning where it produces an actionable failure. Don’t make a scanner the only reason a release is trusted.
Give the platform team ownership and security an exception path
The platform or DevOps team should own workflow templates, permission defaults, action updates, runner configuration, and policy checks. Security should define the baseline, review high-impact changes, and handle exceptions.
Security approval for every routine workflow edit is a queue disguised as governance. Fail automatically on an unpinned action, an exposed secret, an untrusted artifact, or a production-role change. Report lower-confidence findings without blocking the pull request.
Every exception needs an owner, reason, compensating control, expiry date, and review path. A permanent exception is an undocumented change to your security model.
Developers should see the failed control and its remediation path. Reserve escalation for new privileged actions, production-role changes, or deliberate policy bypasses.
That is PO in operational form: prepare the organization so security work has a home instead of becoming a queue.
AI-assisted code still enters the same trust chain
NIST has finalized SP 800-218A, an SSDF Community Profile for Generative AI and Dual-Use Foundation Models. It provides governance context for teams using AI-assisted development; it doesn’t replace CI/CD implementation controls.
AI-generated code should follow the same branch protection, dependency verification, testing, secret scanning, provenance, and review process as human-written code. Its origin doesn’t establish safety. Evidence does.
The zero-CVE problem reinforces the point: known-vulnerability scanning cannot establish trust by itself. Record the source commit, constrain inputs, test the result, and preserve artifact evidence.
Keep the four trust transitions measurable
Define a path as a workflow job plus the credentials, action references, artifacts, and deployment target it can reach. Inventory each path with its trigger and token scopes. Record its secrets and cloud role. Include its artifact outputs and deployment targets.
Control the four transitions next. Authorize the source and constrain build inputs. Preserve artifact evidence and verify deployment. SSDF gives you the risk-based map; ownership and measurable failure conditions keep it alive. Begin with the highest-privilege jobs and third-party actions.