CI/CD Security Controls Every Engineering Team Should Have
Published · Strata Security
1Your Pipeline Has More Privilege Than Your Production Servers
A CI/CD pipeline typically holds deploy credentials, cloud provider keys, signing certificates, and access to every repository it builds. Compromise the pipeline and an attacker doesn't need to find a vulnerability in your application — they can just ship their own code through your own deployment process, signed with your own keys. Pipeline security is really two separate problems: keeping bad code from merging, and keeping the pipeline itself from being compromised independent of what code it's asked to run.
2Branch Protection: The First Gate
Before any pipeline runs against production, code has to get into a protected branch. The controls that matter most here are the ones that are easy to configure once and easy to forget exist:
- Require pull request reviews before merging — including a rule against self-approval.
- Require status checks to pass (tests, linting, and any security scan) before the merge button is even available.
- Restrict who can push directly to protected branches — direct pushes bypass every other control on this list.
- Disallow force-push and branch deletion on protected branches, which otherwise lets someone silently rewrite history.
- Require linear history or signed commits where your workflow supports it, so the commit graph itself is harder to tamper with retroactively.
3Repository Hygiene Nobody Schedules Time For
Branch protection controls what happens at merge time. A separate set of controls governs who and what has standing access to the repository at all, and these tend to decay silently because nothing forces a review:
- Collaborator and team access reviewed on a schedule, not left to accumulate from every past contractor and integration.
- Personal access tokens and deploy keys with an expiry, rotated rather than treated as permanent.
- Third-party GitHub Apps and OAuth grants audited — a forgotten integration with write access is a standing risk nobody is watching.
- Repository secrets scoped to the environment that needs them, not copied into every workflow "just in case."
4Secrets Belong in the Pipeline, Not the Codebase
Both GitHub Actions and GitLab CI provide encrypted variable storage specifically so secrets never need to exist as plaintext in a repository. The control that's easy to miss is scoping: a secret available to a production deploy job should not automatically be available to every job in the pipeline, and definitely not to a job triggered by a pull request from a fork. Most CI systems support environment-scoped secrets for exactly this reason — use them rather than one global secret set shared across every job.
jobs:
build:
runs-on: ubuntu-latest
# No 'environment:' here -- this job gets no deploy credentials,
# even though it's in the same workflow file.
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test
deploy:
needs: build
runs-on: ubuntu-latest
environment: production # only THIS job can see production secrets
steps:
- uses: actions/checkout@v4
- run: ./deploy.sh
env:
DEPLOY_KEY: ${{ secrets.PRODUCTION_DEPLOY_KEY }}5Fork Pull Requests Are a Different Trust Boundary
A pull request from a fork runs code you don't control, in a context that — if misconfigured — can have access to the same secrets and permissions as a trusted internal branch. GitHub Actions' pull_request_target trigger is the canonical example: it runs with the permissions of the target branch even though the code under test came from the fork, which is exactly backwards if the workflow then checks out and executes that fork's code. The safer default is to treat fork-triggered workflows as untrusted by default: no secrets, no write permissions, and a manual approval step before a first-time contributor's workflow runs at all.
# DANGEROUS: runs with target-branch permissions/secrets,
# but checks out and executes the fork's own code
on: pull_request_target
jobs:
test:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: npm ci && npm test # runs untrusted fork code with target-branch secrets
# SAFER: runs with fork-level permissions only -- no secrets,
# no write access, regardless of what the fork's code does
on: pull_request
jobs:
test:
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test6Artifact Integrity
A pipeline that's secure right up until the build step still ships a compromised artifact if the build step itself pulls in something untrusted. Two habits close most of that gap:
Pinning a third-party GitHub Action to @v2 means the maintainer — or anyone who compromises their account — can change what that tag points to at any time, and your pipeline will run it without warning on the next build. Pinning to a specific commit SHA means the code that runs is exactly the code you reviewed.
# Risky: "@v2" can be repointed by the maintainer at any time
- uses: some-org/some-action@v2
# Safer: this exact commit is what runs, regardless of what
# the maintainer's "v2" tag points to later
- uses: some-org/some-action@a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2A minimal software bill of materials is the artifact-level equivalent of a dependency lockfile: a record of exactly what shipped, independent of whether you wrote it. CycloneDX and SPDX are the two widely-used formats:
{
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"components": [
{
"type": "library",
"name": "lodash",
"version": "4.17.21",
"purl": "pkg:npm/lodash@4.17.21"
}
]
}7Deployment Security
The build and deploy stages don't need the same permissions, and treating them identically is a common source of unnecessary blast radius. A build job needs to compile and test code; a deploy job needs credentials to push to production. Keeping those permission sets separate — and requiring a manual approval gate before a production deploy job runs — means a compromised build step can't deploy anything on its own. Every deploy should also produce a log entry recording who or what triggered it, which commit, and when, independent of your application's own logging.
8A Minimum Viable Control Set
| Control | GitHub Actions | GitLab CI |
|---|---|---|
| Required reviews before merge | Branch protection rules | Merge request approval rules |
| Required status checks | Required status checks in branch protection | Pipelines must succeed (merge request settings) |
| Scoped, encrypted secrets | Environment secrets | Protected + masked CI/CD variables |
| Pinned third-party actions/images | Pin Action to a commit SHA | Pin Docker image by digest, not :latest |
| Fork/external contributor gating | Require approval for first-time contributors | Pipelines for merge requests from forks require approval |
| Manual production deploy gate | Required reviewers on an Environment | when: manual on the deploy job |
9Where This Fits in the ATT&CK Framework
MITRE ATT&CK catalogs T1195 — Supply Chain Compromise as a distinct adversary technique, with a specific sub-technique for exactly this threat model: T1195.001 — Compromise Software Dependencies and Development Tools. A mutable action tag repointed to malicious code, or a compromised npm package pulled in at build time, are concrete instances of that sub-technique — not a theoretical risk category. Framing pipeline controls against a named, publicly documented technique rather than a vague "supply chain security" label makes it easier to reason about which controls actually mitigate which step of an attack.
10Common Mistakes
- "Administrators can bypass" left enabled. Branch protection rules commonly ship with an option that lets repository admins skip every rule just configured — a single compromised or careless admin account undoes the whole control set.
- Protecting the wrong branch. Rules applied to
maindon't help if the actual deploy pipeline builds from a separaterelease/*ordeploybranch that was never protected the same way. - Required review satisfied by a bot or self-merge alt account. A review requirement that can be satisfied by an automated account, or by a second account the same person controls, provides the appearance of review without the substance of it.
- Secrets not rotated after offboarding. A deploy key or token generated under a departed contractor's access often keeps working long after their account is disabled, because rotating pipeline secrets isn't part of the offboarding checklist.
11Where Automated Scanning Fits
The controls above govern who can do what and when. They don't tell you whether the code itself introduces a vulnerability. That's a separate, complementary layer — see the Repository Security Assessment Checklist for what to look for in the code, and CI/CD Security Scanning for how Strata runs that assessment automatically inside the pipeline controls described here.
This article supports CI/CD Security Scanner — see the product page for how Strata applies these ideas directly.
Visit CI/CD Security Scanner →