Files
flan 67d6e9154d 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.
2026-07-19 19:50:50 +00:00

125 lines
6.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# One-line ISO entrypoint. From the Arch live ISO:
# curl -fsSL https://raw.githubusercontent.com/sudolulo/arch-turnkey/master/bootstrap.sh | bash
# Installs archinstall, pulls the config, launches the guided install, then reboots.
set -euo pipefail
# Where the rest of the tree is fetched from. Defaults to the public GitHub mirror so a
# plain `curl | bash` works for anyone. Point it at any fork or your own forge with:
# ARCH_TURNKEY_BASE=https://git.example.com/you/arch-turnkey/raw/branch/master
BASE="${ARCH_TURNKEY_BASE:-https://raw.githubusercontent.com/sudolulo/arch-turnkey/master}"
# Optional site config: personal provisioning (dotfiles repo, vault, forge). Absent by
# default, so a stranger gets a clean generic install. Bake one into a custom ISO with
# iso/build.sh to keep your own install fully unattended — see docs/SITE.md.
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 ---
[ -d /sys/firmware/efi ] || { echo "!! Not booted in UEFI mode. Reboot the ISO in UEFI."; exit 1; }
lsblk -dno NAME,TYPE 2>/dev/null | awk '$2=="disk" && $1 !~ /^zram/ {f=1} END{exit !f}' \
|| { echo "!! No target disk found."; exit 1; }
# --- network: prompt + help connect if needed ---
if ! net_up; then
echo "!! No network connection."
echo " Wired usually auto-connects. For wifi, use iwctl:"
echo " device list; station <dev> scan; station <dev> get-networks; station <dev> connect <SSID>"
while ! net_up; do
read -rp ">> Type 'wifi' to open iwctl, or connect another way then press Enter: " ans <"$TTY" || true
[ "$ans" = wifi ] && iwctl <"$TTY" >"$TTY" 2>&1 || true
done
fi
echo ">> Network is up."
# archinstall + curl are normally already on the ISO; only sync the db if not
# (avoids a needless -Sy partial-sync on every run).
if ! command -v archinstall >/dev/null || ! command -v curl >/dev/null; then
echo ">> Installing archinstall..."
pacman -Sy --noconfirm --needed archinstall curl
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
echo ">> Launching archinstall."
echo " In the menu: set hostname, pick Disk -> btrfs + subvolumes + LUKS encryption,"
echo " create your user. Everything else is preset."
echo
archinstall --config /tmp/archinstall.json <"$TTY"
# Hand the site config (if any) to the installed system so firstboot can provision
# dotfiles/secrets unattended. No site.env => firstboot skips that phase entirely.
if [ -f "$SITE" ]; then
echo ">> Installing site config for first-boot provisioning..."
install -Dm600 "$SITE" /mnt/etc/arch-turnkey/site.env
else
echo ">> No site config — first boot will skip dotfiles/secret provisioning."
echo " (That's the generic path. See docs/SITE.md to provision your own.)"
fi
# --- auto-reboot into the installed system (which auto-runs firstboot) ---
echo
echo ">> Base install done. Rebooting in 10s into first-boot setup..."
echo " (Ctrl-C to stay in the live environment.)"
for i in $(seq 10 -1 1); do printf "\r %2ds " "$i"; sleep 1; done; echo
systemctl reboot