apk packages installed by script/bootstrap are not hash-pinned #19
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
REPO_POLICIES.md: "ALL external references must be pinned by cryptographichash. This includes Docker base images, Go modules, npm packages, GitHub
Actions, and anything else fetched from a remote source… There are zero
exceptions to this rule."
The
Dockerfile's base image is correctly pinned:But the very next thing it does is
RUN script/bootstrap, which fetchespackages over the network at build time:
for
git,make,hugo,nodejs, andnpm. Those resolve against alpine'spackage repository whenever the build runs. Pinning the base image layer does
not pin what gets installed into it — the digest fixes the starting filesystem,
not the packages fetched afterward. So the actual toolchain that builds and
publishes lora.vegas is unpinned, which is the exact class of exposure the
policy exists to close.
The same applies to the other three package managers
script/bootstrapsupports (
nix-env -iA,apt-get install -y,brew install) and toscript/fmt/script/fmt-check, which fetch prettier at run time vianpx --yes prettier@3.4.2— that one at least pins an exact version, though not ahash.
How this surfaced
Found while reviewing #7 / PR #17. It predates that PR and was explicitly kept
out of its scope. PR #17 is about to add
nodejsandgitto the deployworkflow's build container via
apk add, which widens the same unpinnedsurface — another reason to settle the policy here.
A second, related consequence
Alpine 3.21's package repository serves Hugo 0.139.0. Current Hugo is
0.164.0. So CI is green on a Hugo roughly two years behind, and #18 documents a
deprecation warning that current Hugo emits but CI cannot see. Whatever is
decided here should also decide how the Hugo version gets chosen deliberately
rather than inherited from whichever alpine release the base digest happens to
be.
This needs a decision before implementation
There is no clean, low-cost way to hash-pin apk packages, and the options
differ a lot in cost and durability. I do not think an implementer should pick
one unilaterally.
Option A — pin apk package versions (not hashes).
apk add hugo=0.139.0-r0etc. Cheap, one line per package. Weaknesses: alpine garbagecollects old package versions from the repo, so builds break when a pinned
version is dropped; and a version is not a hash, so this is compliance in
spirit, not to the letter.
Option B — install Hugo from a hash-verified upstream release archive. This
is the pattern
REPO_POLICIES.mdalready prescribes for exactly this problem:"download a specific release archive from GitHub, verify its hash (hardcoded in
the Dockerfile or script), and only then install." Fully compliant, pins the
Hugo version deliberately, and fixes the stale-Hugo problem in the same stroke.
Costs: more code in
script/bootstrap, and the hash must be updated by hand onevery Hugo bump. Would apply to Hugo specifically;
git/make/nodejswouldstill come from apk.
Option C — build a base image once, pin it by digest, and use it
everywhere. Move the whole toolchain install into a separately built and
published image, then reference it by digest from both the
Dockerfileand theworkflows. Strongest guarantee and the fastest CI, but it introduces a second
artifact to build, publish, and keep current — real infrastructure for a
five-page static site.
Option D — accept the gap and record it. Document in
REPO_POLICIES.md(ora repo-local note) that distro packages atop a pinned base image are considered
acceptable, and close this. Legitimate if the judgement is that a pinned alpine
digest plus a trusted distro repo is enough. It should be an explicit recorded
decision rather than a silent gap.
My recommendation: Option B for Hugo, Option D for the rest. Hugo is the
tool that actually produces the published artifact and the one whose version
drift is already causing problems (#18), so it earns a real pin.
git,make,and
nodejsare build-time conveniences whose behaviour does not affectoutput, and chasing hashes for them is poor value. That combination is
defensible, cheap, and fixes the stale-Hugo issue.
Definition of done (once a direction is chosen)
script/bootstrap, covering the apkpath at minimum; state explicitly whether the nix/apt/brew paths are also
covered or deliberately left out.
# <name> <version>, YYYY-MM-DDcomment in the canonical format, and the download fails closedon hash mismatch.
rather than being an emergent property of the base image.
closed referencing that text.
make checkpasses andscript/cibuildsucceeds.TODO.mdupdated.Explicitly out of scope
0.139.0 changes rendered output, that belongs in its own issue with its own
before/after verification.
languageCodedeprecation) — related, tracked separately.Assigning to @sneak for the direction.
Decision from @sneak: apk does not need pinning. Closing as Option D —
accepted and recorded.
This overrides my recommendation, which was Option B (hash-verified upstream
Hugo release archive) for Hugo with Option D for the rest. Recording the
decision as given rather than the one I proposed.
What is now settled policy for this repo
Distro packages installed on top of a digest-pinned base image are acceptable
and are not treated as a violation of the "ALL external references must be
pinned by cryptographic hash" rule. Concretely,
script/bootstrapmay continueto install
git,make,hugo,nodejs, andnpmviaapk add(and viathe equivalent nix/apt/brew paths) without version or hash pinning.
The reasoning that supports this, for anyone reading later: the base image is
pinned by digest, the package repository is the distribution's own signed
index, and apk verifies package signatures against keys baked into that pinned
image. That is a materially different trust position from
curl | bashor anunpinned third-party container tag, which is what the policy rule is actually
aimed at. The policy's own worked example — "download a specific release
archive from GitHub, verify its hash, and only then install" — is about
untrusted third-party artifacts, not distribution packages.
No code change is required. Nothing to implement.
One consequence that remains open, deliberately not reopened here
Accepting unpinned apk means the Hugo version is an inherited variable, not a
chosen one. Alpine 3.21 currently serves Hugo 0.139.0; upstream current is
0.164.0. CI therefore builds the site on a Hugo roughly two years behind
whatever a developer runs locally.
That is not a pinning problem and is out of scope for this decision, but it has
one live effect already tracked: #18 exists because a current Hugo emits a
languageCodedeprecation warning that CI's older Hugo cannot see. So the repocan be green in CI while a developer sees warnings locally, and the reverse is
also possible — a future Hugo could change rendering in a way CI would not
catch until the base image moves.
I am not filing a follow-up for this unless you want one. If you would like
the Hugo version to be a deliberate choice rather than a side effect of which
alpine digest is pinned, say so and I will file it; it would be a small issue
(pin a Hugo version explicitly, or bump the base image on a schedule). Left
alone otherwise — this is exactly the kind of thing that should not be
manufactured into work without being asked for.
Closing.
Follow-up decision from @sneak, narrowing this issue's resolution: the Hugo
version must be deliberate — container or
go installby hash, not distroHugo.
This issue's outcome stands for
git,make,nodejs, andnpm: apk on adigest-pinned base is fine for build-time conveniences. Hugo is carved out,
because it produces the published artifact, so its version is a property of the
output rather than of the build environment.
Filed as #26, which also records that standard (non-extended) Hugo is
sufficient here — verified: no SCSS, no asset pipeline, no image processing;
CSS is inlined via
readFile. That matters because it makesgo installviable at all, since Hugo extended requires CGO.
This issue stays closed; #26 carries the work.