Parameter

CI/CD pipeline security

Also known as

  • CI/CD security
  • Build pipeline security

CI/CD pipeline security is the practice of controlling who and what can change, trigger and run your build and deployment pipelines, and what those pipelines can reach, so an attacker who gets into a branch, a third-party action or a runner cannot use the pipeline's secrets and deploy rights to ship code or reach production.

Category
Supply chain
Last reviewed

What does CI/CD pipeline security cover?

It covers four things: who can change what the pipeline runs, what identity the pipeline holds, where the jobs execute, and whether anyone can prove what the pipeline produced. A CI system is a remote code execution service by design. The security question is whose code it executes and with which credentials.

The most useful map is the OWASP Top 10 CI/CD Security Risks, which grew out of research at Cider Security. Its ten entries, with their exact labels:

IDRiskWhat it looks like
CICD-SEC-1Insufficient Flow Control MechanismsOne person can push code that reaches production with no second approval
CICD-SEC-2Inadequate Identity and Access ManagementStale accounts, shared admin users, over-broad personal access tokens
CICD-SEC-3Dependency Chain AbuseThe build pulls a malicious or look-alike package
CICD-SEC-4Poisoned Pipeline Execution (PPE)A pull request changes what the pipeline runs
CICD-SEC-5Insufficient PBAC (Pipeline-Based Access Controls)Every job gets every secret and full network reach
CICD-SEC-6Insufficient Credential HygieneLong-lived cloud keys in CI variables, secrets printed to logs
CICD-SEC-7Insecure System ConfigurationAn unpatched build server, default settings, exposed admin consoles
CICD-SEC-8Ungoverned Usage of 3rd Party ServicesUnreviewed apps and actions with write access to repositories
CICD-SEC-9Improper Artifact Integrity ValidationNobody verifies that the deployed artifact is the one the build made
CICD-SEC-10Insufficient Logging and VisibilityNo audit trail for who changed a workflow or used a secret

Two of these have their own pages: poisoned pipeline execution (CICD-SEC-4) and dependency confusion, the best-known form of CICD-SEC-3.

How should a pipeline authenticate to the cloud?

With short-lived tokens issued per job through OIDC federation, not with access keys stored as CI secrets. A stored key works from anywhere, for anyone who reads it, until someone rotates it. An OIDC token is minted for one job, expires with it, and carries claims the cloud provider can check.

On GitHub Actions, the job requests a token from GitHub's OIDC provider and exchanges it with the cloud for temporary credentials. The job needs the id-token: write permission to request one:

# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]

permissions:
  contents: read
  id-token: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0
        with:
          role-to-assume: arn:aws:iam::111122223333:role/demo-deploy
          aws-region: us-east-1
      - run: ./scripts/deploy.sh

The security lives in the cloud-side trust policy. AWS requires a condition on token.actions.githubusercontent.com:sub and rejects a value that is only a wildcard, because an unscoped policy lets workflows in other people's repositories assume your role:

{
  "Effect": "Allow",
  "Principal": { "Federated": "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com" },
  "Action": "sts:AssumeRoleWithWebIdentity",
  "Condition": {
    "StringEquals": {
      "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
      "token.actions.githubusercontent.com:sub": "repo:demo-org/demo-app:environment:production"
    }
  }
}

Scoping to an environment, as above, means only jobs that pass that environment's protection rules can deploy. Two details to check: GitHub states that repositories created after July 15, 2026 use an immutable subject format that embeds owner and repository IDs (repo:OWNER@OWNER-ID/REPO@REPO-ID:...), so copy the sub your tokens actually carry. And a StringLike pattern such as repo:demo-org/* hands the role to every workflow in every repository in the organization.

Why pin third-party actions to a commit SHA?

Because tags move. GitHub's hardening guide says pinning to a full-length commit SHA is currently the only way to use an action as an immutable release. A tag like @v4 is a pointer the action's maintainer, or whoever takes over their account, can repoint at new code.

That is what happened with tj-actions/changed-files in March 2025 (CVE-2025-30066). NVD records that tags v1 through v45.0.7 were modified to point at a malicious commit, and CISA's alert describes the payload exposing secrets such as access keys, personal access tokens and npm tokens in workflow logs. Workflows pinned to an older SHA ran the code they had reviewed; workflows pinned to a tag ran the attacker's.

Pinning has a cost: you stop receiving fixes automatically. Pair it with Dependabot or Renovate so version bumps arrive as reviewable pull requests that change the SHA.

How do you isolate runners?

Give every job a fresh machine and nothing it does not need. GitHub-hosted runners are ephemeral virtual machines. Self-hosted runners are whatever you built, and GitHub's guidance is that they should almost never be used for public repositories, since anyone can open a pull request and run code on them.

  • Use ephemeral self-hosted runners (one job, then destroyed), not long-lived hosts that keep caches, credentials and tool installs between jobs.
  • Separate runner pools by trust: untrusted pull-request builds never share a pool, or a network segment, with deployment jobs.
  • Restrict runner egress to the registries and endpoints the build needs; a build that can reach the cloud instance metadata service or the internal network is a pivot point.
  • Set permissions: at the top of every workflow to contents: read and raise it per job, so the GITHUB_TOKEN cannot push or approve by default.

What are SLSA build levels?

SLSA (Supply-chain Levels for Software Artifacts) is an OpenSSF framework for proving how an artifact was built. The current version is SLSA v1.2, announced November 24, 2025, which added a Source track next to the existing Build track. The Build track has four levels:

LevelNameWhat it adds
Build L0No guaranteesNothing; the absence of SLSA
Build L1Provenance existsThe build platform generates provenance describing how the artifact was built
Build L2Hosted build platformBuilds run on a hosted platform that signs the provenance, so consumers can check it was not forged
Build L3Hardened buildsRuns cannot influence one another, and user-defined build steps cannot reach the provenance signing secrets

L3 is where CICD-SEC-9 gets closed: a deployment step that verifies signed provenance before release will refuse an artifact that did not come from the expected repository and workflow.

How do you assess a pipeline?

Start from the pipeline definitions and trace credentials outward; most findings are visible without running anything.

  1. Inventory every pipeline file and every system that can trigger one: .github/workflows/, .gitlab-ci.yml, Jenkinsfile, plus webhooks and scheduled jobs.
  2. List each secret and cloud role, then which jobs, branches and events can reach it (CICD-SEC-5, CICD-SEC-6).
  3. Grep for uses: lines not pinned to a 40-character SHA, and for installed apps with repository write access (CICD-SEC-8).
  4. Check branch protection and required reviews on every branch that deploys (CICD-SEC-1).
  5. Read cloud trust policies for wildcard sub conditions.
  6. Look for untrusted input interpolated into shell steps, such as ${{ github.event.pull_request.title }} inside run:, which GitHub documents as a script injection path. Pass such values through env: instead.
  7. Search logs and artifacts for leaked credentials with a secrets detection tool.
  8. Prove any finding with a harmless marker, never by reading a real secret, and coordinate with the owner, since the code runs on their infrastructure.

Written by Parameter · Last reviewed

[ related terms ]

Related terms.

Poisoned pipeline execution (PPE)

Poisoned pipeline execution (PPE) is a CI/CD attack where someone who can push a branch or open a pull request changes the pipeline definition, or a file the pipeline runs such as a Makefile or test script, so the build system executes their commands with the pipeline's secrets and permissions.

Dependency confusion

Dependency confusion is a supply chain attack where an attacker publishes a package to a public registry under the same name as a company's internal package, usually with a higher version number, so build tools that consult both sources install the attacker's code and run it on developer machines and CI servers.

Secrets detection

Secrets detection is the automated search of source code, git history, CI logs, container images and build artifacts for credentials such as API keys, tokens, private keys and database passwords, using provider patterns, entropy checks and live verification, so exposed secrets are found and revoked before an attacker uses them.

Software bill of materials (SBOM)

A software bill of materials (SBOM) is a machine-readable inventory of the components inside a piece of software, listing each library's name, version, supplier, unique identifier and dependency relationships, so the people who build, buy or run the software can check which of those components have known vulnerabilities or license problems.

Shift-left security

Shift-left security is the practice of moving security checks earlier in software development, into design, the developer's editor and the pull request, so threat models, static analysis, dependency checks and secrets scanning catch flaws before code merges, while a fix is still a small edit to the author's own change.