Two publish-safety gaps in the docker-manifest job added in #235 (.github/workflows/release.yml).
1. Backfilling an older tag rolls :latest and :X.Y backward
docker_backfill_tag is meant to republish an already-cut release. The manifest step applies the floating tags unconditionally:
docker buildx imagetools create \
-t "$IMAGE:$VERSION" \
-t "$IMAGE:$MAJOR_MINOR" \
-t "$IMAGE:latest" \
$digests
Nothing checks that the resolved tag is the newest release. Dispatching docker_backfill_tag=v0.5.0 while v0.6.0 is the current release re-points :latest (and :0.5) at the older 0.5.0 image, so every consumer pulling :latest silently downgrades. Since dispatch only needs write access, a write-access actor can also deliberately point :latest at known-vulnerable code. Before #235 the docker job ran only on the freshly cut (newest) release, so :latest could only advance.
The immediate v0.6.0 backfill is safe because it is the newest tag; the footgun is every later historical backfill.
Fix: on a backfill, publish only the immutable :X.Y.Z by default, and gate the moving :latest / :X.Y behind either an explicit "move floating tags" dispatch input or a semver check that the backfill version is >= the currently published latest.
2. Manifest stitches whatever is in /tmp/digests with no arity check
digests=""
for f in /tmp/digests/*; do
digests="$digests $IMAGE@sha256:$(basename "$f")"
done
docker buildx imagetools create ... $digests
needs.docker.result == 'success' guarantees both matrix legs passed today, so both digests are present. But the manifest trusts the artifact set blindly: a future third arch, a leg switched to continue-on-error, or an unexpected extra docker-digest-* artifact would publish a wrong-arity image under :latest while the job still reports success. Add a count assertion before imagetools create, e.g. [ "$(ls /tmp/digests | wc -l)" -eq 2 ] (or match the exact arch names), so an unexpected set fails loudly instead of shipping a degraded "multi-arch" manifest.
Two publish-safety gaps in the
docker-manifestjob added in #235 (.github/workflows/release.yml).1. Backfilling an older tag rolls
:latestand:X.Ybackwarddocker_backfill_tagis meant to republish an already-cut release. The manifest step applies the floating tags unconditionally:Nothing checks that the resolved tag is the newest release. Dispatching
docker_backfill_tag=v0.5.0while v0.6.0 is the current release re-points:latest(and:0.5) at the older 0.5.0 image, so every consumer pulling:latestsilently downgrades. Since dispatch only needs write access, a write-access actor can also deliberately point:latestat known-vulnerable code. Before #235 the docker job ran only on the freshly cut (newest) release, so:latestcould only advance.The immediate v0.6.0 backfill is safe because it is the newest tag; the footgun is every later historical backfill.
Fix: on a backfill, publish only the immutable
:X.Y.Zby default, and gate the moving:latest/:X.Ybehind either an explicit "move floating tags" dispatch input or a semver check that the backfill version is >= the currently published latest.2. Manifest stitches whatever is in
/tmp/digestswith no arity checkneeds.docker.result == 'success'guarantees both matrix legs passed today, so both digests are present. But the manifest trusts the artifact set blindly: a future third arch, a leg switched tocontinue-on-error, or an unexpected extradocker-digest-*artifact would publish a wrong-arity image under:latestwhile the job still reports success. Add a count assertion beforeimagetools create, e.g.[ "$(ls /tmp/digests | wc -l)" -eq 2 ](or match the exact arch names), so an unexpected set fails loudly instead of shipping a degraded "multi-arch" manifest.