CI/CD Security: Protecting the Build and Deploy Process
The CI/CD pipeline is the heart of the software delivery process. If an attacker compromises the pipeline, they can inject malicious code into every subsequent build and deploy. Pipeline security is not just about what the pipeline tests, but how the pipeline itself is protected from tampering.
In this article, we will explore practices for securing the CI/CD pipeline: from branch protection to signed commits, from OIDC to least privilege, to audit logging and approval workflows for production deploys.
What You'll Learn
- CI/CD pipeline threat model
- Branch protection and code review enforcement
- OIDC for authentication without static secrets
- Least privilege for runners and workflows
- Signed commits and code verifiability
- Deployment approval workflows
CI/CD Pipeline Threat Model
To protect the pipeline, we need to understand where the attack surfaces are:
- Source code: a malicious contributor injects code in a PR
- Dependencies: a compromised dependency is installed during build
- Pipeline configuration: modification of workflow files to exfiltrate secrets
- Runner: build agent compromise for infrastructure persistence
- Artifact: replacement of the built artifact with a malicious one
- Deploy credentials: theft of deploy credentials for direct infrastructure access
Branch Protection Rules
Branch protection rules are the first line of defense. They ensure that no code reaches the main branch without reviews and automated checks.
Recommended Configuration
| Rule | Setting | Rationale |
|---|---|---|
| Require PR reviews | Minimum 2 approvals | Four-eyes principle |
| Dismiss stale reviews | Enabled | Invalidate approvals after push |
| Require status checks | SAST, SCA, test, lint | Mandatory automated gates |
| Require signed commits | Enabled | Contributor verifiability |
| Restrict push access | CI/CD bot only | No direct push to main |
| Require linear history | Enabled | Clean and verifiable history |
OIDC: Authentication Without Static Secrets
OpenID Connect (OIDC) allows CI/CD workflows to authenticate with cloud providers without using static secrets (API keys, service account keys). The workflow obtains a temporary token based on the repository and branch identity.
# .github/workflows/deploy-oidc.yml
name: Deploy with OIDC
on:
push:
branches: [main]
permissions:
id-token: write # Required for OIDC
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS Credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-deploy
aws-region: eu-west-1
# No secrets! OIDC token is used automatically
- name: Deploy to AWS
run: |
aws s3 sync dist/ s3://my-bucket/
aws cloudfront create-invalidation --distribution-id E123 --paths "/*"
Least Privilege for Workflows
Every GitHub Actions workflow has default permissions that can be too broad. The least privilege principle requires limiting permissions to the minimum necessary for each job:
# Restrictive global permissions
permissions:
contents: read # Read-only repository access
jobs:
test:
runs-on: ubuntu-latest
# No additional permissions for tests
steps:
- uses: actions/checkout@v4
- run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
permissions:
id-token: write # Only for OIDC
contents: read
packages: write # Only for image push
environment: production # Requires manual approval
steps:
- uses: actions/checkout@v4
- name: Deploy
run: ./deploy.sh
Signed Commits
Signed commits guarantee contributor identity through GPG or SSH cryptographic signatures. Without signing, anyone can create a commit with an arbitrary name and email, impersonating other developers.
# Configure GPG for signed commits
gpg --full-generate-key
gpg --list-secret-keys --keyid-format=long
# Configure Git to sign automatically
git config --global user.signingkey YOUR_KEY_ID
git config --global commit.gpgsign true
git config --global tag.gpgsign true
# Alternative: sign with SSH key
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
Deployment Approval Workflows
Production deploys should require explicit approval from authorized personnel. GitHub Environments allows configuring mandatory manual approvals:
# Deploy with manual approval
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging
steps:
- name: Deploy to staging
run: ./deploy.sh staging
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
url: https://myapp.com
# Requires approval from a designated reviewer
steps:
- name: Deploy to production
run: ./deploy.sh production
- name: Notify team
run: |
curl -X POST "






