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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user