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.
OIDC-based AWS access (no stored keys) YAML (GitHub Actions)
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:

restrictive token permissions YAML (GitHub Actions)
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:

pin actions to SHA, not tags YAML
# 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 in GitHub Actions YAML
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