Files
flan c1a81da6e6 Silence SC2024 on the nmtui redirect, with the reason recorded
shellcheck flags 'sudo doesn't affect redirects', which matters when redirecting into a
root-owned file. /dev/tty is the invoking user's own controlling terminal and the redirect
is what gives nmtui a terminal at all, so the warning does not apply here.

Also drop a stale message referencing a forge that a generic install has no knowledge of.
2026-07-13 17:14:08 +00:00

176 lines
8.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# Phase 2 launcher — run once after the archinstall base + first reboot.
# Pulls secrets from Vaultwarden (via rbw) so there is ONE prompt: your vault
# master password. It then auto-registers this machine's SSH key with Gitea,
# clones your dotfiles, decrypts the archive, and runs `yadm bootstrap`.
#
# Vaultwarden setup (once): create an item named "arch-install" with
# - password field = a Gitea token with scope write:public_key
# - custom field "archive-pass" = the yadm encrypted-archive passphrase
# Fallback: if rbw/Vaultwarden is unavailable, it drops back to manual paste.
set -euo pipefail
# Site config — YOUR forge, dotfiles and vault. Never committed: it is written to the
# installed system by bootstrap.sh from a site.env you supply (see docs/SITE.md), so this
# repo carries no personal infrastructure. Absent => the whole phase-2 provisioning block
# below is skipped and you get a clean generic Arch install.
SITE_CONF="${SITE_CONF:-/etc/arch-turnkey/site.env}"
# shellcheck source=/dev/null
[ -r "$SITE_CONF" ] && . "$SITE_CONF"
DOTFILES="${DOTFILES_REPO:-}" # yadm repo; empty => skip provisioning
GITEA_API="${FORGE_API:-}" # e.g. https://git.example.com/api/v1
GITEA_SSH_HOST="${FORGE_SSH_HOST:-}"
GITEA_SSH_PORT="${FORGE_SSH_PORT:-22}"
# Vaultwarden URL + account email — set via site.env/env, else prompted (no PII in the repo)
VAULT_URL="${VAULT_URL:-}"
VAULT_EMAIL="${VAULT_EMAIL:-}"
LOG="$HOME/firstboot.log" # user-writable (firstboot runs as the user)
exec > >(tee -a "$LOG") 2>&1 # QoL: log everything
# populated as we go
TOKEN="" ARCHIVE_PASS="" CLASS="" HOST=""
# Reach for the configured forge when there is one; otherwise just prove we have internet.
net_up() { ping -c1 -W3 "${GITEA_SSH_HOST:-archlinux.org}" >/dev/null 2>&1; }
# --- network: prompt + help connect if needed (nmtui, since NetworkManager is installed) ---
require_network() {
net_up && return
echo "No network yet."
local ans
while ! net_up; do
read -rp ">> Type 'net' to open nmtui, or connect another way then press Enter: " ans </dev/tty || true
# SC2024 warns that sudo doesn't apply to redirects — which matters when redirecting
# into a root-owned file. /dev/tty is this user's own controlling terminal, and the
# redirect exists to hand nmtui a terminal at all. Not applicable.
# shellcheck disable=SC2024
[ "$ans" = net ] && sudo nmtui </dev/tty >/dev/tty 2>&1 || true
done
}
# --- machine class: echo the best guess (chassis, else battery presence) ---
# chassis reports vm/container/embedded/unknown on VMs+containers, so for anything
# that isn't a clear physical form-factor, fall back on whether a battery exists.
detect_class() {
case "$(hostnamectl chassis 2>/dev/null)" in
handset|tablet|convertible|watch) echo handheld ;;
laptop) echo laptop ;;
desktop|server) echo desktop ;;
*) ls /sys/class/power_supply/BAT* >/dev/null 2>&1 && echo laptop || echo desktop ;;
esac
}
# --- yay (AUR helper) — the normal yay package, built from its AUR PKGBUILD.
# makepkg -sir pulls the prebuilt `go` makedepend to compile yay, then --rmdeps
# removes go afterward so the toolchain doesn't linger on the fresh install. A build
# failure is non-fatal: rbw just won't be available and we fall back to manual entry.
install_yay() {
command -v yay >/dev/null && return
echo ">> Building yay from the AUR..."
local tmp; tmp=$(mktemp -d)
if git clone -q https://aur.archlinux.org/yay.git "$tmp/yay" \
&& ( cd "$tmp/yay" && makepkg -sir --noconfirm ); then
echo " yay installed."
else
echo " !! yay build failed — AUR packages unavailable; using manual fallback."
fi
rm -rf "$tmp"
}
# --- secrets: try Vaultwarden (rbw), else leave TOKEN/ARCHIVE_PASS empty (manual path) ---
load_secrets() {
yay -S --needed --noconfirm rbw 2>/dev/null || return
# terminal pinentry (pinentry is installed via archinstall.json) — no GUI exists on
# a fresh install, so the default graphical pinentry would fail; use a TTY prompt.
rbw config set pinentry pinentry-tty
[ -n "$VAULT_EMAIL" ] || read -rp "Vaultwarden email: " VAULT_EMAIL </dev/tty
rbw config set base_url "$VAULT_URL"
rbw config set email "$VAULT_EMAIL"
echo ">> Unlock Vaultwarden — enter your master password (one prompt for everything):"
rbw login && rbw unlock || return
TOKEN="$(rbw get arch-install 2>/dev/null || true)"
ARCHIVE_PASS="$(rbw get --field archive-pass arch-install 2>/dev/null || true)"
}
# --- ssh key: generate, register with Gitea (API if we have a token, else paste) ---
register_ssh_key() {
local key="$HOME/.ssh/id_ed25519"
mkdir -p "$HOME/.ssh"; chmod 700 "$HOME/.ssh"
[ -f "$key" ] || ssh-keygen -t ed25519 -N "" -f "$key"
if [ -n "$TOKEN" ]; then
echo ">> Registering SSH key with Gitea via API..."
curl -fsS -X POST "$GITEA_API/user/keys" \
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d "{\"title\":\"$HOST\",\"key\":\"$(cat "$key.pub")\"}" >/dev/null \
&& echo " registered." || echo " (key may already exist — continuing)"
else
echo ">> Add this key to Gitea (Settings > SSH Keys), then press Enter:"
cat "$key.pub"; read -r _ </dev/tty || true
fi
ssh-keygen -F "[$GITEA_SSH_HOST]:$GITEA_SSH_PORT" -f "$HOME/.ssh/known_hosts" >/dev/null 2>&1 \
|| ssh-keyscan -p "$GITEA_SSH_PORT" "$GITEA_SSH_HOST" >> "$HOME/.ssh/known_hosts" 2>/dev/null || true
}
# --- pull dotfiles + decrypt + bootstrap (yadm is installed via archinstall.json) ---
pull_dotfiles() {
# Clear default /etc/skel dotfiles the repo tracks — yadm clone won't overwrite
# existing untracked files, so its checkout would silently skip them.
rm -f "$HOME"/.bash_profile "$HOME"/.bashrc "$HOME"/.bash_logout 2>/dev/null
if yadm clone "$DOTFILES"; then
yadm checkout "$HOME" 2>/dev/null || true # ensure tracked files are applied
else
yadm pull
fi
yadm config local.class "$CLASS"
local archive="$HOME/.local/share/yadm/archive"
if [ -n "$ARCHIVE_PASS" ] && [ -f "$archive" ]; then
echo ">> Decrypting archive (from Vaultwarden, no prompt)..."
if printf '%s' "$ARCHIVE_PASS" | gpg --batch --pinentry-mode loopback --passphrase-fd 0 \
-d "$archive" 2>/dev/null | tar -C "$HOME" -xf -; then
echo " done."
else
echo " Vaultwarden passphrase failed — falling back to manual decrypt."
yadm decrypt || true
fi
else
yadm decrypt || true # interactive passphrase
fi
yadm bootstrap
}
main() {
echo "== firstboot $(date -u 2>/dev/null || true) =="
require_network
echo ">> Network is up."
local default_class; default_class="$(detect_class)"
read -rp "Machine class [$default_class] (laptop/desktop/handheld): " CLASS </dev/tty || true
CLASS="${CLASS:-$default_class}"
local hn; read -rp "Hostname [$(hostnamectl hostname)]: " hn </dev/tty || true
[ -n "${hn:-}" ] && sudo hostnamectl set-hostname "$hn"
HOST="$(hostnamectl hostname)"
install_yay
# Phase 2 is personal: unlock a vault, register this key with your forge, clone your
# dotfiles. It only runs when a site.env named a dotfiles repo. Without one the generic
# install is already complete, so stop here rather than prompting for infrastructure
# the operator does not have.
if [ -z "$DOTFILES" ]; then
echo
echo ">> No site config — skipping dotfiles/secret provisioning."
echo " The base system is complete: LUKS + btrfs + snapper + yay, class=$CLASS."
echo " To provision your own dotfiles and secrets, see docs/SITE.md."
echo "== Done. Class=$CLASS Host=$HOST. Reboot into your session. Log: $LOG =="
return
fi
load_secrets
register_ssh_key
pull_dotfiles
echo "== Done. Class=$CLASS Host=$HOST. Reboot into your session. Log: $LOG =="
}
main "$@"