The CI/CD Pipeline Is the Attack Surface
Episode 9 of the CAXA Technologies Security Operations Series
Adding a SAST scanner to your CI/CD pipeline is not the same as securing it. 23,000 repositories learned that in March 2025.
Episode 8 closed with the observation that the CI/CD pipeline raises a different version of the same question this series has been working through: what do we depend on, is it scanned, do we have SLAs for it? The difference is the target. When the target is your application’s dependencies, Episode 8’s prioritisation model applies directly. When the target is the pipeline itself, you need a different answer. This is that answer.
The shift-left argument is correct. Getting security earlier in the delivery lifecycle reduces the cost of fixing problems and shortens the window between introduction and detection. I am not arguing against it. What I am arguing is that “shift-left” has been operationalised almost exclusively as “add SAST to CI/CD” and that this addresses only one of the three meaningful attack surfaces in a modern pipeline. The two it misses are where the significant exploitation has been occurring.
Three surfaces, not one
When practitioners talk about securing the CI/CD pipeline, they usually mean scanning code before it reaches production. That is a real security gain. A SAST scanner that catches an injection vulnerability before merge prevents a class of problems from being deployed. But the threat model it addresses is: developer introduces vulnerable code. It does not address:
The pipeline execution layer: the actions, scripts, and tools the pipeline itself runs
The dependency installation layer: the packages pulled into the build environment during install
The secrets the pipeline handles: the credentials present in the runner environment at build time
These three surfaces have all seen documented, large-scale exploitation in the past 13 months. Not in theory. In production pipelines, at organisations with active security programmes.
When the action is the attack
On 12 March 2025, a threat actor used a compromised GitHub bot token to rewrite the version tags across tj-actions/changed-files, a widely used GitHub Action for detecting which files changed in a pull request. All tags, including semantically versioned ones like @v4.0.1, were rewritten to point to a single malicious commit. Any workflow that referenced the action by tag rather than by commit SHA automatically pulled the malicious version without any change to the workflow file itself.
The payload was a Python script that iterated through /proc to locate the Runner.Worker, Runner.Listener, and runsvc processes running in the GitHub Actions runner environment, read /proc/<PID>/environ to extract all environment variables, and printed the results to build logs. The variables it found included GitHub Personal Access Tokens, npm publish tokens, AWS access keys, and private RSA keys. GitHub’s log masking prevents secrets appearing in normal build output, but it cannot mask values read directly from process memory. The attack bypassed log masking entirely. 23,000 repositories were affected before the campaign was contained. CISA issued an advisory covering both tj-actions/changed-files (CVE-2025-30066) and the upstream compromise that made it possible, reviewdog/action-setup (CVE-2025-30154).
The attack was not sophisticated in the sense of requiring novel exploitation techniques. The technique (reading process environment variables via /proc) is standard Unix operating system behaviour. What made it effective was simpler: GitHub Actions version tags are mutable. A tag is a pointer to a commit, and that pointer can be rewritten by anyone with push access to the repository. Every workflow that pinned to a version tag rather than a commit SHA was trusting that the maintainer’s account would never be compromised and that no one with access would ever rewrite the tag. That trust was misplaced.
Twelve months later, the same mechanism produced a more pointed result.
When the scanner becomes the weapon
In late February 2026, a threat actor tracked as TeamPCP extracted a privileged access token from Aqua Security’s GitHub Actions environment. On 1 March, Aqua disclosed the initial compromise and rotated credentials. The attacker retained residual access. On 19 March 2026, TeamPCP force-pushed 75 of 76 version tags in aquasecurity/trivy-action and all 7 tags in aquasecurity/setup-trivy to malicious commits. A malicious version of the Trivy binary itself, v0.69.4, was simultaneously published to GitHub Releases and Docker Hub. CVE-2026-33634 was assigned and added to the CISA KEV catalogue as actively exploited.
Trivy is one of the most widely used open source vulnerability scanners for container images and filesystems. 10,000 or more CI/CD workflows reference trivy-action directly. The malicious version appeared to run normally: it still scanned, still produced vulnerability output, still exited with the expected codes. While doing so, it harvested credentials from the runner environment and exfiltrated them via a command-and-control channel built on a decentralised blockchain canister, polling every 50 minutes to avoid network traffic patterns that would trigger alerting.
The irony is the argument. Organisations added Trivy to their pipelines specifically to improve security. The scanner ran with the same permissions as every other step. It had access to every secret, every environment variable, every credential injected for the build. The step that was meant to find vulnerabilities in your software became the step that extracted your production credentials. The scanner ran. The pipeline appeared clean. The credentials were gone.
This is not a criticism of Trivy or Aqua Security. Maintaining complex open source projects at this scale is hard, and the attack required persistent adversarial focus across multiple weeks. The point is structural: a compromised tool running in your pipeline runs with your permissions. Implicit trust in a pipeline step, granted by virtue of its stated purpose, is not a security posture.
One detail from the incident timeline matters: Aqua disclosed the initial compromise on 1 March and rotated credentials. TeamPCP retained access regardless and executed the larger attack on 19 March. Partial credential rotation is not remediation. When a credential is known to have been in a compromised environment, every credential in that environment should be treated as compromised.
When the dependency install is the attack
On 31 March 2026, a threat actor attributed by Google Threat Intelligence to UNC1069, a North Korea-nexus group, hijacked the npm account of the primary maintainer of Axios and published two malicious versions: axios@1.14.1 and axios@0.30.4.
Axios has approximately 100 to 174 million weekly downloads and 174,000 dependent packages. It is present in the dependency tree of the majority of Node.js applications. Both malicious versions contained a hidden dependency, plain-crypto-js@4.2.1, which executed via npm’s postinstall lifecycle hook during npm install or npm ci. The payload was obfuscated using string reversal and XOR encoding to evade static analysis. Once running, it harvested the full contents of the runner environment: AWS credentials, GitHub tokens, npm tokens, GCP service account keys, Azure credentials, Kubernetes configuration, SSH keys, .env file contents, and database credentials. The malicious versions were available for approximately three hours before Elastic Security Labs’ automated monitoring detected the compromise, coordinated disclosure was filed, and npm removed the packages. Safe versions are axios@1.14.0 and axios@0.30.3.
This attack is mechanically different from tj-actions and Trivy. There was no tag rewriting. The published version passed npm’s integrity checks because it was published by the legitimate account. The mechanism that made it effective is the postinstall lifecycle hook: npm executes arbitrary scripts during package installation with the full permissions of the process running npm. In a CI/CD context, that process has access to the entire runner environment. Package managers do not sandbox postinstall scripts. This is not a recent design decision. It is how package managers have worked for decades. What has changed is that CI/CD environments now routinely inject production credentials at install time, and the install step executes code from packages before anyone has reviewed what that code does on a given run.
For this specific attack, SLSA provenance and SHA pinning on GitHub Actions would not have helped: the malicious version was published through the legitimate account and would have carried a valid signature. The publisher-side controls that would have made a difference (two-factor authentication enforcement on npm publish operations, scoped publish tokens, monitoring for unexpected version publishes) are outside your control as a consumer. What the TVM programme can control is when and how it adopts new versions.
The consumer-side control this points toward is a package adoption cooldown: a formalised waiting period before a new version is pulled into your dependency tree. A 48 to 72 hour hold on new releases from high-impact packages gives the security community time to identify and disclose issues that are not immediately visible. The Axios window was three hours: a 48-hour cooldown would have protected most consumers entirely. For packages with a wider blast radius, longer holds are defensible. The cooldown is not a manual process; it can be implemented as a policy gate in your dependency management tooling, blocking automated dependency updates until the version has aged past the threshold.
The complication is SLA tension. If a critical CVE in a package is patched in a new release, a hard-floor remediation SLA of 24 hours conflicts directly with a 48-hour adoption cooldown. The programme cannot have both. The resolution is the same pattern the series has used throughout: when the preferred control is temporarily unavailable, apply a compensating control for the duration and track it formally. In this case: isolate the services using the unpatched version, reduce their network reach, increase monitoring, and adopt the new release as soon as the cooldown expires and the release has been assessed. The compensating control covers the gap; the cooldown protects against the next Axios.
Episode 4’s operating model named the structural failure that makes compensating controls dangerous: temporary measures tend to become permanent. The immediate pressure lifts, proper remediation is deprioritised, and months later no one remembers that the monitoring increase was a stopgap. Every compensating control deployed during a cooldown window needs a defined owner, a review date, and an escalation trigger that increases organisational visibility rather than simply extending the review period. A compensating control that has outlived its intended window by three months is a different risk than a three-day-old one, and the process should reflect that.
The three incidents together, each using a different attack vector, each reaching the same target, make the argument plainly: the runner environment at build time is a high-value credential store, and it is being treated as one by attackers before most organisations have recognised it as one.
The runner as credential store
The pattern I have seen repeatedly in security programme work is that pipeline credentials are treated as a developer experience concern rather than a security architecture concern. Long-lived AWS access keys stored as GitHub Actions repository secrets, rotated annually at best. Service account credentials shared across multiple workflows because creating scoped credentials per workflow creates friction. The question of what credentials live in the pipeline environment, for how long, and with what scope, is answered by whoever set up the pipeline first, not by a deliberate security decision.
The right model is short-lived, scoped tokens injected at runtime, with no persistent credentials stored in the Actions secret store. GitHub Actions supports OIDC federation with AWS, GCP, and Azure, generating tokens that expire within minutes and leave nothing to steal after the build completes. HashiCorp Vault’s agent injection pattern provides the same capability for any credential type. The credentials exist only for the duration of the workflow, scoped to what that workflow requires.
Short-lived credentials do not make the runner immune to the attacks described above. A compromised action or postinstall hook running during the build can still extract a short-lived token during its validity window. The difference is containment: a 15-minute OIDC token that expires at build completion is materially less damaging than a 90-day access key that continues to work after the build, after the incident, and after the organisation has moved on.
The same prioritisation framework Episode 8 established for application vulnerabilities applies directly here. CVE-2025-30066 and CVE-2026-33634 are both in the CISA KEV catalogue as actively exploited. By the KEV hard-floor rule: any pipeline still referencing the compromised versions after KEV addition is carrying an exploited vulnerability in production infrastructure. That is not a risk acceptance candidate. It is an immediate remediation.
What good looks like in practice
SHA pinning is the baseline control for the pipeline execution layer. A workflow referencing aquasecurity/trivy-action@v0.35.0 is vulnerable to tag rewriting. A workflow referencing aquasecurity/trivy-action@abc123def456... is not: a commit SHA is immutable, and replacing it requires a collision attack rather than a compromised access token. Ratchet (Seth Vargo) and pin-github-action (mheap) both convert version tag references to SHA pins in bulk. Dependabot handles ongoing SHA updates when version comments are present. The OpenSSF published specific tooling guidance on this in June 2025.
For the dependency installation layer, lockfile integrity is the starting point. Running npm ci against a committed package-lock.json with cryptographic integrity hashes ensures the installed packages match what was reviewed, not what happens to be currently published. This does not prevent a postinstall hook attack during a legitimate lockfile update, but it constrains the exposure to that specific window rather than every install.
Runtime monitoring catches what gets through: tools such as StepSecurity’s Harden-Runner detect unexpected outbound network connections during the build, which is the primary signal that a compromised step is exfiltrating data. But monitoring alone is reactive. The complementary control is egress filtering at the pipeline network boundary.
Consider what a CI/CD pipeline legitimately needs to reach. For most pipelines it is a short, documentable list: a package registry or two, a container registry, cloud provider API endpoints for OIDC token exchange and artefact deployment, and possibly a private artefact store. A short, documentable list: rarely more than a dozen destinations for a typical build. Everything else can be blocked by default. Configuring runners with an egress allowlist (via network security group rules, host-based firewall policy, or the allowed-endpoints configuration in Harden-Runner) means a compromised action or postinstall hook that tries to reach an exfiltration endpoint finds the connection refused. The Axios payload attempted to reach sfrclak[.]com. If pipeline egress was restricted to known destinations, the credential harvest runs and produces nothing. The tj-actions campaign used GitHub Gist, which is a permitted destination for many pipelines; egress filtering would not have stopped it, but anomalous traffic monitoring would have: a build suddenly writing to Gist, at a volume inconsistent with its normal behaviour, is a detectable signal.
That distinction matters for how the control is implemented. Egress allowlisting without monitoring of permitted traffic is a partial defence. The Trivy C2 used a blockchain canister, polling every 50 minutes through what appeared to be normal HTTPS traffic. An allowlist that permits HTTPS to arbitrary internet destinations would not have blocked it. What detects it is behavioural monitoring of permitted egress: unexpected timing patterns, unexpected data transfer volumes, unexpected destination patterns within allowed domains. Permitted traffic should be logged, baselined, and alerted on deviation. The permitted destinations list itself should be documented and subject to change control: a new third-party integration that requires a new egress destination goes through review, not a one-off rule addition. That change control process is where the explicit decision is made about what the pipeline is allowed to reach, and it creates a natural audit trail.
On scanning tooling: the Trivy ecosystem compromise has raised legitimate questions about trust. Trivy’s scan functionality was not compromised, and Aqua Security has restored all update and build pipelines. For teams that choose to stay with Trivy, the safe path is trivy v0.69.3 or later, with trivy-action SHA-pinned to the v0.35.0 commit. For teams reassessing their scanning stack, Anchore’s Syft and Grype are the most credible open source alternative. Syft generates SBOMs in CycloneDX and SPDX formats; Grype scans them against a daily-updated database using EPSS and KEV-weighted risk scoring, producing the same prioritised output as the composite model Episode 6 and Episode 8 established. In multi-language environments Grype also produces fewer false positives than Trivy, because it uses ecosystem-aware matching rather than CPE-based matching, which avoids the cross-namespace collisions that inflate Trivy’s finding counts.
The trade-off is scope. Syft and Grype cover container and filesystem vulnerability scanning well. They do not cover IaC scanning, secrets detection, or Kubernetes runtime scanning. Trivy handles all of these in a single binary. Teams that need the full breadth will either run Trivy alongside Grype, or supplement Grype with Checkov for IaC and gitleaks for secrets detection. Neither combination is operationally complex. The point is that the decision should be deliberate: understand what each tool covers and where the gaps are, rather than assuming one binary addresses the complete problem.
The question this leaves open
The three incidents in this episode converge on the same underlying question: what does your pipeline depend on, is it scanned, and do you have SLAs for remediating problems in it?
These are not new questions. They are the same questions this series has been working through since Episode 1 established vulnerability management as a decision-making discipline. What changes when the subject is pipeline dependencies rather than application dependencies is that most organisations have not yet extended their VM programme to cover them. The pipeline is treated as infrastructure: the actions it runs, the packages it installs, and the tools it calls are reviewed once when first adopted and then left in place, unpinned and unmonitored, until something goes wrong.
Extending the same discipline to pipeline dependencies is a programme scope expansion, not a programme redesign. The prioritisation framework is the same. The SLA model is the same. The asset inventory question, what do we have, what does it depend on, is the same. The tooling overlaps significantly.
The September 2026 EU Cyber Resilience Act deadline adds a compliance forcing function to these controls: SHA pinning, egress allowlisting, and adoption cooldown policies are the same controls that satisfy CRA’s 24-hour reporting obligation for actively exploited CVEs, not because they were designed for compliance, but because they are what a well-run programme looks like.
That is also the definition of the software supply chain security problem. What does your software depend on to be built and delivered, not only to run? What is the full chain from developer workstation to production artefact, and where in that chain are the dependencies that are not currently in scope for your vulnerability management programme?
The supply chain series will cover that chain in full. This episode has introduced the question. That series answers it.

Comments