Skip to content

Feature request: pin container images by digest in compiled lock files #27715

@microsasa

Description

@microsasa

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

  1. 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.
  2. 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.
  3. 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.

Metadata

Metadata

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions