CI/CD Pipeline Security Best Practices 2026: A DevSecOps Guide
Secure your CI/CD pipeline against modern threats. Covers secrets management, SAST/DAST integration, supply chain security with SBOM and Sigstore, OIDC auth, least-privilege runners, artifact signing, and policy-as-code with OPA and Kyverno.
Most CI/CD pipeline breaches don’t start with a sophisticated zero-day. They start with a leaked secret in a log file, an overprivileged service account, or a malicious package pulled in through a transitive dependency. The SolarWinds attack injected malicious code into a build pipeline. The Codecov breach exfiltrated environment variables including AWS credentials. The npm event-stream incident shipped a backdoor to millions of projects via a dependency update.
Pipeline security is no longer optional. This guide covers the practical controls that matter—what to implement, why, and how.
Why CI/CD Pipelines Are High-Value Targets
CI/CD pipelines have elevated access by design:
- They clone your source code
- They have credentials to push container images and deploy to production
- They run arbitrary code from your repository (and often from dependencies)
- They frequently have broad cloud permissions to “just make it work”
An attacker who compromises your pipeline often gets more access than one who compromises a developer’s laptop. This makes pipeline security worth treating with the same rigor as production infrastructure.
1. Secrets Management
Never Store Secrets in Source Code or Environment Variables
Environment variables passed to build jobs are the most common source of credential leaks. Logs, error output, and debug dumps all risk exposing them.
What to use instead:
- HashiCorp Vault with dynamic secrets: Generate short-lived credentials per pipeline run
- AWS Secrets Manager / GCP Secret Manager / Azure Key Vault: Fetch secrets at runtime with IAM-scoped access
- GitHub Actions Encrypted Secrets / GitLab CI Variables (masked): Acceptable for low-sensitivity tokens, but prefer external secret managers for production credentials
Enforce secret scanning in CI:
# .github/workflows/secret-scan.yml
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Rotation policy: Any secret that can’t be rotated in under 5 minutes is a liability. Structure your secret management so rotation is automated or at least fast.
Use Short-Lived Credentials
Long-lived credentials are ticking time bombs. Every credential that persists is an additional attack surface. Modern pipelines should prefer credentials that expire within the duration of a single job.
This is exactly what OIDC solves—covered in section 4.
2. SAST and DAST Integration
Static Application Security Testing (SAST)
SAST analyzes source code without running it. Run it on every pull request, fail the build on high-severity findings.
Tools by language:
| Language | Tool | Notes |
|---|---|---|
| Multiple | Semgrep | Highly configurable, community rules |
| Java/Kotlin | SpotBugs + Find-Sec-Bugs | Deep Java analysis |
| JavaScript/TypeScript | ESLint security plugin, NodeJSScan | Fast, integrates with existing lint |
| Python | Bandit, Semgrep | Catches common Python security patterns |
| Go | Gosec | Go-specific security checks |
| Infrastructure | tfsec, Checkov | Terraform/Kubernetes/CloudFormation |
Sample GitHub Actions integration:
- name: SAST - Semgrep
uses: returntocorp/semgrep-action@v1
with:
config: >-
p/ci
p/owasp-top-ten
p/secrets
Triage strategy: Don’t suppress SAST findings by default. Create a semgrep.yml ignore file for confirmed false positives, with a comment explaining why. Every suppression should be a documented decision, not a reflex.
Dynamic Application Security Testing (DAST)
DAST tests a running application by sending attack payloads. Run it in staging/preview environments, not production.
OWASP ZAP in CI:
- name: DAST - ZAP Baseline Scan
uses: zaproxy/[email protected]
with:
target: 'https://staging.yourapp.com'
rules_file_name: '.zap/rules.tsv'
cmd_options: '-a'
Scope DAST carefully: DAST can generate noise in logs, trigger rate limits, and create test data. Use a dedicated staging environment with synthetic data. Never run aggressive DAST against production.
Container Image Scanning
Every container image should be scanned before pushing to a registry:
- name: Scan container image
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'HIGH,CRITICAL'
exit-code: '1'
Upload SARIF output to GitHub Security tab for centralized vulnerability tracking.
3. Supply Chain Security: SBOM and Sigstore
Generate SBOMs for Every Build
A Software Bill of Materials is your dependency inventory. Without it, you can’t answer “are we affected by CVE-2026-XXXX?” in under an hour.
Generate with Syft:
# Generate SBOM in CycloneDX format
syft myapp:latest -o cyclonedx-json > sbom.json
# Or SPDX format (required by some compliance frameworks)
syft myapp:latest -o spdx-json > sbom.spdx.json
In CI:
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: myapp:${{ github.sha }}
format: cyclonedx-json
output-file: sbom.cyclonedx.json
- name: Upload SBOM
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.cyclonedx.json
Scan SBOM for vulnerabilities with Grype:
- name: Vulnerability scan from SBOM
run: |
grype sbom:./sbom.cyclonedx.json --fail-on high
Publish SBOMs: Attach SBOMs to GitHub Releases. Customers and compliance teams increasingly require them.
Verify Dependencies Before Trusting Them
Lock your dependency files and verify checksums:
# npm - generate and commit lockfile
npm ci # Uses package-lock.json, fails if it's out of sync
# pip - use hashes
pip install --require-hashes -r requirements.txt
# Go modules - use module verification
GONOSUMCHECK="" go mod verify
Enable Dependabot or Renovate Bot for automated dependency updates with PR-based review. Don’t let dependencies go stale—old dependencies accumulate known vulnerabilities.
Artifact Signing with Sigstore/Cosign
Sigstore provides keyless artifact signing using your CI OIDC identity. No private key management required.
Sign a container image in GitHub Actions:
- name: Install Cosign
uses: sigstore/cosign-installer@v3
- name: Sign the image
run: |
cosign sign --yes myregistry.io/myapp:${{ github.sha }}
env:
COSIGN_EXPERIMENTAL: "1"
Verify before deployment:
cosign verify \
--certificate-identity "https://github.com/myorg/myrepo/.github/workflows/release.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
myregistry.io/myapp:sha256-abc123
Sign SBOMs too:
cosign attest --yes \
--predicate sbom.cyclonedx.json \
--type cyclonedx \
myregistry.io/myapp:${{ github.sha }}
This creates a verifiable chain: build → SBOM → signature → transparency log. Anyone can verify what went into your image and that it came from your pipeline.
4. OIDC Authentication for Runners
Static credentials (long-lived AWS access keys, service account JSON files) stored in CI secrets are a systemic risk. OIDC eliminates them.
How OIDC Works in CI/CD
- Your CI job runs and requests an OIDC token from the platform
- The token contains claims about the job (repo, branch, workflow name)
- Your cloud provider (AWS/GCP/Azure) verifies the token and issues short-lived credentials
- Credentials expire when the job ends
No credentials to rotate. No credentials to leak. No credentials to store.
GitHub Actions + AWS (AssumeRoleWithWebIdentity)
AWS IAM Trust Policy:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:myorg/myrepo:ref:refs/heads/main"
}
}
}]
}
In your workflow:
permissions:
id-token: write
contents: read
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/github-actions-deploy
aws-region: us-east-1
No AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY stored anywhere. The role exists only for the duration of the job.
Scope the Trust Condition Tightly
The trust condition is your access control. A broad condition like repo:myorg/* allows any repo in your org to assume the role. Scope to:
- Specific repo + specific branch for production deployments
- Specific repo + any branch for lower environments
- Specific workflow file path for extra restriction
5. Least-Privilege Service Accounts
Every service account in your pipeline should have exactly the permissions it needs and nothing more.
Audit Existing Permissions
Run this regularly against your cloud accounts:
# AWS: find overprivileged roles
aws iam get-account-authorization-details \
--query 'RoleDetailList[?contains(AttachedManagedPolicies[].PolicyName, `AdministratorAccess`)]'
# GCP: list service accounts with primitive roles
gcloud projects get-iam-policy PROJECT_ID \
--format=json | jq '.bindings[] | select(.role | startswith("roles/editor", "roles/owner", "roles/viewer"))'
Principle of Least Privilege in Practice
Bad: Give the CI runner an IAM role with s3:* on *.
Good:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::myapp-artifacts/*"
}]
}
Break pipelines into stages with separate service accounts:
- Build stage: Read source, write artifacts to build bucket
- Test stage: Read artifacts, read test fixtures (no write to production)
- Deploy stage: Read artifacts, write to deployment targets (no access to source)
Kubernetes: Restrict Runner Pods
If runners run in Kubernetes, use a dedicated namespace with tight RBAC:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: ci-runners
name: ci-runner-role
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
# No cluster-admin, no secrets access, no cross-namespace access
Disable token automounting for runner pods unless explicitly needed:
spec:
automountServiceAccountToken: false
6. Policy-as-Code with OPA and Kyverno
Manual security reviews don’t scale. Policy-as-code enforces security rules automatically and consistently.
Open Policy Agent (OPA)
OPA uses the Rego language to define policies that evaluate any structured data (JSON/YAML). Use it to gate deployments based on security requirements.
Example: Require signed images before deployment:
package deployment.security
deny[msg] {
input.kind == "Deployment"
container := input.spec.template.spec.containers[_]
not is_signed(container.image)
msg := sprintf("Container image %v must be signed with Cosign", [container.image])
}
is_signed(image) {
# Check Cosign signature via OPA external data or pre-validation step
data.signed_images[image]
}
Integrate with Conftest:
# Validate Kubernetes manifests before applying
conftest test deployment.yaml --policy policies/
Kyverno: Kubernetes-Native Policies
Kyverno runs as an admission webhook and enforces policies on every Kubernetes resource creation/update.
Require non-root containers:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-non-root
spec:
validationFailureAction: Enforce
rules:
- name: check-runAsNonRoot
match:
any:
- resources:
kinds: [Pod]
validate:
message: "Containers must not run as root."
pattern:
spec:
containers:
- =(securityContext):
=(runAsNonRoot): true
Require read-only root filesystem:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-readonly-rootfs
spec:
validationFailureAction: Enforce
rules:
- name: check-readonly-rootfs
match:
any:
- resources:
kinds: [Pod]
validate:
message: "Root filesystem must be read-only."
pattern:
spec:
containers:
- securityContext:
readOnlyRootFilesystem: true
Shift Left: Run Policies in CI
Don’t wait for the admission webhook. Run the same policies in CI so developers get fast feedback:
- name: Validate Kubernetes manifests
run: |
kyverno apply policies/ --resource manifests/
This catches policy violations before code is merged, not after it’s deployed.
7. Monitoring and Incident Response
Security controls are only useful if someone reviews the results.
Centralize Pipeline Logs
Ship CI/CD logs to your SIEM (Splunk, Datadog, OpenSearch). Alert on:
- Failed authentication attempts
- Unexpected secret access patterns
- New processes spawned during builds (potential build injection)
- Outbound network connections to unknown destinations from build agents
Build Provenance with SLSA
SLSA (Supply chain Levels for Software Artifacts) is a framework for grading your build integrity. At SLSA Level 2+, builds are run in a hosted CI platform, and provenance is automatically generated.
Use the SLSA GitHub Generator:
- name: Generate SLSA provenance
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1
with:
base64-subjects: "${{ needs.build.outputs.hashes }}"
This produces a signed provenance attestation that anyone can verify—proving your artifact came from a specific workflow run on a specific commit.
Regular Audit of Pipeline Configuration
Treat your pipeline configuration files (.github/workflows/*.yml, .gitlab-ci.yml, Jenkinsfile) with the same rigor as production infrastructure code:
- Require pull request reviews for workflow changes
- Use branch protection rules that prevent force-pushing to
main - Audit third-party actions: pin to commit SHA, not a mutable tag
# BAD: mutable tag, can be changed by the action author
- uses: actions/checkout@v4
# GOOD: pinned to a specific commit SHA
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
Putting It Together: A Secure Pipeline Checklist
Here’s the minimum viable security posture for a production CI/CD pipeline in 2026:
Secrets:
- No secrets in source code (Gitleaks in CI)
- Short-lived credentials via OIDC (not static keys)
- Secrets managed in Vault or cloud secret manager
Static analysis:
- SAST on every PR (Semgrep or language-specific tool)
- Container image scanning (Trivy)
- Dependency vulnerability scanning (Grype or Snyk)
- IaC scanning (tfsec or Checkov)
Supply chain:
- Lockfiles committed and verified in CI
- SBOM generated and attached to releases
- Artifacts signed with Cosign (keyless)
- Third-party actions pinned to SHA
Access control:
- OIDC for all cloud authentication
- Least-privilege roles per pipeline stage
- No cluster-admin or editor/owner roles for runners
Policy enforcement:
- OPA/Conftest validates manifests in CI
- Kyverno enforces runtime policies in Kubernetes
- SLSA provenance generated for releases
Monitoring:
- Pipeline logs in SIEM
- Alerts for anomalous build behavior
- Regular audit of workflow file changes
Where to Start
If your pipeline currently has none of these controls, start here:
- Week 1: Add Gitleaks for secret scanning. Add Trivy for image scanning. Both are fast to integrate and catch high-impact issues immediately.
- Week 2: Switch to OIDC authentication for your primary cloud. This eliminates your biggest credential exposure with one change.
- Week 3: Add Semgrep SAST and Grype SBOM scanning. Configure fail-on-high-severity.
- Month 2: Implement policy-as-code. Start with Kyverno for Kubernetes, Conftest for manifest validation in CI.
- Ongoing: Audit permissions, rotate any remaining static credentials, integrate Sigstore artifact signing for releases.
Pipeline security compounds. Each control makes the next one more effective. A signed SBOM is meaningless without a verified signing chain. OIDC is more valuable when combined with least-privilege roles. Start with the basics, build toward the full picture.
Related Tools
- Trivy — Container and filesystem vulnerability scanner
- Cosign / Sigstore — Keyless artifact signing
- Syft — SBOM generator
- Grype — Vulnerability scanner for SBOMs
- Semgrep — SAST with community rules
- Gitleaks — Secret scanner
- OPA / Conftest — Policy-as-code testing
- Kyverno — Kubernetes-native policy engine
- SLSA Framework — Supply chain security framework
50 CLI Commands That Save Hours Every Week
The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.
- Git mastery (10 cmds)
- File & search wizardry
- Docker & DevOps tricks
- Process & network tools
- Productivity boosters
- macOS & Linux install guide
No spam. 1 email/week max. Unsubscribe anytime.
Found this guide useful? Check out our free developer tools.
Affiliate disclosure: Some links below are affiliate links — we may earn a small commission at no extra cost to you. Learn more.
Recommended Tools & Resources
DigitalOcean
$200 credit for new users. Simple, affordable cloud hosting for developers.
GitHub Student Pack
Free access to 100+ developer tools. Perfect for students and new devs.
Vercel
Deploy frontend apps instantly. Free tier is generous for side projects.
DevPlaybook Products
Boilerplates, scripts & AI toolkits to 10x your dev workflow.