Software Securityhome
Fix a findingSearch
Phase 3 · Development

Pipeline hardening

Pipeline hardening is configuring your CI/CD system so that the code, tools and people that trigger a build cannot take its credentials or push unreviewed changes toward a release. It covers triggers, token permissions, third-party actions and images, runners, secrets, and the branches and tags releases start from.

A pipeline holds the keys to production: registry tokens, cloud roles and signing identities. Scanners and code review check the application, not the workflow building it. In March 2025 attackers moved version tags of the tj-actions/changed-files action to a malicious commit that printed runner secrets into build logs. Every workflow that referenced a tag ran it; a workflow pinned to a known-good commit SHA did not.

Who
Platform or DevOps engineers who own the CI/CD configuration, with AppSec reviewing changes to workflows and release jobs
When
When a workflow is created or changed, when a runner type or integration is added, and before a repository goes public
How often
A workflow linter on every change; token scopes, runners and integrations reviewed on a regular schedule
You end up with
Read-only default tokens, pinned actions and images, untrusted jobs split from release jobs, and protected branches, tags and workflow files

Do this

  1. Map who can trigger what

    List the triggers that run on outsider input, such as pull_request, pull_request_target, issue_comment and workflow_run in GitHub Actions or merge request pipelines from forks in GitLab, and the jobs that can publish, deploy, sign or read secrets. The risk sits where the two lists overlap. OWASP calls the attack Poisoned Pipeline Execution: changing the pipeline, or a file it runs, so that attacker code runs inside the build.

  2. Make tokens read-only by default

    In GitHub Actions, set permissions: contents: read, or permissions: {}, at the top of every workflow and grant extra scopes only to the job that needs them. Replace long-lived cloud keys with short-lived OIDC credentials whose trust policy names the repository and environment. The OWASP rule is that each pipeline should only have access to the credentials it needs, and each step only to the secrets it requires.

  3. Keep untrusted code away from secrets

    Build and test fork pull requests under pull_request, where GitHub gives them a read-only token and no secrets. Workflows on pull_request_target or workflow_run must not check out or run pull request code, and must treat its artifacts as data. Pass event text such as a pull request title through env: instead of writing ${{ }} inside run:. In GitLab, mark deployment variables as protected so only pipelines on protected branches or tags receive them.

  4. Pin third-party actions and images

    Reference each action by its full commit SHA with the version in a comment, such as uses: actions/checkout@<full SHA> # v7.0.1, and each container image by digest. GitHub states that a full-length commit SHA is currently the only way to use an action as an immutable release. Pins also freeze out fixes, so let Dependabot or Renovate propose updates, and require SHA pinning by organization policy where your platform supports it.

  5. Isolate the runners

    Prefer hosted or ephemeral runners that are discarded after each job. GitHub warns that self-hosted runners should almost never be used for public repositories, because anyone who opens a pull request can compromise the environment. Do not share a runner between pipelines of different sensitivity, return each node to a clean state after every job, and limit its network access to what the job needs.

  6. Protect branches, tags and workflow files

    Require a reviewed pull request and passing checks on the default and release branches, with no bypass list, and stop release tags from being moved or deleted. Put the workflow directory under CODEOWNERS so changes to it need a named reviewer. Gate production with a deployment environment that requires approval: GitHub withholds environment secrets from a job until a reviewer approves it.

  7. Lint every workflow change

    Run a workflow linter such as zizmor or actionlint in CI, or OpenSSF Scorecard's Dangerous-Workflow, Token-Permissions and Pinned-Dependencies checks, so a change that brings back a dangerous trigger, an unpinned action or a broad token fails the build. Paste a workflow into GitHub Actions workflow analyzer for a quick local check; CI security checks covers the scanners these jobs run.

Done when

Workflow permissions are read-only by default, and release jobs cannot be triggered from forks.

Mark it in the gap check

Common mistakes

  • Using pull_request_target to give fork builds access to secrets, then checking out the fork's code. Its build scripts run with your secrets and a token that can write to the repository.
  • Pasting ${{ github.event.pull_request.title }} into a run: script. A crafted title becomes shell commands; Nx believes this is how an attacker obtained the npm token used to publish malicious Nx versions in August 2025.
  • Pinning to a version tag such as @v4. Whoever controls the action's repository can move the tag to new code; only a full commit SHA is immutable.
  • Pinning to a SHA and never updating it. GitHub's 2026 actions/checkout guard against fork checkouts reached floating major tags automatically, but SHA, minor and patch pins had to be upgraded to get it.
  • Running public-repository workflows on a persistent self-hosted runner, where a fork pull request can leave code behind for the next job.
  • Treating log masking as protection. GitLab notes that masking is not a guaranteed way to stop malicious users reading a value; rotate any secret that appears in a log.

OWASP CI/CD risks and the controls above

Five of the OWASP Top 10 CI/CD Security Risks map directly to this practice. The mapping to controls is this site's own.

OWASP CI/CD risks and the controls above: table
OWASP riskWhat goes wrongControl
CICD-SEC-1 Insufficient Flow Control MechanismsOne account pushes code or artifacts toward production with no reviewProtected branches and tags, required review, approval before deployment
CICD-SEC-4 Poisoned Pipeline ExecutionA pull request changes the pipeline or a file it runs, and attacker code runs in the buildFork code runs without secrets; workflow files need owner review
CICD-SEC-5 Insufficient PBAC (Pipeline-Based Access Controls)Code running in a job uses the job's broad permissions to move furtherRead-only default tokens, per-job scopes, isolated runners
CICD-SEC-6 Insufficient Credential HygieneLong-lived secrets spread across pipelines and logsShort-lived OIDC credentials, secrets scoped per job and environment
CICD-SEC-8 Ungoverned Usage of 3rd Party ServicesA third-party integration or build tool gets more access than it usesApproved and pinned actions, least-privilege integrations, unused ones removed

Tools and guides

Standards

NIST SSDF 1.1
PO.3 Implement Supporting ToolchainsPO.5 Implement and Maintain Secure Environments for Software DevelopmentPS.1 Protect All Forms of Code from Unauthorized Access and Tampering

Sources

  1. GitHub Docs: Secure use reference for GitHub Actions
  2. OWASP Top 10 CI/CD Security Risks
  3. OWASP CICD-SEC-4: Poisoned Pipeline Execution (PPE)
  4. OWASP CICD-SEC-5: Insufficient PBAC (Pipeline-Based Access Controls)
  5. OWASP CICD-SEC-6: Insufficient Credential Hygiene
  6. GitHub changelog: Safer pull_request_target defaults for actions/checkout (2026-06-18)
  7. GitHub Docs: Deployments and environments (required reviewers and environment secrets)
  8. GitLab Docs: CI/CD variables (protected and masked variables)
  9. GitHub Advisory GHSA-mrrh-fwg8-r2c3 (CVE-2025-30066): tj-actions/changed-files
  10. Nx security advisory GHSA-cxm3-wv7p-598c: malicious versions of Nx and some supporting plugins were published. 27 August 2025.
  11. OpenSSF Scorecard: checks documentation
  12. zizmor audit rules
  13. NIST SP 800-218: Secure Software Development Framework (SSDF) Version 1.1. February 2022.

Find your next answer

Type to search. Search runs in your browser.

Searches run in your browser, but the host can infer them from the index files they load. The address shows your search so you can bookmark or share it.