Part one

An image of the Cilium supply chain

The last twelve months have been rough on the open source supply chain. Axios was compromised on npm and shipped a remote access trojan inside otherwise normal-looking releases. LiteLLM’s PyPI package was hijacked to exfiltrate environment variables. Typosquatted forks of Trivy were published to catch people who fat-finger go install. And the canonical example, the 2020 SolarWinds breach, is still the cautionary tale we keep coming back to: attackers got into the build system and pushed malware through normal Orion updates to roughly 18,000 organizations, including U.S. federal agencies, NATO, and Microsoft. The malware sat dormant for months. The breach went undetected for the better part of a year.

Cilium runs in the kernel-level networking path of millions of Kubernetes pods. If our supply chain were compromised, the blast radius would not be small. Hardening the project against that scenario is something we work on continuously, and we wanted to write down what we actually do, in detail. Most of what follows isn’t Cilium-specific: any open source project running CI/CD on GitHub Actions can apply these patterns. We’ve also called out where we still fall short, in case any of it makes a useful starting point for someone else.

This is the first post in a three-part series. This post covers access control: who can trigger builds and what code CI is allowed to execute. Part 2 will cover dependency hardening, and Part 3 credential isolation, release verification, and the gaps we’re still closing.

TL;DR

If you don’t have time to read the whole series, here’s what Cilium does to harden its supply chain today, organized by which layer of the pipeline each control lives at:

LayerControlWhat it does
Who triggers buildsTrigger control via ArianeOnly verified org members can fire CI workflows from PR comments, against an explicit allow-list of workflows.
What code CI executesTwo-phase checkouts for pull_request_targetTrusted code (composite actions, scripts, signing logic) is loaded from the base branch; the PR head is only used as Docker build context, never executed as a script.
Who reviews CI changesCODEOWNERS gatesAnything under .github/ requires review from the security-focused CI team, and auto-approve.yaml requires a maintainer.
What dependencies CI pulls inSHA-pinned actions and imagesEvery uses: references a 40-character commit SHA; container images are pinned by @sha256: digest. Renovate keeps the pins fresh and waits 5 days before picking up new releases.
What Go modules ship in the binaryVendored Go dependenciesEverything is checked into vendor/ and reviewed by the @cilium/vendor team, so a typosquatted or hijacked module shows up as a diff at review time.
What workflows are even allowed to look likeStatic analysis on workflowsCodeQL enforces explicit permissions: on every workflow, actionlint catches unsafe patterns, and both flag GitHub Actions expression injection in run: blocks.
What credentials are reachableCI vs. production credential isolationCI credentials can only push to *-ci development tags; production registry credentials sit behind a protected release environment that requires maintainer approval.
What consumers can verifySigned releasesEvery release image and Helm chart is signed with Sigstore Cosign using keyless OIDC, with SBOM attestations attached.
Where we still fall shortGaps we’re still closingNo SLSA provenance yet, no PR-time dependency review, no govulncheck in CI, and a handful of internal @main references that need to move to a dedicated composite-actions repo.

Controlling who runs what

The first question in any CI supply chain story is: who can trigger a build, and what code does it execute? Plenty of CI compromises start right here, by tricking the system into running attacker-controlled code with elevated privileges.

Workflow trigger restrictions with Ariane

Ariane is a GitHub bot we wrote in-house to dispatch CI workflows from PR comments. When a maintainer types /test or /ci-eks on a pull request, Ariane checks that the commenter belongs to the organization-members team, figures out which workflows to fire (including dependencies, like tests that need a fresh image build first), and dispatches them via workflow_dispatch.

The interesting bit is the allow-list. Only verified org members can trigger workflows, and the set of workflows that can be triggered is enumerated by hand in the config:

.github/ariane-config.yaml
allowed-teams:
  - organization-members

triggers:
  /test\s*:
    workflows:
      - conformance-aws-cni.yaml
      - conformance-clustermesh.yaml
      - conformance-eks.yaml
    # ...and so on
    depends-on:
      - /build-images-dependency
  /ci-aks:
    workflows:
      - conformance-aks.yaml
    depends-on:
      - /build-images-dependency

A random external commenter typing /test in a PR is ignored. They can’t kick off our expensive cloud-provider conformance suites or burn through our CI minutes.

Separating trusted and untrusted code in CI

When somebody opens a PR we need to build their code, but we obviously can’t trust it. This is the classic pull_request_target problem. We avoid pull_request_target where we can, but a handful of workflows still need it, and we wrap those in mitigating controls.

The image build workflow is the canonical example. It splits the checkout in two:

.github/workflows/build-images-ci.yaml
- name: Checkout base or default branch (trusted)
  uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
  with:
    ref: ${{ github.base_ref || github.event.repository.default_branch }}
    persist-credentials: false

# ...trusted setup steps run here, including loading composite actions...

# Warning: since this is a privileged workflow, subsequent workflow job
# steps must take care not to execute untrusted code.
- name: Checkout pull request branch (NOT TRUSTED)
  uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
  with:
    persist-credentials: false
    ref: ${{ steps.tag.outputs.sha }}

The first checkout grabs the base branch (code that’s already been reviewed and merged) so we can load our composite actions, scripts, and the Cosign signing logic from a known-good source. Only after that does the workflow check out the PR head, and that checkout is used purely as build context for docker build. Nothing from the PR branch is ever executed as a script.

We get security reports about this pattern fairly regularly. Automated scanners and well-meaning researchers see “pull_request_target plus a second checkout” and flag it as a vulnerability. In the general case they’re right too. In ours, the workflow is intentionally designed so the pattern is safe:

The worst-case outcome of a compromised PR build is a malicious CI image landing in the development registry, which is the same blast radius any CI system that builds contributor code carries. We do appreciate every report and read each one carefully, but this pattern is intentional.

CODEOWNERS as a review gate

We lean on CODEOWNERS pretty heavily so that changes always land in front of the people with the most context. For CI configuration that means anything under .github/ is owned by @cilium/github-sec (our security-focused CI team) plus @cilium/ci-structure, and the auto-approve.yaml workflow is owned by @cilium/cilium-maintainers:

CODEOWNERS

/.github/                          @cilium/github-sec @cilium/ci-structure
/.github/ariane-config.yaml        @cilium/github-sec @cilium/ci-structure
/.github/renovate.json5            @cilium/github-sec @cilium/ci-structure
/.github/workflows/                @cilium/github-sec @cilium/ci-structure
/.github/workflows/auto-approve.yaml  @cilium/cilium-maintainers

Nobody can change the CI pipeline without an explicit review from the team responsible for keeping it safe.

Next up, Part 2 we will cover how we lock down what code builds actually pull in: SHA-pinned actions, automated dependency updates, and Go module vendoring.

André Martins is a Cilium maintainer and Software Engineer, Isovalent at Cisco. Feroz Salam is a member of the Cilium Security Team and a Security Engineer, Isovalent at Cisco. Find Cilium on GitHub and join the community on Slack.