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 "${{ secrets.SLACK_WEBHOOK }}" \
-H "Content-Type: application/json" \
-d '{"text": "Production deploy completed successfully"}'
Pipeline Security Hardening Checklist
Pipeline Security Checklist
- Branch protection with mandatory reviews and status checks
- OIDC instead of static secrets for cloud authentication
- Minimal workflow permissions (least privilege)
- Mandatory signed commits on the main branch
- Action pinning with SHA instead of tags
- Production environment with manual approval
- Active audit log for all CI/CD operations
- Self-hosted runners in isolated network (if applicable)
- Secret scanning on pipeline configuration
- SBOM generated and signed on every build
GitHub Actions Pinning
# INSECURE: uses a mutable tag
- uses: actions/checkout@v4
# SECURE: uses commit SHA (immutable)
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
CI/CD Security Best Practices
- OIDC everywhere: eliminate all static secrets for cloud provider authentication
- Minimal permissions: each job has only the necessary permissions, nothing more
- Pin actions by SHA: never use mutable tags for third-party actions
- Separate environments: staging and production with different configurations and approvals
- Audit everything: log every build, deploy, approval, and pipeline modification
- Ephemeral runners: runners should be recreated on each job to avoid persistence
Conclusions
CI/CD pipeline security is often overlooked, but it's a critical attack vector. A compromised pipeline can inject malicious code into every subsequent build. With branch protection, OIDC, least privilege, and deployment approvals, you build a resilient and verifiable pipeline.
In the next article, we'll explore IaC Scanning, analyzing how to validate Terraform, CloudFormation, and ARM configurations to prevent security misconfigurations.
Related deep dive: 07 - Secret Management - Automazione e Rotazione di Credenziali.
Related deep dive: 08 - Policy as Code - Enforcement di Security Policies nel Deployment.
Frequently asked questions
What is the primary concern when an attacker compromises a CI/CD pipeline?
An attacker can inject malicious code into every subsequent build and deploy.
How do branch protection rules contribute to pipeline security?
Branch protection rules ensure that no code reaches the main branch without reviews and automated checks.
What is the benefit of using OpenID Connect (OIDC) in CI/CD workflows?
OIDC allows authentication with cloud providers without using static secrets, such as API keys or service account keys.
Why are signed commits important for pipeline security?
Signed commits guarantee contributor identity through GPG or SSH cryptographic signatures.
Commenti
Caricamento commenti...
Sign in to comment