Keep :latest patched between releases, not just at them - #211
Merged
Conversation
`:latest` only got rebuilt when a version tag was pushed, so an OS CVE landing mid-cycle sat in the published image until the next feature release. That is what happened here: v1.11.0 shipped 2026-08-22, DSA-6465-1 fixed openssl three days later, and the 08-31 scan found it. refresh.yml rebuilds the newest release tag's source against a fresh base every Monday, scans and smoke-tests the result, signs it, and only then retags it `:latest`. It builds the tag rather than main so `:latest` keeps meaning released code, and pushes a dated tag first so a bad rebuild can't move `:latest` at all. release.yml was also missing the cache guards ci.yml has, while sharing the `type=gha` cache ci.yml writes with mode=max. A release cut today could have restored a stale runtime layer and republished the same packages, which the Dockerfile comment already claimed was impossible. scan.yml's "time to rebuild against a fresh base" is now false by construction — a refresh already tried that — so it says what a surviving finding means instead, and counts distinct CVEs rather than package rows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Qzxns3a3Q12RfayHZRrGZ
AGENTS.md listed `cat:security`, which has never existed. The weekly scan creates and applies `security` + `dependencies` instead, and `idea` went undocumented too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Qzxns3a3Q12RfayHZRrGZ
A patched image nobody pulls patches nothing: refresh.yml republished `:latest` and left production on whatever it last pulled, which made `just deploy` a weekly chore. It now dispatches the same Forgejo deploy release.yml does, gated on `needs: [refresh]` so it can't fire before the rebuild has passed its scan, smoke test and signature. The README section was four ideas long for two ideas' worth of content; trimmed to the bullets and the one caveat you'd act on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Qzxns3a3Q12RfayHZRrGZ
refresh.yml deploys production now, and workflow_dispatch would have let anyone with write access trigger that — a wider door than release.yml's actor gate. Scheduled runs stay ungated on purpose: `github.actor` is the last editor of the workflow on a schedule event, so gating cron would let an unrelated edit quietly stop the weekly patch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Qzxns3a3Q12RfayHZRrGZ
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Qzxns3a3Q12RfayHZRrGZ
CodeQL flagged refresh.yml's deploy job for inheriting the default GITHUB_TOKEN scopes, and release.yml's has the same gap (open alert #1). Neither touches the GitHub API — they curl Forgejo with its own token — so the minimal grant is none at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Qzxns3a3Q12RfayHZRrGZ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #209
What was actually wrong
Not the build. The image was just older than the fix.
:latest= v1.11.0, published3.5.7-1~deb13u2)The three findings are one CVE across openssl's three binary packages.
Verified rather than assumed:
openssl/libssl3t64/openssl-provider-legacyat3.5.6-1~deb13u2.python:3.13-slimtoday yields3.5.7-1~deb13u2for all three. A rebuild clears it, with no Dockerfile change.So the CVE isn't the bug. The bug is that nothing rebuilds
:latest.The gap
release.ymltriggers onpush: tags: v*. Between releases nothing refreshes the published image, so every CVE that lands mid-cycle sits in:latestand gets an issue filed every Monday until someone happens to ship a feature.refresh.ymlRuns Mondays 04:00 UTC (two hours before the scan) and on dispatch:
main—:latesthas to keep meaning released code. Only the OS packages underneath it move.pull: true+no-cache-filters: runtime, so the base and theapt-get upgradeare genuinely fresh.refresh-<version>-<date>only.:latestnever moves./api/health, cosign-signs the digest.imagetools createretags it:latest— a retag of the manifest already in the registry, so:latestresolves to the exact digest that was scanned, tested and signed.deployjob onneeds: [refresh]dispatches the same Forgejo workflowrelease.ymldoes, so production actually moves onto the patched image.cache-fromwithoutcache-to: a weekly multi-archmode=maxexport would evict the entries PR builds actually read out of the 10GB cache.The smoke test is inlined rather than shared with
release.yml. It has to be — this job checks out an old release tag, so any repo script added after that release wouldn't exist in the workspace.Two things found on the way
release.ymlwas missing the cache guardsci.ymlhas. It usescache-from: type=ghawith nono-cache-filters: runtimeand nopull: true, while sharing the cacheci.ymlwrites withmode=max. A release cut today could restore a stale runtime layer and republish the same CVEs. The Dockerfile comment already claimed this couldn't happen ("ci.yml excludes this stage from the build cache") — now it's true of every workflow that builds the image, and the comment says so.AGENTS.mddocumented acat:securitylabel that has never existed. The scan workflow creates and appliessecurity+dependencies;ideawas undocumented too.Contract change worth knowing
:latestno longer always resolves to the newest version tag's digest — it tracks that tag's code on today's packages. Version tags themselves never move. README's Release section documents this.Duplication that's deliberate
The smoke test and the Forgejo dispatch are inlined rather than shared with
release.yml. They have to be: this job checks out an old release tag, so a composite action orbin/script added after that release simply wouldn't exist in the workspace. Sharing them would mean checking the tag out to a subpath and building withcontext: src— more subtlety than 30 duplicated lines are worth.Verification
actionlintclean onrefresh.ymlandscan.ymlprettier --checkclean repo-widescan.yml's issue-body script run under node against this issue's real Trivy rows: renders "1 HIGH/CRITICAL CVE across 3 package(s)"After merge
workflow_dispatchonly appears once the workflow is on the default branch. So merge, then run Refresh published image manually to clear:latesttoday rather than waiting for Monday.🤖 Generated with Claude Code
https://claude.ai/code/session_011Qzxns3a3Q12RfayHZRrGZ