What are the CI/CD Security Top 10 Risks?

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.

1️⃣ CICD-SEC-1 - Insufficient Flow Control Mechanisms

Critical

Overview

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.

Risk

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.

Vulnerable Code Example

YAML (GitHub Actions) ❌ Bad
# 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

Secure Code Example

YAML (GitHub Actions) ✅ Good
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

Mitigation Checklist

2️⃣ CICD-SEC-2 - Inadequate Identity and Access Management

Critical

Overview

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.

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.

Vulnerable Code Example

YAML (GitHub Actions) ❌ Bad
# 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/

Secure Code Example

YAML (GitHub Actions) ✅ Good
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

Mitigation Checklist

3️⃣ CICD-SEC-3 - Dependency Chain Abuse

Critical

Overview

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.

Risk

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.

Vulnerable Code Example

Jenkinsfile ❌ Bad
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
      }
    }
  }
}

Secure Code Example

Jenkinsfile ✅ Good
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 .'
      }
    }
  }
}

Mitigation Checklist

4️⃣ CICD-SEC-4 - Poisoned Pipeline Execution (PPE)

Critical

Overview

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.

Risk

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.

Vulnerable Code Example

YAML (GitHub Actions) ❌ Bad
# 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 }}

Secure Code Example

YAML (GitHub Actions) ✅ Good
# 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/

Mitigation Checklist

5️⃣ CICD-SEC-5 - Insufficient Pipeline-Based Access Controls (PBAC)

High

Overview

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.

Risk

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.

Vulnerable Code Example

YAML (GitLab CI) ❌ Bad
# 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

Secure Code Example

YAML (GitHub Actions) ✅ Good
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/

Mitigation Checklist

6️⃣ CICD-SEC-6 - Insufficient Credential Hygiene

Critical

Overview

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.

Risk

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.

Vulnerable Code Example

Bash (CI Script) ❌ Bad
#!/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/

Secure Code Example

YAML (GitHub Actions) ✅ Good
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 }}

Mitigation Checklist

7️⃣ CICD-SEC-7 - Insecure System Configuration

High

Overview

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.

Risk

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.

Vulnerable Code Example

Groovy (Jenkins) ❌ Bad
// 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 .'
      }
    }
  }
}

Secure Code Example

Groovy (Jenkins) ✅ Good
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'
        }
      }
    }
  }
}

Mitigation Checklist

8️⃣ CICD-SEC-8 - Ungoverned Usage of 3rd Party Services

High

Overview

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.

Risk

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.

Vulnerable Code Example

YAML (GitHub Actions) ❌ Bad
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!

Secure Code Example

YAML (GitHub Actions) ✅ Good
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

Mitigation Checklist

9️⃣ CICD-SEC-9 - Improper Artifact Integrity Validation

High

Overview

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.

Risk

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.

Vulnerable Code Example

Bash ❌ Bad
#!/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!

Secure Code Example

YAML (GitHub Actions) ✅ Good
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 }}

Mitigation Checklist

🔟 CICD-SEC-10 - Insufficient Logging and Visibility

Medium

Overview

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.

Risk

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.

Vulnerable Code Example

Bash ❌ Bad
#!/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

Secure Code Example

YAML (GitHub Actions) ✅ Good
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 }}"}'

Mitigation Checklist

📊 Summary Table

ID Vulnerability Severity Key Mitigation
CICD-SEC-1Insufficient Flow Control MechanismsCriticalBranch protection, required approvals, environment gates
CICD-SEC-2Inadequate Identity and Access ManagementCriticalLeast-privilege tokens, OIDC, MFA, credential rotation
CICD-SEC-3Dependency Chain AbuseCriticalLockfiles, hash verification, private registries, SCA
CICD-SEC-4Poisoned Pipeline Execution (PPE)CriticalSeparate build/deploy, immutable pipeline defs, fork controls
CICD-SEC-5Insufficient PBACHighOIDC federation, scoped IAM roles, environment separation
CICD-SEC-6Insufficient Credential HygieneCriticalSecret management, log masking, rotation, secret scanning
CICD-SEC-7Insecure System ConfigurationHighEphemeral runners, non-root, plugin updates, network isolation
CICD-SEC-8Ungoverned Usage of 3rd Party ServicesHighService inventory, SHA pinning, no remote scripts
CICD-SEC-9Improper Artifact Integrity ValidationHighCosign signing, admission control, SLSA provenance
CICD-SEC-10Insufficient Logging and VisibilityMediumAudit logging, SIEM integration, alerting, log retention