apk packages installed by script/bootstrap are not hash-pinned #19

Closed
opened 2026-08-09 04:02:05 +02:00 by clawbot · 2 comments
Collaborator

Problem

REPO_POLICIES.md: "ALL external references must be pinned by cryptographic
hash.
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:

# alpine 3.21, 2026-02-28
FROM alpine@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709

But the very next thing it does is RUN script/bootstrap, which fetches
packages over the network at build time:

apk) apk add --no-cache "$4" ;;

for git, make, hugo, nodejs, and npm. Those resolve against alpine's
package 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/bootstrap
supports (nix-env -iA, apt-get install -y, brew install) and to
script/fmt/script/fmt-check, which fetch prettier at run time via npx --yes prettier@3.4.2 — that one at least pins an exact version, though not a
hash.

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 nodejs and git to the deploy
workflow's build container via apk add, which widens the same unpinned
surface — another reason to settle the policy here.

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-r0 etc. Cheap, one line per package. Weaknesses: alpine garbage
collects 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.md already 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 on
every Hugo bump. Would apply to Hugo specifically; git/make/nodejs would
still 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 Dockerfile and the
workflows. 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 (or
a 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 nodejs are build-time conveniences whose behaviour does not affect
output, 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)

  1. The chosen approach is implemented in script/bootstrap, covering the apk
    path at minimum; state explicitly whether the nix/apt/brew paths are also
    covered or deliberately left out.
  2. If a hash is hardcoded, it is accompanied by a # <name> <version>, YYYY-MM-DD comment in the canonical format, and the download fails closed
    on hash mismatch.
  3. The Hugo version used by CI is stated explicitly somewhere in the repo
    rather than being an emergent property of the base image.
  4. If Option D, the decision and its rationale are written down; this issue is
    closed referencing that text.
  5. make check passes and script/cibuild succeeds.
  6. TODO.md updated.

Explicitly out of scope

  • Bumping the Hugo version itself as a behavioural change. If moving off
    0.139.0 changes rendered output, that belongs in its own issue with its own
    before/after verification.
  • #18 (the languageCode deprecation) — related, tracked separately.

Assigning to @sneak for the direction.

## Problem `REPO_POLICIES.md`: "**ALL external references must be pinned by cryptographic hash.** 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: ``` # alpine 3.21, 2026-02-28 FROM alpine@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709 ``` But the very next thing it does is `RUN script/bootstrap`, which fetches packages over the network at build time: ```sh apk) apk add --no-cache "$4" ;; ``` for `git`, `make`, `hugo`, `nodejs`, and `npm`. Those resolve against alpine's package 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/bootstrap` supports (`nix-env -iA`, `apt-get install -y`, `brew install`) and to `script/fmt`/`script/fmt-check`, which fetch prettier at run time via `npx --yes prettier@3.4.2` — that one at least pins an exact version, though not a hash. ## 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 `nodejs` and `git` to the deploy workflow's build container via `apk add`, which widens the same unpinned surface — 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-r0` etc. Cheap, one line per package. Weaknesses: alpine garbage collects 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.md` already 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 on every Hugo bump. Would apply to Hugo specifically; `git`/`make`/`nodejs` would still 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 `Dockerfile` and the workflows. 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` (or a 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 `nodejs` are build-time conveniences whose behaviour does not affect output, 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) 1. The chosen approach is implemented in `script/bootstrap`, covering the apk path at minimum; state explicitly whether the nix/apt/brew paths are also covered or deliberately left out. 2. If a hash is hardcoded, it is accompanied by a `# <name> <version>, YYYY-MM-DD` comment in the canonical format, and the download fails closed on hash mismatch. 3. The Hugo version used by CI is stated explicitly somewhere in the repo rather than being an emergent property of the base image. 4. If Option D, the decision and its rationale are written down; this issue is closed referencing that text. 5. `make check` passes and `script/cibuild` succeeds. 6. `TODO.md` updated. ## Explicitly out of scope - Bumping the Hugo version itself as a behavioural change. If moving off 0.139.0 changes rendered output, that belongs in its own issue with its own before/after verification. - #18 (the `languageCode` deprecation) — related, tracked separately. Assigning to @sneak for the direction.
sneak was assigned by clawbot 2026-08-09 04:02:08 +02:00
Author
Collaborator

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/bootstrap may continue
to install git, make, hugo, nodejs, and npm via apk add (and via
the 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 | bash or an
unpinned 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
languageCode deprecation warning that CI's older Hugo cannot see. So the repo
can 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.

**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/bootstrap` may continue to install `git`, `make`, `hugo`, `nodejs`, and `npm` via `apk add` (and via the 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 | bash` or an unpinned 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 `languageCode` deprecation warning that CI's older Hugo cannot see. So the repo can 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.
Author
Collaborator

Follow-up decision from @sneak, narrowing this issue's resolution: the Hugo
version must be deliberate — container or go install by hash, not distro
Hugo.

This issue's outcome stands for git, make, nodejs, and npm: apk on a
digest-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 makes go install
viable at all, since Hugo extended requires CGO.

This issue stays closed; #26 carries the work.

Follow-up decision from @sneak, narrowing this issue's resolution: **the Hugo version must be deliberate — container or `go install` by hash, not distro Hugo.** This issue's outcome stands for `git`, `make`, `nodejs`, and `npm`: apk on a digest-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 makes `go install` viable at all, since Hugo extended requires CGO. This issue stays closed; #26 carries the work.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/lora.vegas#19