How to Secure Your CI/CD Pipeline: Vulnerability Scanning and Access Control
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 has more access than any developer. It touches your source code, builds your binaries, handles your secrets, and deploys to production. If an attacker compromises your pipeline, they own everything. The SolarWinds attack (2020), the Codecov breach (2021), and the CircleCI incident (2023) all demonstrate that supply chain attacks through CI/CD are real, devastating, and increasingly common.
This guide covers the five layers of CI/CD security: secrets management, permissions hardening, build integrity verification, deployment protection, and continuous monitoring.
Why CI/CD Is a Prime Target
| Attack Vector | Real-World Example | Impact |
|---|---|---|
| Compromised build step | SolarWinds Orion (2020) | 18,000+ organizations backdoored through tampered build process |
| Leaked CI/CD secrets | CircleCI (2023) | Customer secrets exposed, requiring mass rotation |
| Poisoned dependency | Codecov (2021) | Bash uploader modified to exfiltrate credentials |
| Compromised runner | Crypto mining on GitHub Actions | Free-tier runners hijacked for cryptocurrency mining |
| Insider threat | Malicious commit from trusted dev | Direct code injection bypassing code review |
The common thread: CI/CD pipelines are trusted implicitly and often have the broadest access of any system in your organization.
Step 1: Secure Pipeline Secrets
1.1 Never Store Secrets in Code
# ❌ NEVER do this — secrets in plain text
env:
DATABASE_URL: "postgres://admin:P@ssw0rd@prod-db:5432/main"
# ✅ Use GitHub Secrets or a vault
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
Detection: Run git log --all -p | grep -iE 'password|secret|api_key|token' against your repository history. If anything matches, those secrets are compromised — rotate immediately and use git filter-branch or BFG Repo-Cleaner to remove them from history.
1.2 Use Environment-Scoped Secrets
Secrets should be scoped to specific environments with approval gates. Production secrets should never be accessible during a PR build.
# GitHub Actions — environment protection with required reviewers
jobs:
deploy-prod:
runs-on: ubuntu-latest
environment:
name: production
url: https://myapp.com
steps:
- name: Deploy
env:
# These secrets are ONLY accessible when deploying to 'production'
# The deployment won't proceed until a reviewer approves
AWS_ACCESS_KEY: ${{ secrets.PROD_AWS_ACCESS_KEY }}
AWS_SECRET_KEY: ${{ secrets.PROD_AWS_SECRET_KEY }}
run: ./deploy.sh
1.3 Rotate Secrets Automatically
Manual rotation is a security debt that accumulates. Automate it.
# AWS Secrets Manager — automatic rotation every 30 days
aws secretsmanager rotate-secret \
--secret-id prod/database \
--rotation-lambda-arn arn:aws:lambda:us-east-1:123456:function:rotate-db-secret \
--rotation-rules '{"AutomaticallyAfterDays": 30}'
1.4 Secrets Hygiene Checklist
| Practice | Priority | Implementation |
|---|---|---|
| No secrets in source code | P0 | Pre-commit hooks (gitleaks, detect-secrets) |
| Environment-scoped secrets | P0 | GitHub Environments, GitLab Protected Variables |
| Automatic rotation (30-90 days) | P1 | AWS Secrets Manager, Azure Key Vault, Vault |
| Short-lived credentials | P0 | OIDC federation (see Step 2) |
| Secret scanning on push | P0 | GitHub Advanced Security, GitGuardian |
| Audit log for secret access | P1 | Vault audit logs, AWS CloudTrail |
Step 2: Lock Down Pipeline Permissions
2.1 OIDC Federation (No Long-Lived Credentials)
Long-lived access keys are the #1 CI/CD security risk. OIDC federation eliminates them entirely. The CI runner requests a short-lived token from your cloud provider, scoped to the specific repository and branch.
permissions:
id-token: write # Required for OIDC
contents: read # Minimum required
steps:
- name: Configure AWS Credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole
aws-region: us-east-1
# No access keys needed! Token is:
# - Short-lived (1 hour default)
# - Scoped to this specific repo + branch
# - Auditable in CloudTrail
2.2 Least Privilege for Pipeline Service Accounts
Your pipeline needs only the permissions it uses. Audit and trim.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PushImages",
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:PutImage"
],
"Resource": "arn:aws:ecr:us-east-1:123456:repository/myapp"
},
{
"Sid": "DeployService",
"Effect": "Allow",
"Action": ["ecs:UpdateService"],
"Resource": "arn:aws:ecs:us-east-1:123456:service/myapp-prod/*"
}
]
}
2.3 GitHub Actions Permissions Best Practice
Always set permissions at the job level in GitHub Actions. The default token has write access to many scopes — restrict to minimum.
# At the workflow level, set restrictive defaults
permissions:
contents: read
jobs:
build:
permissions:
contents: read
packages: write # Only if publishing packages
# ...
Step 3: Verify Build Integrity
Supply chain attacks modify your artifacts between build and deployment. Signing and verification create a chain of trust.
3.1 Sign Your Container Images
# Cosign — sign container images with keyless signing (Sigstore)
cosign sign --yes myregistry.com/myapp:v1.2.3
# Verify signature before deployment (in your deployment pipeline)
cosign verify \
--certificate-identity "https://github.com/myorg/myapp/.github/workflows/build.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
myregistry.com/myapp:v1.2.3
3.2 Generate and Scan SBOM (Software Bill of Materials)
An SBOM lists every dependency in your build. When a new CVE is published, you can instantly determine if you are affected.
# Syft — generate SBOM in SPDX format
syft myregistry.com/myapp:v1.2.3 -o spdx-json > sbom.json
# Grype — scan SBOM for known vulnerabilities, fail on high/critical
grype sbom:sbom.json --fail-on high
3.3 Pin Dependencies and Actions
# ❌ Risky — uses mutable tag, could be compromised
- uses: actions/checkout@v4
# ✅ Secure — pinned to immutable SHA
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
Pinning to SHA prevents an attacker from modifying a published action version after you start using it.
Step 4: Protect Branch and Deployment Rules
# Branch protection via GitHub CLI
gh api repos/{owner}/{repo}/branches/main/protection \
--method PUT \
--field required_status_checks='{"strict":true,"contexts":["security-scan","tests"]}' \
--field enforce_admins=true \
--field required_pull_request_reviews='{"required_approving_review_count":2,"dismiss_stale_reviews":true}' \
--field restrictions=null
Required Checks Before Merge
| Check | Purpose | Blocks Merge? | Tool |
|---|---|---|---|
| Unit Tests | Code correctness | ✅ Yes | pytest, Jest, Go test |
| SAST Scan | Static code vulnerabilities | ✅ Yes | Semgrep, CodeQL, SonarQube |
| Dependency Audit | Known CVEs in dependencies | ✅ Yes (high/critical) | Dependabot, Snyk, Grype |
| Container Scan | Image vulnerabilities | ✅ Yes | Trivy, Grype, Snyk Container |
| Code Review | Human approval (2+ reviewers) | ✅ Yes | GitHub, GitLab |
| License Check | Open-source license compliance | ⚠️ Warning only | FOSSA, Licensee |
| Secret Scan | Accidental secret commits | ✅ Yes | gitleaks, GitGuardian |
Step 5: Monitor Pipeline Activity
Every deployment should produce an immutable audit log. When a security incident occurs, you need to answer: who deployed what, when, and from which commit.
audit-logging:
runs-on: ubuntu-latest
steps:
- name: Log Deployment to Audit System
run: |
curl -X POST "${{ secrets.AUDIT_WEBHOOK }}" \
-H "Content-Type: application/json" \
-d '{
"event": "deployment",
"environment": "production",
"commit": "${{ github.sha }}",
"actor": "${{ github.actor }}",
"timestamp": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'",
"workflow": "${{ github.workflow }}",
"run_id": "${{ github.run_id }}",
"image_digest": "'$IMAGE_DIGEST'",
"sbom_hash": "'$SBOM_HASH'"
}'
What to Monitor
- Unusual deployment times — Deployments at 3 AM by a developer who normally works 9-5
- Skipped checks — Admin overrides of required status checks
- New contributors — First-time contributors to sensitive repositories
- Self-merged PRs — PRs merged by the author without independent review
- Secret access patterns — Unusual vault/secret access from pipeline service accounts
Self-Hosted Runner Security
If you use self-hosted runners (GitHub Actions, GitLab Runners), they require additional hardening:
| Control | Implementation | Why |
|---|---|---|
| Ephemeral runners | Spin up fresh runner per job, destroy after | Prevents persistence from previous jobs |
| Network isolation | Runners in dedicated VPC/subnet with limited egress | Limits blast radius if compromised |
| No shared runners for public repos | Dedicated runners per org | Prevents crypto-mining and code exfiltration |
| Runner-level secrets | Inject secrets via vault at runtime, not environment variables | Prevents cross-job secret leakage |
CI/CD Security Hardening Priorities
| Priority | Action | Impact | Effort |
|---|---|---|---|
| 1 | Remove hardcoded secrets from pipelines | Critical — number one breach vector | Low |
| 2 | Pin all action and image versions to SHA | Critical — prevents supply chain attacks | Low |
| 3 | Enable branch protection (require reviews) | High — prevents unauthorized deployments | Low |
| 4 | Scan dependencies for known CVEs | High — catches known vulnerabilities | Medium |
| 5 | Scan container images before deployment | High — prevents vulnerable deployments | Medium |
| 6 | Implement OIDC for cloud auth (no long-lived keys) | High — eliminates key management | Medium |
| 7 | Add SAST scanning | Medium — catches code-level vulnerabilities | Medium |
| 8 | Implement signed commits and artifacts | Medium — ensures provenance | High |
Supply Chain Attack Prevention
- Pin GitHub Actions to commit SHA, not tags (tags can be moved maliciously)
- Use private registries for container images and npm packages
- Verify checksums on all downloaded binaries and dependencies
- Enable Dependabot or Renovate for automated dependency updates with review
Pipeline Security Checklist
- No secrets in source code (pre-commit hooks + scanning active)
- Environment-scoped secrets with approval gates for production
- OIDC federation in place (no long-lived credentials in CI/CD)
- Least-privilege service account permissions (audited quarterly)
- Container image signing enabled (Cosign/Sigstore)
- SBOM generation and vulnerability scanning on every build
- All actions/dependencies pinned to immutable SHAs
- Branch protection with required checks (SAST, tests, review)
- 2+ reviewer requirement for production deployment PRs
- Audit logging for all pipeline activities (immutable, searchable)
- Self-hosted runners hardened (ephemeral, network-isolated)
:::note[Source] This guide is derived from operational intelligence at Garnet Grid Consulting. For DevOps maturity assessments, visit garnetgrid.com. :::