Why CI/CD Is a High-Value Attack Target
Your CI/CD pipeline has something attackers want more than most systems: trusted access to everything. It reads your source code, runs your tests, holds your deployment credentials, and writes to your production environment. Compromise the pipeline and you compromise everything downstream.
Real attacks targeting CI/CD are now common. The SolarWinds supply chain attack compromised the build system. The Codecov breach injected a malicious script via a tampered bash uploader. The tj-actions/changed-files attack in 2025 exfiltrated secrets from thousands of GitHub Actions workflows. These aren't theoretical risks.
CI/CD is production: Most organisations apply strong security controls to production infrastructure but treat CI/CD pipelines as low-risk internal tooling. That's the wrong mental model. Your pipeline is production β protect it accordingly.
Secrets in Pipelines: The Most Common Failure
The most common CI/CD security failure is simple: secrets in the wrong place. This means hardcoded credentials in workflow YAML files, secrets stored as plaintext environment variables, secrets printed in build logs, and long-lived credentials that never rotate.
The right approach is layered:
- Use the platform's secret store: GitHub encrypted secrets, GitLab CI/CD variables (masked and protected), or an external vault (HashiCorp Vault, AWS Secrets Manager)
- Prefer short-lived credentials: OIDC tokens for cloud provider access mean no long-lived AWS/GCP/Azure keys stored anywhere
- Scan for secrets in code: Run secret detection on every commit, not just in CI β use pre-commit hooks too
- Never print secrets in logs: Pipeline logs are often accessible to all developers. Secrets in logs are effectively public within your organisation.
permissions: id-token: write # required for OIDC contents: read steps: - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789:role/deploy-role aws-region: us-east-1 # No AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY needed
Least-Privilege Tokens for Pipeline Jobs
In GitHub Actions, the GITHUB_TOKEN is automatically available to every workflow. By default it has write access to contents, packages, and pull requests. That's far more than most jobs need.
Explicitly restrict token permissions at the workflow and job level:
permissions: read-all # workflow-level: deny all by default jobs: test: runs-on: ubuntu-latest permissions: contents: read # only what this job needs steps: - uses: actions/checkout@v4 - run: npm test comment-pr: runs-on: ubuntu-latest permissions: pull-requests: write # only this job needs PR write steps: - name: Post test results uses: actions/github-script@v7
In GitLab CI, use per-job deploy tokens or CI/CD variables scoped to specific environments. Never use a group-level token with full access for a job that only needs to push to a single registry.
Integrating SAST into Your Pipeline
Static Application Security Testing should run on every pull request and every commit to a main branch. Fast SAST tools (under 2 minutes) can run as PR checks. Deeper analysis can run as a nightly job or on merge.
Key principles for SAST in CI/CD:
- Fail fast: Critical and high findings should block merge. Medium and low should create issues, not block developers.
- Incremental scanning: Only scan changed files in PRs to keep feedback fast. Run full scans on main.
- Noise management: A SAST tool generating 200 findings per PR will be ignored. Tune signal-to-noise before making SAST a required check.
- Fix in PR: Surface findings in the PR diff view, not a separate security portal. Fix where the code is reviewed.
SCA and Dependency Scanning
Software Composition Analysis scans your dependencies for known CVEs. It should run on every PR that changes dependency manifests (package.json, requirements.txt, go.mod, etc.) and on a schedule to catch newly published CVEs.
SCA in CI/CD should:
- Block on critical CVEs with known exploits (high EPSS score)
- Create issues (not block) for medium CVEs without active exploits
- Generate and upload an SBOM artifact with every build
- Check for licence compliance violations alongside CVEs
Don't just run npm audit: npm audit and similar built-in tools are a starting point, not a complete SCA solution. They miss transitive vulnerabilities in complex dependency graphs and don't assess exploitability context.
Supply Chain Controls: Pinning and Signing
For GitHub Actions, pin every third-party action to a specific commit SHA, not a tag. Tags can be moved by the action author. A SHA is immutable:
# BAD: tag can be moved to point to malicious code - uses: actions/checkout@v4 # GOOD: SHA is immutable - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
For container images, use digest-pinned references (image@sha256:...) rather than tags. For dependencies, commit lock files (package-lock.json, Pipfile.lock) and verify them in CI.
Runner Security: Isolation and Hardening
GitHub's hosted runners are ephemeral β a fresh VM for every job, deleted after completion. This is a significant security advantage: no state persists between jobs, and lateral movement from a compromised job is limited.
Self-hosted runners are more complex. Key rules:
- Never register a self-hosted runner for public repositories β untrusted PRs can run arbitrary code on your runner
- Run each job in an isolated container or VM, not directly on the host
- Apply principle of least privilege β runners should not have write access to production systems
- Regularly rotate runner tokens and audit registered runners
Audit Logging for Pipeline Activity
Your CI/CD system should maintain comprehensive audit logs: who triggered a workflow, what secrets were accessed, what was deployed and when, and any permission changes. This is essential for incident response β when something goes wrong, you need to reconstruct exactly what the pipeline did.
GitHub provides organisation-level audit logs via the API. GitLab has a built-in audit event log. Forward these to your SIEM and alert on anomalous patterns: deployments outside business hours, unusual secret access, or pipeline runs on unexpected branches.
Security Gates: Fail the Build on Critical Findings
A security gate is a pipeline step that fails the build if security findings exceed a threshold. Without gates, scanning is advisory β findings accumulate and nobody fixes them.
security-gate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: AquilaX Security Scan uses: aquilax/scan-action@v1 with: api-key: ${{ secrets.AQUILAX_API_KEY }} fail-on-severity: critical # block on critical warn-on-severity: high # warn on high
Start permissive, tighten over time: Start with security gates that only block on confirmed critical vulnerabilities. Build developer trust by making the tool useful first. Once developers trust the tool's signal, progressively lower the threshold.
Build Security Into Every Pipeline
AquilaX integrates with GitHub Actions and GitLab CI to enforce security gates, scan for secrets, vulnerabilities, and supply chain risks β automatically on every commit.
Start Free Scan