Courses / CI/CD & GitHub Actions
Lesson 11 of 16
CI/CD Pipeline Security Best Practices
Harden your CI/CD pipelines with secret scanning, SAST integration, dependency audits, and supply chain security measures.
What you'll learn
- ✓How to integrate secret scanning into your CI pipeline
- ✓Running SAST tools automatically on every pull request
- ✓Auditing dependencies for known vulnerabilities
- ✓Protecting your software supply chain with SBOM and signing
- ✓Applying least-privilege principles to CI/CD runners and tokens
Prerequisites
- •Basic CI/CD pipeline experience
- •Familiarity with GitHub Actions or similar CI system
A compromised CI/CD pipeline is one of the most damaging attack vectors in modern software development. An attacker who gains access to your pipeline can inject malicious code into every build, steal secrets, and push compromised artifacts to production. Securing your pipeline is not optional; it is as critical as securing your application code.
Secret Scanning in the Pipeline
Secrets accidentally committed to source control are the most common CI/CD security failure. Catching them before they reach a remote branch prevents credential exposure.
Using Gitleaks
Gitleaks scans git history and staged changes for patterns matching API keys, tokens, and passwords:
# .github/workflows/security.yml
name: Security Scan
on:
pull_request:
push:
branches: [main]
jobs:
secret-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
For more control, run Gitleaks directly and configure custom rules:
# .gitleaks.toml
title = "Custom Gitleaks Config"
[[rules]]
id = "internal-api-key"
description = "Internal API key pattern"
regex = '''INTERNAL_KEY_[A-Za-z0-9]{32}'''
tags = ["key", "internal"]
[allowlist]
paths = [
'''\.test\.ts$''',
'''__fixtures__''',
]
Using TruffleHog
TruffleHog goes further by verifying whether detected secrets are actually live:
secret-scan-trufflehog:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: trufflesecurity/trufflehog@main
with:
extra_args: --only-verified
Static Application Security Testing (SAST)
SAST tools analyze source code for vulnerabilities without executing it. Integrating SAST into your pull request workflow catches security issues before code review.
CodeQL for GitHub Repositories
GitHub’s CodeQL is free for public repositories and provides deep semantic analysis:
codeql-analysis:
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: github/codeql-action/init@v3
with:
languages: javascript, python
queries: +security-extended
- uses: github/codeql-action/autobuild@v3
- uses: github/codeql-action/analyze@v3
The security-extended query suite catches more issues than the default, including data flow analysis for injection vulnerabilities.
Semgrep for Multi-Language Scanning
Semgrep is lightweight, fast, and supports custom rules:
semgrep:
runs-on: ubuntu-latest
container:
image: semgrep/semgrep
steps:
- uses: actions/checkout@v4
- run: semgrep scan --config auto --config p/security-audit --sarif -o results.sarif
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
if: always()
Writing custom Semgrep rules for your codebase catches organization-specific antipatterns:
# .semgrep/no-eval.yml
rules:
- id: no-eval-usage
patterns:
- pattern: eval(...)
message: "eval() is banned. Use JSON.parse() or a safe parser instead."
languages: [javascript, typescript]
severity: ERROR
Dependency Vulnerability Scanning
Third-party dependencies are the largest attack surface for most applications. Automated scanning catches known CVEs before they reach production.
GitHub Dependabot
Enable Dependabot alerts and automatic security updates:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 10
labels:
- "dependencies"
- "security"
groups:
dev-dependencies:
dependency-type: "development"
update-types: ["minor", "patch"]
npm audit in CI
Run npm audit as a pipeline step that fails on high-severity vulnerabilities:
dependency-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm ci
- run: npm audit --audit-level=high
For Python projects, use pip-audit:
- run: pip install pip-audit
- run: pip-audit --strict --desc
Trivy for Container Scanning
If you build Docker images, scan them for OS and application vulnerabilities:
container-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -t myapp:${{ github.sha }} .
- uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1'
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-results.sarif
if: always()
Software Supply Chain Security
Supply chain attacks target the tools and dependencies you use to build software, not the software itself.
Generating an SBOM
A Software Bill of Materials (SBOM) lists every component in your build. Generate one automatically:
generate-sbom:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: anchore/sbom-action@v0
with:
format: spdx-json
output-file: sbom.spdx.json
- uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.spdx.json
Signing Artifacts with Cosign
Sign your container images so consumers can verify they were built by your pipeline:
sign-image:
runs-on: ubuntu-latest
permissions:
id-token: write
packages: write
steps:
- uses: sigstore/cosign-installer@v3
- run: cosign sign --yes ghcr.io/your-org/myapp@${{ steps.build.outputs.digest }}
Cosign’s keyless signing uses OIDC identity from the GitHub Actions runner, so no private keys need to be stored.
Pinning Action Versions by SHA
Never use mutable tags for third-party actions in security-sensitive workflows:
# Insecure: tag can be moved to point to malicious code
- uses: actions/checkout@v4
# Secure: SHA is immutable
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
Use Renovate or Dependabot to automate SHA updates when new versions are released.
Least-Privilege Pipeline Permissions
GitHub Actions workflows default to broad permissions. Restrict them explicitly:
# At the workflow level, set minimal defaults
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
id-token: write
steps:
- uses: actions/checkout@v4
# ... deployment steps
Token Scoping
The GITHUB_TOKEN has configurable permissions. For self-hosted runners, also consider:
- Short-lived tokens. Use OIDC to request cloud provider credentials that expire in minutes, not stored long-lived access keys.
- Environment-scoped secrets. Restrict production secrets to the
productionenvironment with required reviewers. - No write access for PRs from forks. GitHub restricts this by default; do not override it.
deploy-prod:
runs-on: ubuntu-latest
environment:
name: production
url: https://myapp.example.com
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/deploy-role
aws-region: us-east-1
# Short-lived credentials, no stored keys
Branch Protection and Required Checks
Security scans are useless if developers can bypass them. Enforce them through branch protection:
- Require status checks to pass before merging. Mark your secret-scan, SAST, and dependency-audit jobs as required.
- Require pull request reviews. At least one approval from a code owner.
- Dismiss stale reviews when new commits are pushed.
- Restrict who can push to
maindirectly.
Configure this in repository settings or via the GitHub API:
gh api repos/your-org/your-repo/branches/main/protection \
--method PUT \
--field required_status_checks='{"strict":true,"contexts":["secret-scan","codeql-analysis","dependency-audit"]}' \
--field enforce_admins=true \
--field required_pull_request_reviews='{"required_approving_review_count":1,"dismiss_stale_reviews":true}'
A Complete Security Pipeline
Here is a full workflow combining all the techniques:
name: Security Pipeline
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
security-events: write
jobs:
secrets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: github/codeql-action/init@v3
with:
languages: javascript
- uses: github/codeql-action/autobuild@v3
- uses: github/codeql-action/analyze@v3
dependencies:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm ci
- run: npm audit --audit-level=high
container:
runs-on: ubuntu-latest
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- run: docker build -t myapp:${{ github.sha }} .
- uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
severity: 'CRITICAL,HIGH'
exit-code: '1'
Summary
Pipeline security is a layered defense. Secret scanning prevents credential leaks, SAST catches code-level vulnerabilities, dependency auditing addresses known CVEs, and supply chain measures ensure your build tools and artifacts are trustworthy. Apply least-privilege permissions, pin action versions by SHA, and enforce security checks through branch protection rules. No single tool solves everything; the combination of all these layers is what makes a pipeline secure.
Progress is saved locally to your browser.