Harden your CI/CD pipeline against supply chain attacks. Covers runner security, artifact signing, RBAC, pipeline secrets management, and audit logging.
Your CI/CD pipeline is the engine of software delivery: code pushed to a branch triggers builds, tests, and deployments across environments. This page guides you through securing that engine—specifically, hardening the pipeline against vulnerabilities and enforcing granular access control. It matters when you’re deploying to production, when a security incident involves a compromised build or deployment artifact, or when a developer accidentally promotes a vulnerable dependency to staging.
A vulnerability scan is not a one-off audit. It must be embedded in every phase of the CI/CD lifecycle: from source code and container images to dependencies and runtime artifacts.
When a developer opens a pull request, the pipeline should run static analysis and dependency scanning on the entire codebase. Use snyk for dependency vulnerability detection and sonarqube for code quality and security.
name: PR Security Scan
on: pull_request
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Snyk
run: |
snyk test --file=package.json --org=your-org --project-name=web-app
snyk monitor --file=package.json
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- name: Run SonarQube Analysis
run: |
sonar-scanner \
-Dsonar.projectKey=web-app \
-Dsonar.sources=. \
-Dsonar.host.url=https://sonar.yourcompany.com \
-Dsonar.login=${{ secrets.SONAR_TOKEN }}
The snyk test command scans package.json and reports all known vulnerabilities in npm packages. It fails the job if any high or critical severity vulnerability is introduced. snyk monitor sends results to a central dashboard and sends alerts on new vulnerabilities.
Every image built in the pipeline must be scanned for vulnerabilities using trivy, a lightweight, open-source scanner.
# .github/workflows/build.yml
name: Build and Scan Image
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker Image
run: |
docker build -t myapp:v1.2.3 .
- name: Scan Image with Trivy
run: |
trivy image --exit-code 1 --severity HIGH,CRITICAL myapp:v1.2.3
env:
TRIVY_TOKEN: ${{ secrets.TRIVY_TOKEN }}
The --exit-code 1 flag causes the job to fail if any high or critical vulnerability is found. Trivy reports vulnerabilities by CVE ID, severity, and a link to the upstream advisory. If a new image has a known RCE vulnerability in nginx:alpine, the pipeline stops before deployment.
Use renovate to automatically update dependencies and trigger a security scan on every update.
{
"extends": ["config:base"],
"packageRules": [
{
"packagePatterns": ["^@types/"],
"schedule": ["every 14 days"]
},
{
"packageNames": ["react", "express"],
"automerge": true,
"automergeType": "pr"
}
],
"postUpdateTasks": {
"commands": [
"npm run test",
"npm run build",
"trivy image --exit-code 1 myapp:latest"
]
}
}
Renovate creates PRs for dependency updates. When a PR is opened, it runs tests and a Trivy scan. If the scan fails, the PR is marked as requiring review, and the PR title includes the number of new vulnerabilities.
Access control in CI/CD ensures that only authorized people and systems can trigger, approve, or promote pipelines. It prevents unauthorized changes and provides auditability.
Use a declarative policy language in ci-policies.yaml to define who can do what across the pipeline.
# ci-policies.yaml
policies:
- name: "Deploy to Production"
description: "Allow team leads to deploy to prod after review"
permissions:
- pipeline: "deploy-prod"
actions:
- trigger
- approve
- promote
conditions:
- user: "team-leads"
- branch: "main"
- environment: "production"
- name: "Merge to Main"
description: "Allow all contributors to merge to main branch"
permissions:
- pipeline: "ci"
actions:
- trigger
- merge
conditions:
- branch: "main"
- user: "contributors"
- name: "Security Review"
description: "Require security team to approve before production"
permissions:
- pipeline: "security-review"
actions:
- approve
conditions:
- environment: "production"
- approval: "required"
This file is checked into version control and applied at pipeline startup. Each job reads the policy, checks conditions, and enforces access.
In the CI configuration, use require: true and reviewers: ["security-team", "platform-team"] to require approval before merging.
# .github/workflows/ci.yml
name: CI Pipeline
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
- name: Merge to Main
run: |
git checkout main
git merge --ff-only ${{ github.ref }}
git push origin main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
required_reviews: 2
reviewers: ["security-team", "platform-team"]
The required_reviews field sets the minimum number of approvals, and reviewers specifies which teams must approve. If a PR is merged without a security team review, the pipeline logs an error: PR missing required review from security-team.
Only allow certain identities to trigger specific pipelines. Use trigger: "workflow_dispatch" with inputs and permissions to define who can start a job.
# .github/workflows/manual-deploy.yml
name: Manual Deploy to Staging
on:
workflow_dispatch:
inputs:
environment:
description: "Target environment"
required: true
type: choice
options: ["staging", "canary", "production"]
version:
description: "Version to deploy"
required: true
type: string
permissions:
contents: read
deployments: write
pull-requests: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy
run: |
echo "Deploying version $VERSION to $ENVIRONMENT"
# Deploy logic here
env:
VERSION: ${{ github.event.inputs.version }}
ENVIRONMENT: ${{ github.event.inputs.environment }}
When a user triggers this workflow, GitHub checks the user’s permissions. The permissions block ensures that only users with pull-requests: write can trigger it. A developer without the required permissions sees: Permission denied: user does not have pull-requests: write.
Promoting a build from one environment to another must require explicit approval and validation.
Define a promotion workflow that checks for approvals before moving to the next stage.
# .github/workflows/promote-to-prod.yml
name: Promote to Production
on:
workflow_dispatch:
inputs:
release-notes:
description: "Release notes for this version"
required: true
type: string
jobs:
promote:
runs-on: ubuntu-latest
steps:
- name: Check Approval
run: |
if ! git log --oneline -1 | grep -q "Approved by security-team"; then
echo "Error: No approval from security-team"
exit 1
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Run Smoke Tests
run: |
curl -f http://prod.myapp.com/health
curl -f http://prod.myapp.com/api/v1/users
env:
PROD_URL: "http://prod.myapp.com"
- name: Deploy to Production
run: |
kubectl apply -f manifests/prod.yaml
kubectl rollout status deployment/web-app -n production
env:
KUBECONFIG: ${{ secrets.KUBECONFIG }}
The git log check ensures that the PR has a commit message including "Approved by security-team". If the smoke tests fail, the deployment job logs: HTTP 503: Service Unavailable. The kubectl rollout status command waits until the deployment is fully ready or times out after 5 minutes.
Use curl to call an external approval service when a promotion is pending.
# .github/workflows/external-approval.yml
name: External Approval for Production
on:
workflow_dispatch:
inputs:
approval-url:
description: "URL to external approval service"
required: true
type: string
jobs:
approve:
runs-on: ubuntu-latest
steps:
- name: Request Approval
run: |
curl -X POST \
-H "Content-Type: application/json" \
-d '{
"title": "Promote version $VERSION to production",
"url": "$APPROVAL_URL",
"metadata": {
"version": "$VERSION",
"environment": "production"
}
}' \
https://approval.yourcompany.com/api/v1/request
env:
VERSION: ${{ github.event.inputs.version }}
APPROVAL_URL: ${{ github.event.inputs.approval-url }}
The external service returns a JSON response with approval_id, status, and expires_at. The pipeline waits for the approval ID to be updated or for the status to be approved. If the service returns {"status": "pending", "reason": "Review in progress"}, the workflow logs: Approval pending: waiting for external review.
The pipeline fails silently if not properly instrumented.
Log every significant action in the pipeline using structured JSON.
# .github/workflows/logging.yml
name: Log Events
on: workflow_run
jobs:
log:
runs-on: ubuntu-latest
steps:
- name: Log Pipeline Event
run: |
echo '{
"event": "pipeline_started",
"pipeline": "${{ github.workflow }}",
"ref": "${{ github.ref }}",
"sha": "${{ github.sha }}",
"author": "${{ github.event.sender.login }}",
"timestamp": "'"$(date -Iseconds)"'"
}' > /tmp/event.json
curl -X POST \
-H "Content-Type: application/json" \
-d @/tmp/event.json \
https://logging.yourcompany.com/api/v1/events
The date -Iseconds command produces 2024-04-05T12:34:56+00:00, which is consumed by a central logging system.
--exit-code 1 flag is critical. Without it, a low-severity vulnerability would not block deployment.postUpdateTasks field ensures that every dependency update runs the full pipeline.check approval step must use git log or git commit to verify that a commit exists with the required message. A simple grep fails silently if the message is slightly off.curl request up to three times with exponential backoff.Your CI/CD pipeline is the first line of defense. Vulnerability scanning must run at every stage: source, dependency, and artifact. Access control must be enforced through declarative policies, required approvals, and identity-based triggers. Promotions across environments require structured logs, approval gates, and integration with external systems. When the pipeline fails, it must do so explicitly—no silent failures. Use exact commands, exact configurations, and precise failure modes. This is how you secure a CI/CD pipeline.
This page was rewritten on 16 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy