Skip to main content

max / alloy

Document the answer sheet and what may never be in it
Author: Max Johnson <me@maxj.phd> · 2026-09-03 17:50 UTC
Signed with PGP, not checked
Commit: d0f6f72555bbcc2b8f9e51db31959167ef090ad2
Parent: 58428c8
1 file changed, +47 insertions, -3 deletions
M docs/IMAGE.md +47 -3
@@ -280,9 +280,53 @@
280 280 Recipe values go in ahead of anything typed on the command line, so an explicit
281 281 `--build-arg` overrides the recipe rather than racing it.
282 282
283 - What the three recipes say today: fw12 is a stock client mint and sets nothing;
284 - fw13 takes `LANGS=rust,js` and `DB=postgres16` for the build-host role; astra
285 - takes those plus `PROFILE=server` and `ARCH=aarch64`.
283 + What the three recipes say today: fw13 takes `LANGS=rust,js` and
284 + `DB=postgres16` for the build-host role; astra takes those plus
285 + `PROFILE=server` and `ARCH=aarch64`; fw12 sets no dials at all and carries only
286 + identity and answers, below.
287 +
288 + ## The answer sheet
289 +
290 + A recipe can also answer the questions `alloy install` would otherwise ask. The
291 + identity step writes them to `/usr/lib/alloy/answers.toml`, beside the baked
292 + `authorized_keys`, and `crates/alloy/src/preseed.rs` reads that file once when
293 + the wizard opens. A step the sheet answers in full is skipped.
294 +
295 + | Argument | Answers | Notes |
296 + |---|---|---|
297 + | `ALLOY_HOSTNAME` | the machine's name | also rewrites `DEFAULT_HOSTNAME`; the build asserts the two agree |
298 + | `ALLOY_USERNAME` | the account to create | the password is never here |
299 + | `ALLOY_DISK` | which disk | `single-internal` or `single-internal-nvme`; a device path is refused |
300 + | `ALLOY_ENCRYPT` | the encryption checkbox | `yes` leaves the passphrase to be typed; `no` answers the whole step |
301 + | `ALLOY_LOCATE_TIMEZONE` | the timezone lookup | both this and the hostname are needed to skip that step |
302 +
303 + Two rules govern what may go in, and neither is a preference.
304 +
305 + **No secrets, ever.** Not the account password, not the LUKS passphrase. The
306 + identity step's own comment states the invariant: the image "can be kept, copied
307 + or rebuilt without care and a leak of it costs nothing". A passphrase written
308 + into the sheet would be in every layer cache, in every `podman save`, and on
309 + every stick written from the medium. So the account and encryption steps are
310 + still shown, with everything except the secret already filled in. A fully
311 + unattended install is not what this buys, and saying otherwise would be a lie
312 + about where the secrets are.
313 +
314 + **The disk is a rule, not a path.** `single-internal-nvme` means "the one
315 + non-removable NVMe in this machine", and a rule that matches nothing, or matches
316 + two, falls back to asking and says why in the status line. A medium that erased
317 + `/dev/nvme0n1` on sight would be one wrong laptop away from erasing the wrong
318 + machine.
319 +
320 + What that leaves for fw12, whose recipe answers everything it can: the disk and
321 + hostname steps are skipped, the account step opens with the username and ssh key
322 + already in it and the password to type, and the encryption step opens with the
323 + box ticked and the passphrase to type. The review names every answer that came
324 + from the medium, because a screen whose job is to be checked has to say which
325 + lines nobody typed.
326 +
327 + `build/vmtest/install_preseeded.py` drives exactly that install in a VM, and
328 + `crates/alloy/tests/answer_sheet.rs` pins the Containerfile and the installer to
329 + the same keys and the same disk vocabulary, since neither can see the other.
286 330
287 331 ## Version fields
288 332