CI/CD Pipeline Security Testing: Securing Your Build and Deployment Chain

The CI/CD pipeline is the most privileged part of most engineering organizations. It has production deployment credentials, database access, signing keys, and access to every external service the application uses. It runs code automatically, triggered by events that can often be influenced by outsiders through pull requests and forks.

The 2020 SolarWinds attack compromised the build pipeline, not the application. The 2021 Codecov breach exfiltrated credentials from thousands of CI environments through a modified bash uploader script. These aren't edge cases — pipelines are high-value targets with wide attack surfaces and historically weak security controls.

This guide covers CI/CD pipeline security from a penetration testing perspective: the attack surface, common vulnerabilities, and how to test for them.

The CI/CD Attack Surface

A typical CI/CD pipeline involves:

Each link in this chain is a potential entry point. The attacker's goal is typically secrets exfiltration (cloud credentials, API keys, signing keys) or code injection into the build to ship malicious artifacts.

Pipeline Injection via Pull Requests

The most common CI/CD vulnerability class: pipelines that run untrusted code from pull requests with access to secrets.

GitHub Actions example of the vulnerable pattern:

# VULNERABLE: runs on pull_request from forks, has access to secrets
on:
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    env:
      DATABASE_URL: ${{ secrets.DATABASE_URL }}  # exposed to PR code
    steps:
      - uses: actions/checkout@v4
      - run: npm test

Any contributor who can open a pull request can modify the workflow file or test code to exfiltrate DATABASE_URL. For public repositories, this means anyone on the internet.

Safer pattern — use pull_request_target only for display/metadata, keep secrets out:

# SAFER: separates the trusted checkout from the untrusted PR code
on:
  pull_request_target:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      # Checkout at the base SHA, not the PR SHA
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.base.sha }}

Testing approach:

Secrets in CI/CD Environments

Secrets are frequently exposed in CI/CD pipelines through:

Environment Variable Exposure

Environment variables in CI are often printed to logs accidentally:

# Accidentally prints all environment variables including secrets
- run: env
- run: printenv
- run: set  # Windows equivalent

# Or through debug mode
- run: echo "Debug: $DATABASE_URL"

Test for secrets in build logs: search log output for patterns like AWS_, SECRET, KEY, TOKEN, PASSWORD. Many CI platforms have log scrubbing, but it often misses variables that aren't explicitly registered as secrets.

Secrets Committed to Repositories

Scan the repository for secrets committed directly:

# Scan with truffleHog
trufflehog git https://github.com/org/repo --only-verified

# Or gitleaks
gitleaks detect --source . --verbose

# Check git history including deleted files
git log --all --full-history -- "**/*.env"
git log -p --all | grep -E "(password|secret|key|token)" -i

Hardcoded Credentials in Pipeline Files

Pipeline configuration files themselves sometimes contain credentials. Check all .github/workflows/*.yml, .gitlab-ci.yml, Jenkinsfile, and equivalent files for hardcoded values.

Dependency Confusion and Supply Chain Attacks

Build pipelines pull dependencies — npm packages, pip packages, Docker base images, GitHub Actions, Terraform modules. Each is a potential supply chain attack vector.

Dependency Confusion

If your organization has internal packages with generic names (like utils, logger, auth), check whether public registries have packages with the same name. Package managers configured to check public registries first will pull the public package over the internal one — and if the public package is malicious, it executes during the build.

# Check if internal package names exist on npm
npm view @yourorg/internal-package  # Should 404 or show your package

# For pip
pip index versions internal-package-name  # Should not exist publicly

Unpinned Dependencies

Pipelines that use unpinned dependencies (latest tags, version ranges like ^1.0.0) are vulnerable to malicious package updates. The left-pad incident showed availability risk; malicious maintainer takeover shows the security risk.

For GitHub Actions specifically, always pin to a full commit SHA:

# VULNERABLE: tag can be updated to point to malicious code
uses: actions/checkout@v4

# SAFE: pinned to exact commit
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2

Malicious Third-Party Actions

GitHub Actions from unknown or unmaintained publishers can be modified to exfiltrate secrets. Review all third-party actions used in workflows:

Self-Hosted Runner Risks

Self-hosted CI runners have access to the network segment they're deployed in. If a runner is compromised through pipeline injection, an attacker can pivot to internal services.

Testing for self-hosted runner risks:

Overpermissioned Pipeline Tokens

GitHub Actions provides an automatically generated GITHUB_TOKEN with configurable permissions. The default permissions in many organizations are broader than necessary.

# Check default permissions in workflow
- run: |
    curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
      "https://api.github.com/repos/${{ github.repository }}" \
      | jq '.permissions'

Principle of least privilege for pipeline tokens:

Artifact Integrity and Signing

Build artifacts (container images, binaries, packages) should be signed so consumers can verify they came from your pipeline, not from an attacker who compromised distribution.

Check for:

Jenkins-Specific Vulnerabilities

Jenkins is widely deployed and has a large historical vulnerability surface:

Testing Checklist

Tools

Remediation Priorities

Treat pipeline code as privileged code. Apply the same review standards to Jenkinsfiles, GitHub Actions workflows, and CI scripts as you do to production application code. Require mandatory review for changes to pipeline configuration files.

Use short-lived credentials everywhere possible. OIDC-based authentication (AWS IAM OIDC, GCP Workload Identity Federation) eliminates long-lived credentials entirely. If you must use long-lived credentials, rotate them regularly and monitor for use from unexpected sources.

Segment runner access. Runners should not have access to production databases, internal APIs, or other services unless the pipeline genuinely needs it. Network egress from runners should be restricted to what's necessary for the build.

Pin everything. Actions, Docker base images, language dependencies — pin to exact versions with cryptographic hashes. Use a tool like Dependabot or Renovate to keep pins updated automatically.

CI/CD vulnerabilities often manifest as exposed secrets in deployed applications. Ironimo scans your web application for leaked credentials, insecure headers, and misconfigured endpoints that indicate pipeline security issues — running the same checks a security engineer would, on demand.

Start free scan
← Back to blog