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:
| ID | Risk | What it looks like |
|---|---|---|
| CICD-SEC-1 | Insufficient Flow Control Mechanisms | One person can push code that reaches production with no second approval |
| CICD-SEC-2 | Inadequate Identity and Access Management | Stale accounts, shared admin users, over-broad personal access tokens |
| CICD-SEC-3 | Dependency Chain Abuse | The build pulls a malicious or look-alike package |
| CICD-SEC-4 | Poisoned Pipeline Execution (PPE) | A pull request changes what the pipeline runs |
| CICD-SEC-5 | Insufficient PBAC (Pipeline-Based Access Controls) | Every job gets every secret and full network reach |
| CICD-SEC-6 | Insufficient Credential Hygiene | Long-lived cloud keys in CI variables, secrets printed to logs |
| CICD-SEC-7 | Insecure System Configuration | An unpatched build server, default settings, exposed admin consoles |
| CICD-SEC-8 | Ungoverned Usage of 3rd Party Services | Unreviewed apps and actions with write access to repositories |
| CICD-SEC-9 | Improper Artifact Integrity Validation | Nobody verifies that the deployed artifact is the one the build made |
| CICD-SEC-10 | Insufficient Logging and Visibility | No 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.shThe 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 tocontents: readand raise it per job, so theGITHUB_TOKENcannot 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:
| Level | Name | What it adds |
|---|---|---|
| Build L0 | No guarantees | Nothing; the absence of SLSA |
| Build L1 | Provenance exists | The build platform generates provenance describing how the artifact was built |
| Build L2 | Hosted build platform | Builds run on a hosted platform that signs the provenance, so consumers can check it was not forged |
| Build L3 | Hardened builds | Runs 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.
- Inventory every pipeline file and every system that can trigger one:
.github/workflows/,.gitlab-ci.yml,Jenkinsfile, plus webhooks and scheduled jobs. - List each secret and cloud role, then which jobs, branches and events can reach it (CICD-SEC-5, CICD-SEC-6).
- Grep for
uses:lines not pinned to a 40-character SHA, and for installed apps with repository write access (CICD-SEC-8). - Check branch protection and required reviews on every branch that deploys (CICD-SEC-1).
- Read cloud trust policies for wildcard
subconditions. - Look for untrusted input interpolated into shell steps, such as
${{ github.event.pull_request.title }}insiderun:, which GitHub documents as a script injection path. Pass such values throughenv:instead. - Search logs and artifacts for leaked credentials with a secrets detection tool.
- 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.
[ Sources ]
- OWASP Top 10 CI/CD Security Risks
- GitHub Docs: Security hardening for GitHub Actions (secure use reference)
- GitHub Docs: OpenID Connect reference
- AWS IAM: Configuring a role for the GitHub OIDC identity provider
- SLSA v1.2: Build track basics
- CISA: Supply chain compromise of tj-actions/changed-files (CVE-2025-30066) and reviewdog/action-setup
Written by Parameter · Last reviewed

