The 10 most critical security risks in CI/CD pipelines and how to mitigate them.
The OWASP Top 10 CI/CD Security Risks identifies the most significant security risks in Continuous Integration and Continuous Delivery pipelines. CI/CD pipelines are high-value targets because they have direct access to source code, secrets, and production environments. This guide covers flow control, identity management, dependency attacks, pipeline poisoning, and more.
Insufficient flow control mechanisms allow attackers to push malicious code through CI/CD pipelines without proper review or approval gates. This includes bypassing branch protection rules, missing required approvals, and lack of enforcement on who can trigger deployments to production.
Without proper flow control, a single compromised developer account or malicious insider can push code directly to production, bypassing code review, security scans, and approval processes. This can lead to deployment of backdoors, data exfiltration code, or destructive payloads.
# No branch protection — anyone can push directly to main name: Deploy on: push: branches: [main] # Triggers on any push to main jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: | # Deploys immediately — no approval gate! kubectl apply -f deploy.yaml kubectl rollout status deployment/myapp
name: Deploy on: push: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm test deploy: needs: test runs-on: ubuntu-latest environment: production # Requires manual approval steps: - uses: actions/checkout@v4 - run: | kubectl apply -f deploy.yaml kubectl rollout status deployment/myapp
CI/CD systems involve multiple identities: human users, service accounts, bot tokens, and machine identities. Inadequate IAM allows over-permissive access to repositories, pipelines, and deployment targets. Shared credentials, stale accounts, and lack of MFA compound the risk.
Compromised CI/CD identities with excessive permissions can modify pipelines, access secrets, alter build artifacts, and deploy to production. Shared service accounts make it impossible to audit who performed an action.
# Over-permissive workflow with admin token name: CI on: push permissions: write-all # Full permissions to everything! jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: | # Using org-wide PAT with admin access curl -H "Authorization: token ${{ secrets.ADMIN_PAT }}" \ https://api.github.com/repos/org/other-repo/contents/
name: CI on: push permissions: contents: read # Minimum required permissions packages: write # Only what's needed jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/create-github-app-token@v1 # Scoped app token id: app-token with: app-id: ${{ vars.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} repositories: other-repo # Scoped to specific repo
CI/CD pipelines pull dependencies from external package registries (npm, PyPI, Maven, Docker Hub). Attackers exploit this through dependency confusion, typosquatting, compromised maintainer accounts, and malicious packages. A single poisoned dependency can execute arbitrary code during build.
Malicious dependencies execute code during installation (postinstall scripts, setup.py), stealing secrets, injecting backdoors into build artifacts, or establishing persistence. Supply chain attacks like SolarWinds and Codecov demonstrate the catastrophic impact.
pipeline { agent any stages { stage('Build') { steps { // No lockfile verification, no integrity checks sh 'npm install' // Fetches latest — could be compromised! sh 'pip install -r requirements.txt' // No hash verification } } } }
pipeline { agent any stages { stage('Build') { steps { // Use lockfile with integrity verification sh 'npm ci' // Uses package-lock.json sh 'npm audit --audit-level=high' // Python: verify hashes from lockfile sh 'pip install --require-hashes -r requirements.lock' } } stage('SCA Scan') { steps { // Software Composition Analysis sh 'trivy fs --scanners vuln,secret .' } } } }
Poisoned Pipeline Execution (PPE) occurs when attackers can modify CI/CD pipeline definitions or inject malicious code that executes within the pipeline context. This can happen through manipulating pipeline configuration files in branches, pull requests from forks, or modifying shared pipeline templates.
An attacker who can modify pipeline configuration gains access to all secrets, credentials, and permissions available to the pipeline. They can exfiltrate secrets, tamper with build outputs, or deploy malicious code — all within a trusted execution context.
# Runs pipeline from fork PRs with access to secrets name: CI on: pull_request_target: # Runs in base repo context with secrets! branches: [main] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} # Checks out fork code! - run: make build # Fork's Makefile executes with secrets access env: DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
# Separate workflows: untrusted build + trusted deploy name: CI on: pull_request: # No secret access for PR builds branches: [main] permissions: contents: read jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # Checks out merge commit (safe) - run: npm ci && npm test # No secrets needed for build/test - uses: actions/upload-artifact@v4 with: name: build-output path: dist/
Pipeline-Based Access Controls (PBAC) govern what resources a pipeline can access: cloud accounts, Kubernetes clusters, databases, and internal services. Insufficient PBAC means pipelines have broader access than needed, violating the principle of least privilege.
Over-privileged pipelines can be exploited to access resources far beyond their intended scope. A compromised build pipeline for a minor service could be used to access production databases, modify infrastructure, or pivot to other environments.
# Pipeline with admin-level cloud credentials deploy: stage: deploy script: - aws configure set aws_access_key_id $AWS_ACCESS_KEY - aws configure set aws_secret_access_key $AWS_SECRET_KEY # This key has AdministratorAccess policy! - aws s3 sync dist/ s3://my-bucket/ - aws ecs update-service --cluster prod --service myapp # Same credentials could access ANY AWS resource
name: Deploy on: push: branches: [main] permissions: id-token: write # For OIDC contents: read jobs: deploy: runs-on: ubuntu-latest steps: - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456:role/deploy-s3-only # Scoped role: only s3:PutObject on specific bucket aws-region: us-east-1 - run: aws s3 sync dist/ s3://my-bucket/
CI/CD pipelines handle numerous credentials: API keys, cloud tokens, registry passwords, SSH keys, and signing certificates. Poor credential hygiene includes hardcoding secrets in pipeline files, printing secrets in logs, using unencrypted secret storage, and never rotating credentials.
Leaked CI/CD credentials are one of the most common initial access vectors. Secrets exposed in build logs, committed to repositories, or stored without encryption provide attackers with direct access to production systems, cloud accounts, and artifact registries.
#!/bin/bash # Secrets hardcoded and leaked in logs export DOCKER_PASSWORD="MyS3cret!" # Hardcoded! echo "Logging in with $DOCKER_PASSWORD" # Printed to logs! docker login -u admin -p "$DOCKER_PASSWORD" registry.example.com docker push registry.example.com/myapp:latest # AWS credentials in environment — visible in process listing export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCY" aws s3 cp artifact.zip s3://releases/
name: Publish on: push: tags: ['v*'] jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: docker/login-action@v3 # Handles credentials securely with: registry: registry.example.com username: ${{ secrets.DOCKER_USER }} # Masked in logs password: ${{ secrets.DOCKER_TOKEN }} # Short-lived token - run: | docker build -t registry.example.com/myapp:${{ github.ref_name }} . docker push registry.example.com/myapp:${{ github.ref_name }}
CI/CD systems (Jenkins, GitLab, GitHub Actions runners) often run with insecure default configurations. This includes outdated software versions, exposed management interfaces, disabled security features, overly permissive network access, and self-hosted runners shared across projects.
Misconfigured CI/CD infrastructure can be exploited to gain unauthorized access to build environments, intercept secrets, or pivot to internal networks. Shared self-hosted runners allow cross-project attacks where one compromised workflow affects others.
// Jenkins with insecure configuration // - Script console enabled without auth // - Agent-to-controller access unrestricted // - Outdated plugins with known CVEs pipeline { agent any // Runs on any available agent — no isolation stages { stage('Build') { steps { // Running as root on shared agent sh 'whoami' // root sh 'docker build -t myapp .' } } } }
pipeline { agent { kubernetes { // Ephemeral, isolated pod per build yaml """ apiVersion: v1 kind: Pod spec: securityContext: runAsNonRoot: true runAsUser: 1000 containers: - name: builder image: builder:1.2.3 securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true """ } } stages { stage('Build') { steps { container('builder') { sh 'make build' } } } } }
CI/CD pipelines often integrate with third-party services: code quality tools, security scanners, notification systems, and deployment platforms. These integrations are granted access tokens and permissions, creating a trust chain. Ungoverned usage means no visibility into what third-party services can access or do.
A compromised third-party service (like the Codecov breach) can access source code, secrets, and build artifacts. Without governance, teams may grant excessive permissions to unknown services, creating invisible attack vectors in the supply chain.
name: CI on: push jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm test # Unknown third-party action with full repo access - uses: random-org/code-analysis@main # Unpinned, unvetted! with: token: ${{ secrets.GITHUB_TOKEN }} # Full token access! # Uploading coverage to external service with repo token - run: | bash <(curl -s https://example.com/uploader.sh) # Remote script!
name: CI on: push permissions: contents: read jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm test # Vetted action pinned to SHA - uses: github/codeql-action/analyze@8a470fddafa5cbc14 # Pinned SHA # Upload via official CLI tool, not remote scripts - uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 with: token: ${{ secrets.CODECOV_TOKEN }} # Scoped token fail_ci_if_error: true
Build artifacts (container images, binaries, packages) flow through CI/CD pipelines to production. Without integrity validation, artifacts can be tampered with at any point: during build, in transit, in the artifact registry, or at deployment time. This breaks the trust chain from code to production.
Tampered artifacts can contain backdoors, malware, or modified logic. Without signing and verification, there is no way to detect if an artifact was modified after build. Attackers can replace legitimate images in registries or intercept artifacts in transit.
#!/bin/bash # Build and deploy without any integrity checks docker build -t myregistry.com/app:latest . docker push myregistry.com/app:latest # On deployment side — no verification docker pull myregistry.com/app:latest # Could be tampered! docker run myregistry.com/app:latest # Tag is mutable!
name: Build and Sign on: push: tags: ['v*'] permissions: id-token: write packages: write jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: | # Build with immutable tag (SHA) docker build -t myregistry.com/app:${{ github.sha }} . docker push myregistry.com/app:${{ github.sha }} - uses: sigstore/cosign-installer@v3 - run: | # Sign image with keyless signing (Sigstore) cosign sign myregistry.com/app:${{ github.sha }} # Generate and attach SBOM syft myregistry.com/app:${{ github.sha }} -o spdx-json > sbom.json cosign attest --predicate sbom.json myregistry.com/app:${{ github.sha }}
CI/CD environments generate critical security events: pipeline executions, configuration changes, secret access, and deployment activities. Without comprehensive logging and monitoring, malicious activities in the pipeline go undetected, and incident response is severely hampered.
Without visibility into CI/CD activities, attackers can modify pipelines, exfiltrate secrets, and tamper with artifacts without triggering any alerts. The lack of audit trails makes it impossible to determine the scope and impact of a breach.
#!/bin/bash # Pipeline with no logging or audit trail echo "Starting deployment..." kubectl apply -f deploy.yaml echo "Done." # No record of: who triggered this, what changed, # which image was deployed, what secrets were accessed # Build logs expire after 30 days with no archival
name: Audited Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Log deployment metadata run: | echo "=== Deployment Audit Log ===" echo "Triggered by: ${{ github.actor }}" echo "Commit: ${{ github.sha }}" echo "Ref: ${{ github.ref }}" echo "Workflow: ${{ github.workflow }}" echo "Run ID: ${{ github.run_id }}" - name: Deploy with audit run: | kubectl apply -f deploy.yaml 2>&1 | tee deploy.log # Send audit event to SIEM curl -X POST "${{ secrets.SIEM_WEBHOOK }}" \ -d '{"event":"deploy","actor":"${{ github.actor }}","sha":"${{ github.sha }}"}'
| ID | Vulnerability | Severity | Key Mitigation |
|---|---|---|---|
| CICD-SEC-1 | Insufficient Flow Control Mechanisms | Critical | Branch protection, required approvals, environment gates |
| CICD-SEC-2 | Inadequate Identity and Access Management | Critical | Least-privilege tokens, OIDC, MFA, credential rotation |
| CICD-SEC-3 | Dependency Chain Abuse | Critical | Lockfiles, hash verification, private registries, SCA |
| CICD-SEC-4 | Poisoned Pipeline Execution (PPE) | Critical | Separate build/deploy, immutable pipeline defs, fork controls |
| CICD-SEC-5 | Insufficient PBAC | High | OIDC federation, scoped IAM roles, environment separation |
| CICD-SEC-6 | Insufficient Credential Hygiene | Critical | Secret management, log masking, rotation, secret scanning |
| CICD-SEC-7 | Insecure System Configuration | High | Ephemeral runners, non-root, plugin updates, network isolation |
| CICD-SEC-8 | Ungoverned Usage of 3rd Party Services | High | Service inventory, SHA pinning, no remote scripts |
| CICD-SEC-9 | Improper Artifact Integrity Validation | High | Cosign signing, admission control, SLSA provenance |
| CICD-SEC-10 | Insufficient Logging and Visibility | Medium | Audit logging, SIEM integration, alerting, log retention |