Summary
Compiled .lock.yml workflows produced by gh aw compile reference container images by mutable tag, never by immutable digest. This includes both version-tagged and floating tags.
What we see today
A compiled lock file (e.g., review-responder.lock.yml) contains a gh-aw-manifest header listing six containers, referenced by tag only:
{
"containers": [
{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.23"},
{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.23"},
{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.23"},
{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.22"},
{"image":"ghcr.io/github/github-mcp-server:v0.32.0"},
{"image":"node:lts-alpine"}
]
}
The same tags appear in the generated docker run command lines and download_docker_images.sh invocation inside the lock file.
Why this matters
- Floating tags:
node:lts-alpine moves with every Node.js LTS release. Supply-chain compromise of that tag would silently propagate to every gh-aw consumer on next workflow run.
- Mutable version tags:
ghcr.io/github/gh-aw-firewall/agent:0.25.23 could in principle be re-pushed to a different digest after publication. Consumers have no way to detect this.
- Asymmetry with
actions-lock.json: gh-aw already pins GitHub Actions to specific SHAs in actions-lock.json (we see actions/checkout@de0fac2e... etc. in lock files). The same discipline should apply to containers.
Request
Add digest pinning for containers in one of the following ways, in decreasing order of preference:
Option 1 — Native digest resolution at compile time
gh aw compile resolves each container tag to its current digest (via registry HEAD request / docker manifest inspect semantics) and emits image@sha256:<digest> everywhere in the lock file and manifest. A containers-lock.json (analogous to actions-lock.json) records the resolved digests so subsequent compiles on the same gh-aw version produce reproducible locks.
Option 2 — Opt-in pinning
Add a top-level containers: frontmatter directive or a gh aw pin-containers subcommand that performs digest resolution once and persists the result. Users who opt in get deterministic, auditable containers.
Option 3 — At minimum, stop using floating tags
Replace node:lts-alpine with a specific version tag (e.g., node:22-alpine3.20 or node:22.11.0-alpine3.20). This does not fix mutable-tag risk but removes the worst case (an LTS flip silently pulling a major-version change).
Threat model context
Consumers running gh-aw workflows on GitHub-hosted ephemeral runners have limited blast radius per run, but:
- Secrets (the workflow's
GITHUB_TOKEN, PATs, OIDC tokens) are available inside the containers
/var/run/docker.sock is mounted into the MCP gateway (see -v /var/run/docker.sock:/var/run/docker.sock in the compiled docker run command), giving a compromised container full daemon control on the runner
- Any repo with write scopes on its workflow token would allow code push / release manipulation
Related prior discussion
I searched digest and pin in this repo's issues and saw several closed as "not planned". If there is an existing decision record explaining the tradeoff, a pointer in the frontmatter docs would help downstream security audits. (We tried to find one and could not.)
Our plan
We are documenting this gap in our internal security audit and accepting the residual risk for now, pending upstream direction. Happy to contribute a PR if one of the options above is acceptable.
Summary
Compiled
.lock.ymlworkflows produced bygh aw compilereference container images by mutable tag, never by immutable digest. This includes both version-tagged and floating tags.What we see today
A compiled lock file (e.g.,
review-responder.lock.yml) contains agh-aw-manifestheader listing six containers, referenced by tag only:{ "containers": [ {"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.23"}, {"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.23"}, {"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.23"}, {"image":"ghcr.io/github/gh-aw-mcpg:v0.2.22"}, {"image":"ghcr.io/github/github-mcp-server:v0.32.0"}, {"image":"node:lts-alpine"} ] }The same tags appear in the generated
docker runcommand lines anddownload_docker_images.shinvocation inside the lock file.Why this matters
node:lts-alpinemoves with every Node.js LTS release. Supply-chain compromise of that tag would silently propagate to every gh-aw consumer on next workflow run.ghcr.io/github/gh-aw-firewall/agent:0.25.23could in principle be re-pushed to a different digest after publication. Consumers have no way to detect this.actions-lock.json: gh-aw already pins GitHub Actions to specific SHAs inactions-lock.json(we seeactions/checkout@de0fac2e...etc. in lock files). The same discipline should apply to containers.Request
Add digest pinning for containers in one of the following ways, in decreasing order of preference:
Option 1 — Native digest resolution at compile time
gh aw compileresolves each container tag to its current digest (via registry HEAD request /docker manifest inspectsemantics) and emitsimage@sha256:<digest>everywhere in the lock file and manifest. Acontainers-lock.json(analogous toactions-lock.json) records the resolved digests so subsequent compiles on the same gh-aw version produce reproducible locks.Option 2 — Opt-in pinning
Add a top-level
containers:frontmatter directive or agh aw pin-containerssubcommand that performs digest resolution once and persists the result. Users who opt in get deterministic, auditable containers.Option 3 — At minimum, stop using floating tags
Replace
node:lts-alpinewith a specific version tag (e.g.,node:22-alpine3.20ornode:22.11.0-alpine3.20). This does not fix mutable-tag risk but removes the worst case (an LTS flip silently pulling a major-version change).Threat model context
Consumers running gh-aw workflows on GitHub-hosted ephemeral runners have limited blast radius per run, but:
GITHUB_TOKEN, PATs, OIDC tokens) are available inside the containers/var/run/docker.sockis mounted into the MCP gateway (see-v /var/run/docker.sock:/var/run/docker.sockin the compileddocker runcommand), giving a compromised container full daemon control on the runnerRelated prior discussion
I searched
digestandpinin this repo's issues and saw several closed as "not planned". If there is an existing decision record explaining the tradeoff, a pointer in the frontmatter docs would help downstream security audits. (We tried to find one and could not.)Our plan
We are documenting this gap in our internal security audit and accepting the residual risk for now, pending upstream direction. Happy to contribute a PR if one of the options above is acceptable.