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:
- A version control system (GitHub, GitLab, Bitbucket)
- A CI platform (GitHub Actions, GitLab CI, Jenkins, CircleCI, Buildkite)
- Build runners that execute pipeline steps (cloud-hosted or self-hosted)
- Artifact registries (container registries, npm feeds, package repositories)
- Secrets management (environment variables, vault, cloud KMS)
- Deployment targets (Kubernetes, cloud providers, servers)
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:
- Review all workflow triggers:
pull_request,pull_request_target,issue_comment,workflow_dispatch - Identify which triggers expose secrets to untrusted code
- For public repositories, check if forks can trigger secret-bearing workflows
- Look for
pull_request_targetworkflows that also check out PR code — this is the dangerous pattern that looks safe but isn't
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:
- Is the publisher's account trustworthy?
- Is the action pinned to a specific commit SHA?
- Does the action request more permissions than necessary?
- Are there recent reviews of the action's source code?
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:
- Runner metadata access: From a pipeline step, attempt to access cloud metadata endpoints (
http://169.254.169.254/,http://metadata.google.internal/). If accessible, runner credentials are retrievable. - Network access: Map what internal services the runner can reach (databases, internal APIs, Kubernetes API server).
- Persistent state: Check if build artifacts, credentials, or workspace files persist between runs and could be read by subsequent pipelines from other branches or repositories.
- Runner registration tokens: Runner registration tokens, if leaked, allow registering malicious runners that intercept jobs.
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:
- Restrict
GITHUB_TOKENpermissions to the minimum needed: setpermissions: read-allat the workflow level and override per-job for write access - Audit what AWS/GCP/Azure roles are bound to CI service accounts
- Rotate credentials that pipelines use on a schedule — treat them as short-lived
- Use OIDC-based cloud authentication where possible — pipeline gets short-lived tokens, no long-lived credentials to exfiltrate
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:
- Are container images signed with Cosign or Notary?
- Are release binaries signed with GPG or Sigstore?
- Is there provenance attestation (SLSA) for build artifacts?
- Do deployment processes verify signatures before deploying?
- Is the artifact registry access-controlled? Can an attacker push malicious artifacts?
Jenkins-Specific Vulnerabilities
Jenkins is widely deployed and has a large historical vulnerability surface:
- Script Console RCE: The Jenkins Script Console (
/script) allows executing arbitrary Groovy. If accessible to authenticated users without admin role, or if anonymous access is enabled, it's direct RCE. - Groovy sandbox escapes: Jenkinsfile pipelines run in a sandbox, but many sandbox escape CVEs exist. If Jenkinsfile files are committed and run without approval review, review the Groovy code.
- Credential store access: Jenkins stores credentials in an encrypted store. Users with the ability to run pipelines can potentially read credentials via
withCredentialsblocks and logging tricks. - Old plugins: Jenkins plugins are often unmaintained and carry their own CVEs. Run
jenkins-plugin-manageror check the plugin manager for outdated plugins.
Testing Checklist
- Review all pipeline trigger conditions — identify which allow untrusted code to run with secret access
- Scan build logs and repository history for accidentally committed secrets
- Check all third-party actions and dependencies for pinning to exact versions
- Audit dependency names against public registries for confusion attack risk
- Test self-hosted runner network access to internal services and metadata endpoints
- Review cloud IAM roles assigned to CI service accounts for over-permission
- Check whether pipeline tokens have only minimum required permissions
- Verify artifact signing and integrity verification at deployment
- For Jenkins: check Script Console access controls and plugin versions
- Test branch protection rules — can non-maintainers modify pipeline configuration files?
- Review OIDC federation configuration for cloud credential issuance
Tools
- truffleHog — scans git history and CI outputs for secrets
- gitleaks — fast secret scanning with configurable rules
- semgrep — static analysis for pipeline configuration security issues
- Checkov — scans pipeline files (GitHub Actions, GitLab CI) for misconfigurations
- zizmor — GitHub Actions-specific security analysis
- Ironimo — automated web application scanning that detects exposed credentials, misconfigured CI/CD-related endpoints, and API key exposure in deployed applications
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