bootstrap: verify fetched archinstall.json before running it

The config's custom-commands run as root during install, so a config poisoned
by a compromised mirror was root RCE — TLS covers the transport, not the
upstream. Verify it before the @@BASE@@ substitution:

- default: SHA-256 pinned in the script (kept in lockstep with the config);
  ARCH_TURNKEY_CONFIG_SHA256 overrides it, =skip opts out. Catches drift,
  truncation, and a mirror poisoning only the config.
- ARCH_TURNKEY_PUBKEY: require + verify a detached GPG signature
  (configs/archinstall.json.sig) against an out-of-band key — the only mode
  that resists a fully compromised upstream.

Document both knobs in the README and CHANGELOG.
This commit is contained in:
flan
2026-07-19 19:50:50 +00:00
parent c1a81da6e6
commit 67d6e9154d
3 changed files with 73 additions and 0 deletions
+8
View File
@@ -30,6 +30,14 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning: [S
- **The README advertised ISO downloads from a Releases page that did not, and could not, exist.**
It now tells you to build the ISO with `iso/build.sh` and explains why no prebuilt image is
published.
### Security
- **`bootstrap.sh` now verifies the fetched `archinstall.json` before running it.** Its
custom-commands execute as root during install, so a config poisoned by a compromised mirror was
root RCE at install time — TLS authenticates the transport, not the upstream. The config is
checked against a SHA-256 pinned in the script by default; `ARCH_TURNKEY_CONFIG_SHA256` overrides
it (`=skip` opts out), and `ARCH_TURNKEY_PUBKEY` switches to detached GPG-signature verification
(`configs/archinstall.json.sig`), the only mode that resists a fully compromised upstream.
- Docs no longer reference the maintainer's forge, private dotfiles, or personal username:
`docs/CI.md` uses a generic forge URL, `docs/nginx-redirect.conf` is an explicit template,
`docs/INSTALL.md` no longer hardcodes a user or point at a private recovery doc, and the ISO
+14
View File
@@ -107,6 +107,20 @@ ISO (`iso/build.sh`) for a near-unattended install or drop it at `/root/site.env
ISO. Set `ARCH_TURNKEY_BASE` if you want the install to fetch from your own forge rather than
this repo's GitHub mirror. Full details: **[docs/SITE.md](docs/SITE.md)**.
### Config integrity
`bootstrap.sh` verifies the fetched `archinstall.json` before running it — its
custom-commands execute as root during install, so a poisoned config from a compromised
mirror would be root RCE. By default it checks the config against a SHA-256 pinned in the
script (kept in lockstep with the file). Two knobs:
- `ARCH_TURNKEY_CONFIG_SHA256=<sha>` — verify against your own hash (needed if you point
`ARCH_TURNKEY_BASE` at a fork with a different config); `=skip` opts out.
- `ARCH_TURNKEY_PUBKEY=/path/to/pubkey` — require and verify a detached GPG signature
(`configs/archinstall.json.sig`) instead. This is the only option that resists a fully
compromised upstream, since the key is yours and supplied out of band (bake it into a
custom ISO). Sign a config with `gpg --detach-sign configs/archinstall.json`.
## Requirements
- UEFI machine, current Arch ISO
+51
View File
@@ -17,6 +17,54 @@ SITE="${ARCH_TURNKEY_SITE:-/root/site.env}"
# interactive I/O must come from the console, since this runs via `curl | bash`
TTY=/dev/tty
# Integrity of the fetched config. TLS authenticates the transport, not the upstream:
# a compromised mirror or branch could serve a poisoned archinstall.json whose
# custom-commands run as root during install. Verify it against a trust anchor first.
# ARCH_TURNKEY_PUBKEY=/path/to/key -> require + verify a detached GPG signature
# (configs/archinstall.json.sig). Strongest: the key is yours, supplied out of
# band (e.g. baked into a custom ISO), so a mirror cannot forge it.
# otherwise the pinned SHA-256 below is checked automatically. That catches drift,
# truncation, and a mirror poisoning *only* the config -- but NOT a fully
# compromised BASE, which would also rewrite this script. Point BASE at a fork?
# set ARCH_TURNKEY_CONFIG_SHA256=<sha> (the mismatch message prints it), or
# ARCH_TURNKEY_CONFIG_SHA256=skip to opt out. Keep this pin in lockstep with
# configs/archinstall.json -- change one, change the other in the same commit.
CONFIG_SHA256_PINNED="2f556dea0b98e1b07feed85ba6481c36421c41b88bbc0308e9d1f2beb62e9f30"
verify_config() {
local f="$1"
if [ -n "${ARCH_TURNKEY_PUBKEY:-}" ]; then
echo ">> Verifying config signature (key: $ARCH_TURNKEY_PUBKEY)..."
command -v gpg >/dev/null || { echo "!! gpg unavailable to verify the signature."; exit 1; }
curl -fsSL "$BASE/configs/archinstall.json.sig" -o "$f.sig" \
|| { echo "!! No signature at $BASE/configs/archinstall.json.sig."; exit 1; }
local gnupghome; gnupghome="$(mktemp -d)"
if ! gpg --homedir "$gnupghome" --quiet --import "$ARCH_TURNKEY_PUBKEY" 2>/dev/null \
|| ! gpg --homedir "$gnupghome" --trust-model always --verify "$f.sig" "$f" 2>/dev/null; then
rm -rf "$gnupghome"
echo "!! Config signature verification FAILED. Aborting."; exit 1
fi
rm -rf "$gnupghome"
echo " signature OK."
return
fi
local want="${ARCH_TURNKEY_CONFIG_SHA256:-$CONFIG_SHA256_PINNED}"
if [ "$want" = skip ]; then
echo "!! Config integrity check SKIPPED — trusting $BASE over TLS only."
return
fi
local got; got="$(sha256sum "$f" | awk '{print $1}')"
if [ "$got" != "$want" ]; then
echo "!! archinstall.json checksum mismatch. Aborting."
echo " expected: $want"
echo " got: $got"
echo " If you changed the config or point BASE at a fork, set"
echo " ARCH_TURNKEY_CONFIG_SHA256=$got (or =skip to bypass)."
exit 1
fi
echo " config checksum OK."
}
net_up() { ping -c1 -W3 archlinux.org >/dev/null 2>&1; }
# --- preflight ---
@@ -45,6 +93,9 @@ fi
echo ">> Fetching config..."
curl -fsSL "$BASE/configs/archinstall.json" -o /tmp/archinstall.json
# Verify the fetched config BEFORE the @@BASE@@ substitution below, so the hash is
# taken over the file exactly as published.
verify_config /tmp/archinstall.json
# archinstall's custom-commands run in the installed system and cannot see our env, so
# bake the resolved BASE into them. Keeps a single source of truth for the fetch root.
sed -i "s|@@BASE@@|$BASE|g" /tmp/archinstall.json