Same class of problem as the deployedTargets/README fixes: hand-maintained prose that drifted from reality and nobody was obligated to update. - CLAUDE.md: "18 hosts" was a stale hardcoded count (actually 20); reworded to not need updating as hosts are added. Also added the missing tailscale-exit-node build type to a list that had it everywhere else in the file except one bullet. - AGENTS.md: same missing tailscale-exit-node build type. - docs/auto-installer.md: the hand-enumerated lxc-* list was missing lxc-tailscale-exit-node. - flake-target-refactor-spec.md: added a "Status: implemented" note so this completed historical spec (referenced elsewhere purely for rationale) can't be mistaken for an open plan with unresolved Open Questions. - remove-sensetive-info-refactor.md: the "Definition of done" checklist was entirely unchecked despite most of the work being done. Checked off what's actually done (sops-nix migration, history scrub just performed, the pre-commit gitleaks hook), and left rotation of the GitHub PAT found in history explicitly flagged as the one still-open item -- an operator action against GitHub, not something this repo can attest to itself. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
14 KiB
Auto-installer
This flake builds a self-contained NixOS installer environment that can
install any host exposed by its own nixosConfigurations. It was migrated
from a formerly-separate nix-auto-installer repo — everything it did now
lives here.
The installer provides a small NixOS install environment (ISO, or the same
image netbooted via PXE) with SSH access, Git support, and an interactive
installation script.
Logging in as any user (root or nixos) runs /etc/auto-install.sh,
discovers available hosts from this same flake, lets the operator choose a
target, applies that host's Disko storage configuration, installs NixOS, and
reboots.
This applies to every nixosConfigurations target except lxc-* hosts —
see "LXC hosts" immediately below for why those are different.
LXC hosts
lxc-* targets (lxc-minimal, lxc-nix-cache, lxc-server, lxc-docker,
lxc-gui, lxc-pxe-boot, lxc-tailscale-exit-node) are not installed via auto-install.sh — the
interactive menu deliberately excludes them. Don't try to select one there;
nixos-install would bind-mount / onto /mnt (LXC containers have no raw
disk to partition) and then refuse to touch the filesystem it's currently
running on — it's designed to protect exactly this case, so it just fails.
modules/platforms/lxc.nix imports nixpkgs' own
virtualisation/proxmox-lxc.nix module, which gives every lxc-* host a
config.system.build.tarball output — a complete, directly Proxmox-importable
container image, no install step at all:
nix build .#nixosConfigurations.lxc-minimal.config.system.build.tarball
This is a plain rootfs tarball, not a vzdump backup archive — restoring it
with pct restore fails ("archive contains no configuration file"), since
that command expects backup-archive metadata this tarball doesn't have. Use
it as a CT template instead: drop it under Proxmox's template storage
(conventionally /var/lib/vz/template/cache/ for the local storage, or
the GUI's "Create CT" → upload-as-template flow) and create a container
from it, supplying all config on the command line since a template has none
of its own:
pct create <vmid> local:vztmpl/<file>.tar.xz \
--unprivileged 1 --features nesting=1,keyctl=1 \
--rootfs local-lvm:8 --hostname <name> --cores 2 --memory 2048 --swap 2048 \
--net0 name=eth0,bridge=vmbr0,ip=dhcp
pct start <vmid>
Every one of those extra flags is load-bearing, confirmed by actually booting one:
--unprivileged 1—modules/platforms/lxc.nixsetsproxmoxLXC.privileged = false, so the image assumes it's running unprivileged.pct create's own CLI default for this flag is privileged (unlike the web UI, whose checkbox defaults the other way) — omit it and you get a privileged container running a NixOS config that assumes unprivileged, a real mismatch.--features nesting=1,keyctl=1— required for a modern (v247+) systemd guest to boot unprivileged at all. Without it, AppArmor denies the nested user namespaces and credential mounts systemd routinely uses (even plain getty units) — every getty crash-loops on a denied/run/credentials/*mount every ~3s (this is what garbage on the console turns out to be) while core services likensncdfail the same way, and the system never finishes activating.--swap 2048—--memorydoesn't touch swap; it silently stays at Proxmox's own 512M default otherwise. Match it to--memoryunless you deliberately want otherwise.
First boot runs boot.postBootCommands (registers the Nix store DB and
system profile) — there's no separate activation step to run yourself.
scripts/create-proxmox-resource.sh --type lxc --host <name> automates all
of this (build, host-key handling, upload, pct create with the flags
above) — see its --help.
Host keys still need pre-seeding the same way as any other host — the
sops-nix activation-vs-first-boot race is identical regardless of how the
image reaches the machine. Unlike the ISO/PXE installer (where
modules/installer/host-keys.nix bakes every host-keys/ entry into
/etc/host-keys/ for auto-install.sh to pick from and copy at install
time — see "Host keys" below), an lxc-* tarball has no install step to
copy anything during, so modules/platforms/lxc.nix bakes this one
target's key straight into /etc/ssh/ssh_host_ed25519_key(.pub) directly,
keyed by its own exact flake target name (config.environment.etc can't
be read back from within a module still contributing to it, so this comes
in via specialArgs.flakeTarget, set by flake.nix's mkTarget):
NIXOS_HOST_KEYS_DIR="$(pwd)/host-keys" \
nix build .#nixosConfigurations.lxc-nix-cache.config.system.build.tarball --impure
Confirmed the hard way: without this, the tarball's own built-in system
just generates a fresh host key at first boot like any host would, which
can never match whatever .sops.yaml actually trusts for that target —
sops-install-secrets fails with Error getting data key: 0 successful groups required, got 0, and every secret (including this host's own
login) permanently fails to decrypt, silently — no error in the boot log
at all, since the activation step that would install secrets only runs on
a from-scratch first activation and skips silently once /run/current-system
already exists. scripts/create-proxmox-resource.sh always builds with
NIXOS_HOST_KEYS_DIR set for this reason.
Layout
modules/installer/common.nix— shared by every installer target: SSH access, users, the generated/etc/auto-install.shscript, and theprograms.bash.loginShellInithook that runs it on login.modules/installer/iso.nix— ISO/netboot-specific: imports the stockinstallation-cd-minimal.nixmodule pluscommon.nix. Also used, paired withnetboot-minimal.nix, to build the PXE netboot variant (seedocs/pxe-boot.md).modules/installer/host-keys.nix— optionally bakes pre-generated SSH host keys into the image; see "Host keys" below.scripts/sync-host-keys.sh— admin-workstation tool that generates, registers, and (via--remove/--regenerate-all-keys) retires host keys; see "Creating a New Machine" below.scripts/prepare-host-key.sh— narrower predecessor: generates a single key by an arbitrary name without touching.sops.yaml. Still useful for pre-generating a key before its flake target exists (sync-host-keys.shcan only act on targetsnixosConfigurationsalready has); otherwisesync-host-keys.shdoes the same thing and more.
Flake outputs:
nixosConfigurations.installer # ISO/netboot installer image
packages.x86_64-linux.iso # installer ISO/netboot image
packages.x86_64-linux.pxe # netboot-ipxe + netboot-initrd + netboot-kernel, bundled
nix build .#iso
nix build .#pxe
There's no nixosConfigurations.proxmox-lxc (installer-boots-as-an-LXC-
container) or packages.x86_64-linux.lxc/.all anymore. Both existed only
to let the installer itself run as an LXC container so you could
nixos-install some other host from within it — but LXC targets are
excluded from the install menu (same bind-mount problem as any LXC
nixos-install), and now have their own direct tarball path anyway (see
"LXC hosts" above), which left the installer's own LXC form with no real
use case.
The pxe variant is also built automatically as part of the pxe-boot host
itself (modules/pxe-boot/stage-installer-artifacts.nix) and served over
iPXE — see docs/pxe-boot.md.
Host keys
sops-nix derives each host's decryption key from its own
/etc/ssh/ssh_host_ed25519_key, generated at activation time — before
systemd would otherwise generate one on first boot. Without pre-seeding this
key, secrets (including the root/nixos login password) fail to decrypt on a
genuinely fresh install.
Generated host keys live in host-keys/ at the repo root (ssh_host_ed25519_key
.pubpairs per hostname). This directory is gitignored on purpose — private key material must never be committed — which also means flakes can't see it through a normal relative path.modules/installer/host-keys.nixreads it throughbuiltins.getEnv, which Nix silently returns as an empty string under normal (non---impure) evaluation, so the module is a no-op — safe by default, including in CI — unless explicitly opted into:
NIXOS_HOST_KEYS_DIR="$(pwd)/host-keys" nix build .#iso --impure
When built this way, every key currently in host-keys/ is baked into the
image at /etc/host-keys/<hostname>_ssh_host_ed25519_key(.pub), and
auto-install.sh automatically installs whichever one matches the flake
target selected at install time — no manual per-host scp step needed.
Trade-off, accepted deliberately for this LAN-only setup: baking keys in
means every key present in host-keys/ at build time becomes readable by
anyone who can reach the built image — including, for the PXE variant, anyone
who can reach the pxe-boot host's unauthenticated HTTP server. This is
considered acceptable here because pxe-boot sits behind LAN-only network
infrastructure, not the open internet. If that ever changes, reconsider this
default.
auto-install.sh still supports the older manual path as a fallback: if a
host's key isn't baked in (/etc/host-keys), it checks /root/host-keys
next, where you can scp a key in after boot, same as before this migration.
Storage
Disk partitioning is handled by Disko — the installer has no hardcoded
parted/mkfs/mkswap/mount commands, and auto-install.sh runs
disko --mode destroy,format,mount unconditionally, no branching on whether
the target has a Disko config. Every host reachable through this menu has
one:
proxmox-*(modules/disko/proxmox.nix): a real GPT partition table (ESP + swap + root) on/dev/sda.linode-*(modules/disko/linode.nix): Linode provisions and sizes/dev/sda//dev/sdbitself as whole, unpartitioned block devices before the OS ever boots, so this declares them withdestroy = false(skips disko's wipe stage for these disks entirely — see the option's own docs) and a barefilesystem/swapcontent type with no partition table, and the format step it does run only callsmkfs/mkswapifblkidshows the device isn't already formatted — a re-run against an already-provisioned Linode disk is a no-op, not a wipe.
lxc-* is the only category without one — it's excluded from this menu
entirely (see "LXC hosts" above), so it never reaches this code path.
Installer process
/etc/auto-install.sh:
- Queries
nixosConfigurationsfrom this flake over the network (git+https://<lanDomain>/beatzaplenty/nixos.git) — this happens at install time, not build time, so a generic installer image always sees whatever hosts are currently committed, without needing a rebuild. - Presents them as a menu; confirms the choice.
- Skips the
nix-cachesubstituter when installing anix-cachehost itself (consistent with that host's own runtime config). - Runs
disko --mode destroy,format,mount(see "Storage" above — every host reachable through this menu has a Disko config, so this is unconditional). - Installs the target's SSH host key from
/etc/host-keysor/root/host-keys(see "Host keys" above). - Runs
nixos-install --flake <url>#<choice> --no-root-password. - Cleans up and reboots.
Creating a new machine
Do this instead of jumping straight to a plain install whenever the target
host consumes any sops-nix secret — as of this writing, that's every host
(modules/common/configuration.nix puts the root/nixos password hash and the
GitHub token behind sops-nix for all of them).
-
Add the flake target —
hosts/<name>/host.nixplus the matchingmkTarget { ... }entry inflake.nix'sgeneratedTargets(see "Composition pattern" inCLAUDE.md). No secrets involved yet, so this is safe to commit on its own if you want a clean history. -
On your admin workstation, generate and register its host key:
./scripts/sync-host-keys.sh <flake-target>This generates
host-keys/<flake-target>_ssh_host_ed25519_key(.pub), adds it as a new.sops.yamlanchor, works out whichsecrets/*.yamlfiles this specific host actually references (from its ownconfig.sops.secrets, not guessed), adds it to each one'skey_groups, and re-encrypts them withsops updatekeys— no manual YAML editing. Safe to re-run; it only fills in what's missing.Doing this for every host that needs one at once — after adding several new targets, or just to catch up any that were missed — is
./scripts/sync-host-keys.sh --all. Seescripts/sync-host-keys.sh --helpfor its other modes (--remove,--regenerate-all-keys). -
Commit and push. The flake build the installer uses has to see the new recipient before you install, or decryption fails on first boot regardless of the next step.
-
Build the installer image with keys baked in (or reuse an already-serving
pxe-boothost, which does this automatically once redeployed):NIXOS_HOST_KEYS_DIR="$(pwd)/host-keys" nix build .#iso --impure -
Boot it on the target machine, log in, select the new host's flake target from the menu, confirm.
auto-install.shfinds the baked-in key, runs Disko +nixos-install, and reboots. -
Verify after reboot:
ssh <new-host> ls /run/secrets/If that's empty or login fails, the host's age key most likely wasn't in
.sops.yaml(or wasn't re-encrypted into the secrets file it needs) whennixos-installran — fix.sops.yaml/secrets/*.yaml, push, then re-runnixos-install --flake .#<hostname> --no-root-passwordfrom a rescue environment against the existing/mnt, or just redo the install.
Safety
This installer is destructive: disko --mode destroy,format,mount erases
any disk defined by the selected host's Disko configuration. Always verify
the selected host profile and target machine before confirming.