CI/CD Security Best Practices: Securing Your Software Supply Chain
📅 Published: July 2026
⏱️ Estimated Reading Time: 22 minutes
🏷️ Tags: CI/CD Security, DevSecOps, Software Supply Chain, Pipeline Security
Introduction: The CI/CD Security Challenge
CI/CD pipelines are the backbone of modern software delivery. They automate everything from code commit to production deployment. But with great automation comes great responsibility. A compromised CI/CD pipeline can give attackers direct access to your source code, cloud infrastructure, and production data.
Why CI/CD security matters:
Single point of failure: The pipeline has access to everything
High privileges: Pipelines often run with elevated permissions
External access: Pipelines connect to GitHub, cloud providers, artifact registries
Automation scale: An attacker can leverage automation to move fast
Software supply chain: Your pipeline is part of your customers' supply chain
This guide covers the essential security practices to protect your CI/CD pipelines from source code to production.
Part 1: Source Control Security
Branch Protection Rules
Protect your main branches from direct commits and force pushes.
GitHub Branch Protection:
Settings → Branches → Add rule ✅ Require pull request reviews before merging ✅ Require approvals (set to 1-2) ✅ Dismiss stale approvals ✅ Require status checks to pass before merging ✅ Require branches to be up to date ✅ Require conversation resolution before merging ✅ Require signed commits ✅ Require linear history (no merge commits) ✅ Include administrators ❌ Allow force pushes ❌ Allow deletions
GitLab Protected Branches:
Settings → Repository → Protected Branches ✅ Allowed to merge: Maintainers ✅ Allowed to push: No one ✅ Code owner approval required ✅ Require approval from eligible approvers
CODEOWNERS
Specify who must review changes to critical files.
# .github/CODEOWNERS # Global owners * @security-team # Critical paths /terraform/ @platform-team @security-team /infrastructure/ @platform-team @security-team /security/ @security-team /README.md @docs-team
Secret Scanning
Prevent secrets from ever entering your repository.
GitHub Secret Scanning:
Settings → Code security and analysis → Secret scanning → Enable ✅ Push protection enabled
Pre-commit hooks:
#!/bin/bash # .git/hooks/pre-commit # Check for AWS keys if git diff --cached | grep -qE "AKIA[0-9A-Z]{16}"; then echo "❌ AWS Access Key detected!" exit 1 fi # Check for private keys if git diff --cached | grep -q "BEGIN.*PRIVATE KEY"; then echo "❌ Private key detected!" exit 1 fi # Check for passwords if git diff --cached | grep -iqE "password\s*=\s*.+"; then echo "❌ Potential password detected!" exit 1 fi
Part 2: Pipeline Security
Principle of Least Privilege
Grant only the minimum permissions needed. Use permissions in GitHub Actions:
permissions: contents: read # Read-only by default pull-requests: write # Only if needed id-token: write # Only for OIDC issues: read # Only if needed
Use OIDC Instead of Secrets
OIDC (OpenID Connect) eliminates long-lived credentials.
GitHub Actions OIDC:
- name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789012:role/github-actions aws-region: us-west-2
GitLab CI OIDC:
variables: AWS_ROLE_ARN: arn:aws:iam::123456789012:role/gitlab-actions AWS_REGION: us-west-2 before_script: - export AWS_WEB_IDENTITY_TOKEN_FILE=$(pwd)/token - echo $CI_JOB_JWT_V2 > $AWS_WEB_IDENTITY_TOKEN_FILE
Benefits of OIDC:
No secrets to rotate
Credentials are temporary (1 hour)
Every run is auditable
Access can be controlled by repository, branch, or environment
Pin Actions by Commit SHA
Never use @latest or @v3 without verifying.
# ❌ Bad - uses: actions/checkout@v3 # ✅ Better - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3
Automated Dependabot for actions:
# .github/dependabot.yml version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly"
Scan for Vulnerabilities in CI
GitHub Actions:
name: Security Scan on: [push, pull_request] jobs: codeql: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: github/codeql-action/init@v2 with: languages: javascript - uses: github/codeql-action/analyze@v2 snyk: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: snyk/actions/node@master env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} with: args: --severity-threshold=high
Part 3: Secret Management
Never Store Secrets in Code
❌ Bad:
env: API_KEY: sk_live_12345 # Exposed in logs
✅ Good:
env: API_KEY: ${{ secrets.API_KEY }} # Masked in logs
Use Environment-Specific Secrets
GitHub Environments:
Repository → Settings → Environments
Environment: production
secrets:
- API_KEY (prod-specific)
- DB_PASSWORD (prod-specific)
Environment: staging
secrets:
- API_KEY (staging-specific)
- DB_PASSWORD (staging-specific)Rotate Secrets Regularly
Automated rotation with AWS Secrets Manager:
aws secretsmanager rotate-secret --secret-id /prod/database/password
Use Dedicated Secret Scanning
GitHub Advanced Security:
Secret scanning detects accidental commits
Push protection blocks commits containing secrets
Alerts sent to repository admins
Part 4: Build and Artifact Security
Container Image Scanning
Scan all container images before deployment.
# GitHub Actions - name: Scan image uses: aquasecurity/trivy-action@master with: image-ref: myapp:${{ github.sha }} format: 'sarif' output: 'trivy-results.sarif' severity: 'CRITICAL,HIGH'
Sign Container Images
Use Cosign or Notary to sign images.
# Generate key pair cosign generate-key-pair # Sign image cosign sign --key cosign.key myregistry/myapp:latest # Verify image cosign verify --key cosign.pub myregistry/myapp:latest
Software Bill of Materials (SBOM)
Generate SBOMs for all artifacts.
# Generate SBOM syft myregistry/myapp:latest -o spdx-json > sbom.json # Upload to GitHub - name: Upload SBOM uses: actions/upload-artifact@v4 with: name: sbom path: sbom.json
Part 5: Deployment Security
Use Deployment Environments
jobs: deploy-production: runs-on: ubuntu-latest environment: name: production url: https://example.com
Environment protection:
Required reviewers for production
Wait timer for deployments
Branch restrictions
Require Approval for Production
deploy-production: runs-on: ubuntu-latest environment: production if: github.ref == 'refs/heads/main' steps: - run: ./deploy.sh
Use Signed Commits
on: push: branches: [ main ] jobs: verify-signature: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Verify commit signature run: | git log -1 --format=%G? HEAD | grep -q 'G' || exit 1
SBOM Attestation
Attach SBOMs to releases and deployments for supply chain transparency.
- name: Attest SBOM to release uses: actions/attest-build-provenance@v1 with: subject-path: dist/
Part 6: Monitoring and Auditing
Monitor Pipeline Activities
GitHub Audit Log:
Settings → Security → Audit log Monitor: - Repository creation/deletion - Secret changes - Permission changes - Workflow runs - Deployments
GitLab Audit Events:
Settings → Audit Events Monitor: - User login/logout - Project changes - Pipeline runs - Deployment events
CloudTrail for Cloud Access
Monitor cloud access from pipelines:
aws cloudtrail lookup-events \ --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRole \ --start-time $(date -v-7d +%Y%m%d)
Fail Fast, Alert Fast
- name: Notify on failure if: failure() run: | curl -X POST -H 'Content-type: application/json' \ --data '{"text":"❌ Pipeline failed!"}' \ ${{ secrets.SLACK_WEBHOOK }}
CI/CD Security Checklist
Source Control
Branch protection rules enabled
CODEOWNERS configured for critical files
Secret scanning enabled
Pre-commit hooks configured
Dependabot enabled
Pipeline
Least privilege permissions
OIDC used instead of long-lived secrets
Actions pinned by commit SHA
Dependabot for actions
Security scanning in CI
Secrets
Secrets stored in CI/CD secrets manager
Environment-specific secrets
Secrets rotated regularly
No secrets in code or logs
Artifacts
Container images scanned
Images signed
SBOMs generated
Deployment
Deployment environments configured
Manual approval for production
Rollback procedures documented
Rollback practiced
Monitoring
Audit logging enabled
Alerts for suspicious activity
Failures notified immediately
Common CI/CD Security Mistakes
| Mistake | Why It's Risky | Fix |
|---|---|---|
| Storing secrets in pipeline variables | Exposed in logs, accessible to all jobs | Use environment-specific secrets |
| Running with excessive permissions | Compromise gives attacker full access | Use least privilege |
| Using latest tag in actions | Security updates missed, pinning lost | Pin by commit SHA |
| No approval for production | Anyone can deploy to production | Add manual approval |
| No monitoring | Attacks go undetected | Enable audit logging |
| No SBOM generation | No visibility into dependencies | Generate SBOMs for all artifacts |
Summary
CI/CD security is not optional. It protects your source code, your cloud infrastructure, and your customers.
| Layer | Key Practice |
|---|---|
| Source Control | Branch protection, secret scanning |
| Pipeline | Least privilege, OIDC, pinned actions |
| Secrets | Environment-specific, rotated |
| Artifacts | Scanned, signed, SBOMs |
| Deployment | Environments, approvals, rollbacks |
| Monitoring | Audit logs, alerts |
Remember: Your CI/CD pipeline has access to everything. Secure it like you would secure your production environment.
Learn More
Practice CI/CD security with hands-on exercises in our interactive labs:
https://devops.trainwithsky.com/
Comments
Post a Comment