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.
176 lines
8.1 KiB
Bash
Executable File
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 "$@"
|