Git, CI/CD & GitOpsLXV · Software Supply Chain SecurityCITrust
CI trust and the runner as an actor — the runner is an attacker if compromised
What you'll learn
- Recognise the CI runner as an actor at the build boundary with delegated production trust
- Identify the controls that establish CI trust - ephemeral runners, secret isolation, pinned build steps, hardened build platforms
- Distinguish a hosted build platform from a self-hosted runner and the threat model of each
- Recognise why provenance must be signed by a build platform the verifier trusts, not by the runner itself
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
The CI runner is the actor at the build boundary. The runner executes the build, reads the source, pulls the dependencies, holds the secrets, and writes the artifact. The runner is delegated the production trust that the developer cannot exercise: the trust to sign the artifact, to push to the registry, and to attest to what the build did. A compromised runner is a build-stage attacker with all of that trust delegated to it. The attacker at the build boundary does not need to compromise the developer; the attacker only needs to compromise the runner.
The runner as an actor
A CI run is an actor in the supply chain. The actor has:
- Read access to the source. The runner checks out the commit. The runner can read every file in the repository, including secrets stored in the repository.
- Network access to external registries. The runner pulls dependencies. The runner can also be redirected to a malicious registry if the dependency tree is not pinned.
- Access to build secrets. The runner holds the secrets configured for the job — registry credentials, signing keys, cloud credentials. The secrets are available to every step the runner executes.
- Write access to the artifact registry. The runner pushes the artifact and the attestation. The signatures applied here are bound to the runner’s identity.
flowchart LR
A["Source commit"] --> B["Runner"]
B --> C["Read source"]
B --> D["Pull deps"]
B --> E["Use secrets"]
B --> F["Build artifact"]
F --> G["Sign artifact"]
G --> H["Push to registry"]
H --> I["Attestation published"]
A -. "compromised runner can substitute bytes" .-> F
D -. "compromised runner can substitute deps" .-> F
E -. "compromised runner can leak secrets" .-> H
The arrows are the operations the runner performs. The dotted lines are the failure modes. A compromised runner can substitute bytes at the build step, substitute dependencies at the pull step, and leak secrets at the sign-and-push step. The signatures the runner applies are valid — the attacker controls the signing key — but the artifact the signature attests is not the artifact the developer intended.
This is the threat model that SolarWinds illustrates. The attacker did not compromise the developer or the source code. The attacker compromised the build pipeline and substituted bytes at the build step. The artifact was signed by the build platform’s identity, and the signature was valid. The attacker forged nothing; the attacker changed the build.
The threat model: hosted vs self-hosted
The two deployment models for CI runners have different threat models:
- Hosted build platforms (GitHub Actions, GitLab.com CI, Buildkite cloud, etc.) run the runner on infrastructure the platform provides. The secrets are managed by the host; the runner is ephemeral; the signing key is held by the platform and not by the user. The threat model is “the platform is honest and isolated”. The platform’s hardening is the defence.
- Self-hosted runners run on infrastructure the user controls. The user is responsible for the runner’s hardening, the secrets, and the signing key. The threat model is “the runner is an attacker”. The user must assume the runner is compromised and design the build around that assumption.
The SolarWinds attack was on a hosted build pipeline. The attacker compromised the build pipeline, not the runner host. The Codecov attack was on a self-hosted runner equivalent — the attacker compromised a script the build pulled in and executed on the runner. Both succeeded at the build boundary because the runner held the trust delegated to it.
The controls: ephemeral, isolated, hardened
The controls that establish CI trust are:
- Ephemeral runners. The runner is created for the build and destroyed after. There is no persistent state on the runner. The attacker cannot persist across runs.
- Step isolation. Each build step runs in a fresh container, with no shared filesystem and no shared state with the previous step. The attacker cannot read secrets from one step in another.
- Pinned build steps. The build uses pinned actions or
pinned images at every step. The build does not pull from
latestor from a mutable tag. - Hardened build platform. The platform signs the artifact; the signing key is not available to the runner. The platform provides the provenance attestation; the attestation is signed by the platform’s identity, not by the runner’s.
- Short-lived credentials. Secrets are issued per-job, scoped to the registry they need to write to, and revoked when the job ends. A leaked secret is a secret that expires in minutes.
The five controls together establish CI trust. A team that uses ephemeral runners but holds the signing key in the runner has closed the persistence attack but not the signing attack. A team that uses pinned steps but holds long-lived credentials has closed the dependency attack but not the secret-leak attack. The chain is the five together.
Production discipline
- Treat the runner as an attacker. The trust model is “the runner is compromised; what is the blast radius?”. The answer should be “small”.
- Use ephemeral runners. No persistent state. No long-lived credentials. The attacker cannot persist.
- Move signing to a hardened build platform. The runner requests a signature; the runner does not hold the key.
- Pin every build step. Every action, every image, every tool is pinned by digest. The build is reproducible.
- Generate and verify provenance. The platform signs the provenance; the deployment step verifies it before deploying.
Cross-course references
- Git, CI/CD & GitOps — Part LXV-01 (Trust Boundaries) maps the build boundary this lesson covers.
- Git, CI/CD & GitOps — Part XLV-06 (Provenance) covers the attestation model the build platform produces.
- Git, CI/CD & GitOps — Part XXXVII (CI Foundations) covers the runner model in detail.
- Linux for Production Sysadmins — Part XXVIII (Service Hardening) covers the OS-level analogue of hardening the runner.
Quiz
Knowledge check · 4 questions
Q1. Why must the signing key be held by the build platform and not by the runner?
Q2. A self-hosted runner that is hardened with a long-lived deploy credential is a runner that holds the trust anchor of the artifact registry.
Q3. Name the five controls that, together, establish CI trust at the build boundary.
Q4. Identify the gap in the team's CI trust posture and the rule that closes it.
Team T runs a self-hosted GitHub Actions runner on a VM in the team's VPC. The runner is configured with a long-lived personal access token that can push to the team's container registry. The token is stored in the runner's environment. The runner is reused across jobs; the runner's filesystem persists between runs. The runner executes a build step that pulls an action by tag (`@v3`) from a public marketplace. An attacker compromises the action's maintainer account and publishes a backdoored version under the `@v3` tag. The next build pulls the backdoored action. The action reads the registry token from the runtime environment and publishes a malicious image to the registry. The image is signed by the team's key because the runner holds the key.
Passing score: 75%. Answers are checked in this browser.