Require every docker image to come from compose.yaml - #167
Open
Gerrrt wants to merge 3 commits into
Open
Conversation
The one image reference left outside compose.yaml, sitting directly beneath the comment in ci.yml saying image versions are not duplicated there. It carried a tag and no digest, so it contradicted the claim in README.md and SECURITY.md that every image is pinned by tag and digest. Being outside compose.yaml is what made it unfixable in place: Dependabot watches that file, and `make pin-digests` reads that file, so a pin anywhere else is one nothing bumps and nothing digests. It also survived all three of CI's image guards — not `:latest`, not a prom/ or grafana/ prefix, not an `image:` line — which is the same gap #65 was opened about. So it moves to compose.yaml as a profile-gated stub, the way `archiver` and `renderer` already are, and ci.yml resolves it with scripts/image-for.sh. The digest is what scripts/pin-digests.sh resolved from the registry, not a hand-copied one. `make up` still starts six services. Refs #65 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The runbook for onboarding a Linux host pinned grafana/alloy by hand. It was correct on the day it was written and would have been wrong from the next Dependabot bump onward, at which point a newly onboarded host would run a different Alloy from the one the monitoring host runs. check_docs.py bans image versions in prose for exactly this reason (#73), but it only inspects inline code spans, so a version inside a fenced block was invisible to it. A runbook is a shell recipe that a human executes, and this one had no more claim to a hand-written pin than a script does. Refs #65 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI had three image checks and #65 walked past all three, because each is a pattern match and the defect was a pin that was absent rather than wrong: `make backup` ran a bare `alpine`, which has no `:latest` to grep for, no prom/ or grafana/ prefix, and is not an `image:` line in compose.yaml. Both of the pins fixed in the previous two commits got in the same way. scripts/check_image_pins.py parses instead. Every docker run/pull/create in the Makefile, scripts/*.sh, the workflows and the runbooks' fenced shell blocks must carry an argument that resolves through scripts/image-for.sh — either a variable traced to an image-for.sh assignment in the same file, or an inline substitution. Deliberately not "the image operand must be a variable". That would still pass `IMG=alpine; docker run "$IMG"`, which is the identical defect, and it would need a table of which docker flags take a value, maintained against Docker's CLI forever, where an unknown one becomes a false positive — already broken here, since the first positional in snmp-generate is a bash array. Tracing the name back to compose.yaml is what closes the hole; being a variable closes nothing. The reasoning and the known limits are in the script's docstring. Success prints the number of invocations examined. A parser that quietly stopped matching would otherwise report green having checked nothing, which is a failure mode this repository has been bitten by before. Also widens the duplication grep past the prom/ and grafana/ prefixes that let gitleaks through, and wires the check into scripts/validate.sh and a `make check-image-pins` target so local and CI stay in step (#68). Verified by reverting each of the three real violations in a scratch copy and confirming the check goes red, and by reconstructing the original bare `alpine`, the `IMG=alpine` variant, and a digest pinned outside compose.yaml. Closes #65 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 #65.
The first half of #65 was already fixed by #164 —
make backup's barealpineis gone, replaced by the profile-gated
archiverservice. This is the secondhalf: "widen the CI check so a shell recipe cannot introduce another one."
Two live violations it found
CI had three image checks and #65 walked past all three, because each is a
pattern match and the defect was a pin that was absent rather than wrong. Two
more had got in the same way and were still there:
ci.yml:23GITLEAKS_IMAGE: zricethezav/gitleaks:v8.24.0— a tag with no digest, outsidecompose.yamlwhere Dependabot cannot see it andmake pin-digestscannot re-resolve it. Sitting directly beneath the comment saying image versions are not duplicated there.docs/runbooks/add-monitored-device.md:29grafana/alloy:v1.18.1hardcoded in a fencedbashblock. Correct on the day it was written; stale from the next Dependabot bump onward, at which point a newly onboarded host runs a different Alloy from the monitoring host.check_docs.pybans versions in prose (#73) but only inspects inline code spans.Both contradicted README.md and SECURITY.md's claim that every image carries a
tag and a digest.
The rule
scripts/check_image_pins.pyparses rather than greps. Everydocker run/pull/createin the Makefile,scripts/*.sh, the workflows and therunbooks' fenced shell blocks must carry an argument that resolves through
scripts/image-for.sh— a variable traced to animage-for.shassignment inthe same file, or an inline
$(… image-for.sh …).Deliberately not "the image operand must be a variable":
IMG=alpine; docker run "$IMG", which is the identicaldefect — as is
IMG=alpine:3.22@sha256:…, since a digest outsidecompose.yamlis one nothing bumps and nothing re-resolves. Tracing the nameback is what closes those; being a variable closes nothing.
docker runflagsconsume a value, maintained against Docker's CLI forever, where an unknown one
silently becomes a false positive. It is already broken here: the first
positional in
snmp-generateis$${flags[@]}, a bash array.Reasoning and the known limits are in the script's docstring. There is no ignore
mechanism, per the
.gitleaksignoreargument incheck_docs.py.Success prints the number of invocations examined — a parser that quietly
stopped matching would otherwise report green having checked nothing.
Also
prom/andgrafana/prefixes that letgitleaks through. Verified: the only other repo-wide hit is an illustrative
version in
pin-digests.sh's header, already filtered as a comment.scripts/validate.shand amake check-image-pinstarget, so localand CI stay in step (make validate and CI have drifted apart in both directions #68).
scan-profile stub incompose.yaml, likearchiverandrenderer.make upstill starts six services. The digest is whatpin-digests.shresolved from the registry, not a hand-copied one.Verification
violations, correct line numbers, no false positives. It finds 24 invocations
across 8 files, and the count is unchanged after the fixes.
alpine;IMG=alpine; a digest pinned outsidecompose.yaml; reverting each of thethree real fixes;
docker compose runmust not match; and renamingTAR_IMAGE→Tthroughoutbackup-volumes.shto prove the trace is realand not a name allowlist.
restore-volumes.shreports 5 sites, not 6 — the quotedwarn " docker run …"advice string at L379 is correctly not treated as a command.scripts/validate.sh,yamllint --strict,shellcheck,markdownlint-cli2,check_docs.py,check_compose_health.py, andpin-digests.shall clean.command: ["version"]is a valid no-op,and the real CI invocation runs.
window between them.
🤖 Generated with Claude Code