-
Notifications
You must be signed in to change notification settings - Fork 3
Description
Background
This repository provides Docker-maintained reusable GitHub Actions workflows intended to securely build container images while producing signed SLSA-compliant provenance. The build pipeline is designed around trust minimization, immutability, and isolation:
- Build logic runs inside Docker-owned reusable workflows, not in user repositories.
- Provenance binds the image to an immutable source (git SHA) + workflow identity.
- The user repository cannot modify or inject build logic.
- BuildKit generates provenance and uploads attestations that downstream consumers can verify.
A key selling point of this model is that no actor in a consuming repository (even those with admin/write permissions) should be able to tamper with the build pipeline or provenance.
However, we have identified an important issue related to cache exporters and GitHub Actions cache semantics.
GitHub Actions Cache is not strongly isolated
We currently use a BuildKit cache exporter that pushes cache objects to the GitHub Actions cache backend. This backend has the following constraints:
- Caches are scoped to the repository and not visible across repositories.
- A reusable workflow cannot access caches from another repo, which does provide cross-repo isolation.
- Forked PRs can restore cache but cannot write cache, which prevents untrusted cache writes from forks.
However, any actor with write access to the consuming repository can write to caches in that repository, including producing arbitrary cache blobs.
For example:
- name: Poison cache
run: |
mkdir -p ~/.cache/tool
echo "malicious code" > ~/.cache/tool/bin
- uses: actions/cache@v4
with:
path: ~/.cache/tool
key: tool-linux-x64This is a known and accepted limitation of how GitHub Actions' cache works:
- Users with write access are assumed to be trusted.
- CodeQL explicitly documents this attack: https://codeql.github.com/codeql-query-help/actions/actions-cache-poisoning-poisonable-step/
- GitHub's security model states that anyone with write access can already access secrets and run arbitrary workflows.
So from GitHub's perspective, this is an "expected behavior".
Why this is a problem for our SLSA Model
SLSA's build isolation requirement explicitly states:
It MUST NOT be possible for one build to inject false entries into a build cache used by another build, also known as "cache poisoning". The output of the build MUST be identical whether or not the cache is used.
— https://slsa.dev/spec/v1.1/requirements#isolated
Today, because our reusable workflow restores cache from the user repository, the following attack is theoretically possible even though the attacker only has write access to their own repository:
- A repository author with write access intentionally poisons the BuildKit cache.
- The reusable workflow later restores that cache during the official Docker GitHub Builder build.
- The reusable workflow consumes the poisoned cache object without having visibility into how it was constructed.
This does not compromise provenance, because provenance is tied to the source SHA and workflow identity, not the cache contents. But it does violate the SLSA isolation guarantee and could influence the build result if BuildKit were to reuse modified cache layers.
Why it is more sensitive?
In a normal GitHub Actions repository:
- Cache poisoning is acceptable because all actors with write access are trusted.
- The attack is constrained to the same repository.
But in our model:
- The trusted build logic runs in a Docker-owned repository.
- The user repository only decides when a build happens, not how it runs.
Thus, users with write access should not be able to influence the build environment beyond declared inputs.
Allowing user-controlled cache contents introduces an implicit backchannel through which untrusted modifications could influence build behavior—even though the whole point of reusable workflows is to prevent users from altering the build.
Caching breaks that trust boundary.
Disable cache?
Given:
- GitHub Actions cache is not isolated enough for SLSA requirements.
- SLSA v1.1 explicitly forbids any form of cross-build cache poisoning.
- The SLSA reference implementation avoids caching entirely.
- A poisoned cache could change build results even if provenance remains trusted.
We should disable BuildKit cache exporters in this repository until GitHub provides a cache mechanism that guarantees isolation between builds or we implement a way to sign cache blobs.