Separate personal provisioning from the generic installer
build-iso / build (push) Failing after 25s

The install hardcoded one specific forge: bootstrap and the archinstall custom-commands
fetched from a private Gitea, and firstboot cloned a private dotfiles repo from it. Anyone
else cloning this got an installer that could not work and that pointed at a repo they
cannot read.

Split the two phases. The base install (LUKS + btrfs + snapper + yay) is now generic and
runs from a fresh clone with no configuration. The personal phase (vault unlock, forge key
registration, dotfiles, yadm bootstrap) reads a gitignored site.env; with none present
firstboot reports what it is skipping and leaves a complete system.

One fetch root, ARCH_TURNKEY_BASE, now feeds bootstrap, the archinstall custom-commands
(via an @@BASE@@ placeholder) and the ISO autorun, defaulting to the public mirror. iso/build.sh
bakes site.env into a custom ISO so an unattended personal install stays one boot.
This commit is contained in:
flan
2026-07-13 16:38:30 +00:00
parent 09ec9a1822
commit bd7f46a260
12 changed files with 204 additions and 25 deletions
+2
View File
@@ -0,0 +1,2 @@
# Personal site config — never commit. See configs/site.env.example / docs/SITE.md.
site.env
+20 -10
View File
@@ -13,7 +13,7 @@ auto-detection, and a two-phase flow built entirely on mature tools —
Boot the current Arch ISO, connect to the network, then run one line:
```
curl -fsSL arch.onetick.ninja | bash
curl -fsSL https://raw.githubusercontent.com/sudolulo/arch-turnkey/master/bootstrap.sh | bash
```
That preflight-checks the machine, installs `archinstall`, pulls the config, and
@@ -83,17 +83,27 @@ iso/ # archiso profile + build script (zero-touch ISO)
docs/ # detailed install guide + nginx redirect snippet
```
## Fork it
## Making it yours
Point it at your own infrastructure by editing the Gitea/dotfiles URLs in
`bootstrap.sh` and `firstboot.sh`, and set `VAULT_URL` / `VAULT_EMAIL` (or let
`firstboot` prompt). The design — archinstall + yadm classes + snapper — is
generic; only those URLs are installation-specific.
Nothing installation-specific is committed here. The install runs in two phases:
1. **Base** — archinstall: LUKS + btrfs subvolumes + snapper + yay. Generic; works from a
fresh clone with no configuration at all.
2. **Personal** — unlock a vault, register the new machine's SSH key with your forge, clone
your dotfiles, run `yadm bootstrap`. This needs *your* infrastructure, so it reads a
gitignored **`site.env`** that you supply. Without one, firstboot says what it's skipping
and leaves you a complete, clean Arch system.
Copy `configs/site.env.example` to `site.env`, fill it in, and either bake it into a custom
ISO (`iso/build.sh`) for a near-unattended install or drop it at `/root/site.env` on the live
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)**.
## Requirements
- UEFI machine, current Arch ISO
- A yadm dotfiles repo with a `~/.config/pkglist/` (common + class-* + per-host)
- Optional: a Vaultwarden item `arch-install` (password = Gitea token with
`write:public_key`; custom field `archive-pass` = dotfiles archive passphrase)
to skip all manual secret entry
- For the personal phase only (all optional — omit and you get the base install):
- A yadm dotfiles repo with a `~/.config/pkglist/` (common + class-* + per-host)
- A Vaultwarden/Bitwarden item `arch-install` (password = a forge token with
`write:public_key`; custom field `archive-pass` = dotfiles archive passphrase) to
collapse all secret entry behind one master-password prompt
+23 -1
View File
@@ -4,7 +4,16 @@
# Installs archinstall, pulls the config, launches the guided install, then reboots.
set -euo pipefail
BASE="https://git.onetick.ninja/flan/arch-turnkey/raw/branch/master"
# 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
@@ -36,6 +45,9 @@ fi
echo ">> Fetching config..."
curl -fsSL "$BASE/configs/archinstall.json" -o /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,"
@@ -43,6 +55,16 @@ 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..."
+3 -3
View File
@@ -18,9 +18,9 @@
],
"custom-commands": [
"curl -fsSL https://git.onetick.ninja/flan/arch-turnkey/raw/branch/master/firstboot.sh -o /usr/local/bin/firstboot && chmod +x /usr/local/bin/firstboot",
"curl -fsSL https://git.onetick.ninja/flan/arch-turnkey/raw/branch/master/configs/firstboot-once -o /usr/local/bin/firstboot-once && chmod +x /usr/local/bin/firstboot-once",
"curl -fsSL https://git.onetick.ninja/flan/arch-turnkey/raw/branch/master/configs/firstboot.service -o /etc/systemd/system/firstboot.service",
"curl -fsSL @@BASE@@/firstboot.sh -o /usr/local/bin/firstboot && chmod +x /usr/local/bin/firstboot",
"curl -fsSL @@BASE@@/configs/firstboot-once -o /usr/local/bin/firstboot-once && chmod +x /usr/local/bin/firstboot-once",
"curl -fsSL @@BASE@@/configs/firstboot.service -o /etc/systemd/system/firstboot.service",
"systemctl enable firstboot.service"
]
}
+41
View File
@@ -0,0 +1,41 @@
# arch-turnkey site config — YOUR machine's personal provisioning. Never commit a filled-in
# copy: `site.env` is gitignored precisely so no personal infrastructure lands in this repo.
#
# Without a site.env, arch-turnkey installs a clean generic Arch system (LUKS + btrfs +
# subvolumes + snapper + yay) and stops. Everything below is the optional second phase:
# unlock a vault, register the new machine's SSH key with your forge, clone your dotfiles,
# and run `yadm bootstrap`.
#
# HOW IT REACHES THE INSTALLED SYSTEM
# bootstrap.sh copies this file to /etc/arch-turnkey/site.env (mode 0600) after
# archinstall finishes; firstboot.sh sources it on the first boot.
#
# - Unattended: bake it into a custom ISO — put site.env at iso/site.env and run
# iso/build.sh. Booting that ISO provisions the machine with no extra typing.
# - Ad hoc: drop it at /root/site.env on the live ISO before running bootstrap.sh, or
# point ARCH_TURNKEY_SITE at another path.
#
# See docs/SITE.md.
# --- dotfiles (required to enable phase 2; unset => phase 2 is skipped entirely) ---
# A yadm-managed repo containing ~/.config/pkglist/ (common + class-* + per-host).
DOTFILES_REPO="ssh://git@git.example.com:22/you/dotfiles.git"
# --- forge (used to auto-register this machine's new SSH key, so the clone above works) ---
FORGE_API="https://git.example.com/api/v1"
FORGE_SSH_HOST="git.example.com"
FORGE_SSH_PORT="22"
# --- vault (Vaultwarden/Bitwarden via rbw) ---
# Provides the forge token and the dotfiles archive passphrase from ONE master-password
# prompt. Create an item named "arch-install" with:
# password field = a forge token with scope write:public_key
# custom field = archive-pass -> your yadm encrypted-archive passphrase
# If rbw is unavailable, firstboot falls back to prompting you to paste both.
VAULT_URL="https://vault.example.com"
VAULT_EMAIL="you@example.com"
# --- optional: fetch root override ---
# Where bootstrap.sh pulls the rest of the tree from. Defaults to the public GitHub mirror.
# Point it at your own forge if you would rather not depend on GitHub at install time.
#ARCH_TURNKEY_BASE="https://git.example.com/you/arch-turnkey/raw/branch/master"
+1 -1
View File
@@ -21,7 +21,7 @@ plug in), then:
```
pacman -Sy archinstall
curl -O https://git.onetick.ninja/flan/arch-turnkey/raw/branch/master/configs/archinstall.json
curl -O https://raw.githubusercontent.com/sudolulo/arch-turnkey/master/configs/archinstall.json
archinstall --config archinstall.json
```
+67
View File
@@ -0,0 +1,67 @@
# Site config — provisioning your own machines
arch-turnkey installs in two phases:
| Phase | What it does | Needs a site config? |
|---|---|---|
| 1 · Base | archinstall: LUKS + btrfs subvolumes + snapper + yay | no |
| 2 · Personal | unlock vault → register SSH key with your forge → clone dotfiles → `yadm bootstrap` | **yes** |
Phase 1 is generic and works for anyone straight from a clone. Phase 2 is inherently
personal — it needs *your* forge, *your* vault, *your* dotfiles — so none of it is baked
into this repo. Instead it reads a **`site.env`** that you supply. With no `site.env`,
firstboot prints what it's skipping and stops with a complete, clean Arch system.
`site.env` is gitignored. It should never be committed.
## Setting one up
Copy the template and fill it in:
```
cp configs/site.env.example site.env
$EDITOR site.env
```
You need, at minimum, `DOTFILES_REPO` — a yadm repo carrying `~/.config/pkglist/`
(`common` + `class-*` + per-host). Unset, phase 2 is skipped entirely. The forge variables
exist so the freshly-generated SSH key on the new machine can be registered with your forge
*before* the dotfiles clone is attempted, and the vault variables collapse the two secrets
phase 2 needs (a forge token, the archive passphrase) behind a single master-password prompt.
## Getting it onto the machine
`bootstrap.sh` installs `site.env` to `/etc/arch-turnkey/site.env` (mode 0600) once
archinstall finishes; `firstboot.sh` sources it on first boot. Three ways to get it there:
**Unattended — bake it into an ISO.** Put it at `iso/site.env` and build:
```
cd iso && ./build.sh
```
Booting that ISO auto-runs bootstrap, which finds the config at `/root/site.env`. This is
the near-zero-typing path: boot, answer archinstall's disk/user prompts, enter your vault
master password once, done.
**Ad hoc — drop it on the live ISO.** Before running bootstrap, write it to `/root/site.env`.
**Anywhere else.** Point `ARCH_TURNKEY_SITE` at it:
```
ARCH_TURNKEY_SITE=/mnt/usb/site.env bash bootstrap.sh
```
## Not depending on GitHub at install time
`bootstrap.sh` fetches the rest of the tree from the public GitHub mirror by default, so a
plain `curl | bash` works for anyone. If you'd rather the install pull from your own forge,
set `ARCH_TURNKEY_BASE` (in `site.env` or the environment):
```
ARCH_TURNKEY_BASE="https://git.example.com/you/arch-turnkey/raw/branch/master"
```
Everything downstream — the archinstall config's `custom-commands`, `firstboot.sh`, the
systemd unit — is fetched relative to that one root, so a single variable re-points the
whole install at a mirror you control.
+1 -1
View File
@@ -5,5 +5,5 @@
# nginx -t && systemctl reload nginx
location = /arch {
return 302 https://git.onetick.ninja/flan/arch-turnkey/raw/branch/master/bootstrap.sh;
return 302 https://raw.githubusercontent.com/sudolulo/arch-turnkey/master/bootstrap.sh;
}
+30 -7
View File
@@ -10,12 +10,20 @@
# Fallback: if rbw/Vaultwarden is unavailable, it drops back to manual paste.
set -euo pipefail
DOTFILES="ssh://git@git.onetick.ninja:55214/flan/dotfiles.git"
GITEA_API="https://git.onetick.ninja/api/v1"
GITEA_SSH_HOST="git.onetick.ninja"
GITEA_SSH_PORT=55214
# Vaultwarden URL + account email — set via env, else prompted (no PII in the repo)
VAULT_URL="${VAULT_URL:-https://pass.onetick.ninja}"
# 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
@@ -23,7 +31,8 @@ exec > >(tee -a "$LOG") 2>&1 # QoL: log everything
# populated as we go
TOKEN="" ARCHIVE_PASS="" CLASS="" HOST=""
net_up() { ping -c1 -W3 "$GITEA_SSH_HOST" >/dev/null 2>&1; }
# 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() {
@@ -140,6 +149,20 @@ main() {
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
+3
View File
@@ -1,2 +1,5 @@
out/
*.iso
# Personal site config — never commit (see configs/site.env.example)
site.env
+1 -1
View File
@@ -10,5 +10,5 @@ if [ "$(tty)" = /dev/tty1 ]; then
echo
printf "Press Enter to install, or Ctrl-C for a shell... "
read -r _ || true
curl -fsSL https://git.onetick.ninja/flan/arch-turnkey/raw/branch/master/bootstrap.sh | bash
curl -fsSL https://raw.githubusercontent.com/sudolulo/arch-turnkey/master/bootstrap.sh | bash
fi
+12 -1
View File
@@ -17,12 +17,23 @@ rm -rf "$WORK" "$PROFILE"
cp -r /usr/share/archiso/configs/releng "$PROFILE"
install -Dm755 airootfs/root/.zprofile "$PROFILE/airootfs/root/.zprofile"
# Bake in the personal site config if there is one (iso/site.env, gitignored). bootstrap.sh
# picks it up at /root/site.env, so booting this ISO provisions dotfiles + secrets with no
# extra typing. Without it the ISO is a clean generic installer — which is what anyone
# building from a fresh clone gets. See docs/SITE.md.
if [ -f site.env ]; then
install -Dm600 site.env "$PROFILE/airootfs/root/site.env"
echo ">> Baked site.env into the ISO — personal provisioning enabled."
else
echo ">> No iso/site.env — building a generic ISO (no personal provisioning)."
fi
# Distinct branding so it's clearly the turnkey ISO, not a stock Arch one.
LABEL="TURNKEY_$(date +%Y%m)" # date-stamped ISO9660 volume label, injected as a literal
sed -i \
-e 's/^iso_name=.*/iso_name="arch-turnkey"/' \
-e "s/^iso_label=.*/iso_label=\"$LABEL\"/" \
-e 's/^iso_publisher=.*/iso_publisher="flan <https:\/\/git.onetick.ninja\/flan\/arch-turnkey>"/' \
-e 's/^iso_publisher=.*/iso_publisher="flan <https:\/\/github.com\/sudolulo\/arch-turnkey>"/' \
-e 's/^iso_application=.*/iso_application="arch-turnkey installer"/' \
"$PROFILE/profiledef.sh"