From 56c1ebd98a9f4648e271db09d9fee3e28b3ba9af Mon Sep 17 00:00:00 2001 From: alexion Date: Sun, 19 Jul 2026 00:54:12 -0400 Subject: [PATCH] Document the one-shot install flow for neogaia Add docs/install.md, the operator runbook that turns the completed neogaia Host into a running encrypted laptop from the NixOS live ISO: push the flake to its remote, join wifi, clone the repo locally, disko-install against neogaia with the chaotic substituter handed to the install-time daemon, set the bootstrap login password by hand via nixos-enter, and reboot. --- .claude/tasks/0009-install-flow-docs.md | 36 ++++++ docs/install.md | 153 ++++++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 .claude/tasks/0009-install-flow-docs.md create mode 100644 docs/install.md diff --git a/.claude/tasks/0009-install-flow-docs.md b/.claude/tasks/0009-install-flow-docs.md new file mode 100644 index 0000000..a710747 --- /dev/null +++ b/.claude/tasks/0009-install-flow-docs.md @@ -0,0 +1,36 @@ +--- +spec: laptop-mvi +blocked-by: [0002-neogaia-disk-and-boot, 0003-kernel-and-hardware, 0004-networking-and-base-system, 0005-fish-shell-module, 0006-tmux-module, 0007-nvim-module, 0008-claude-code-module] +--- + +## What to build + +Document the one-shot install procedure that turns the completed `neogaia` `Host` into a running encrypted laptop from the NixOS live ISO — the capstone, written once every functional slice is in place so it describes the actually-complete `Host`. + +The procedure: push the repo to Gitea first; from the live ISO, join wifi, clone the repo locally (avoiding self-signed-TLS/auth problems with flake fetching during install), and run `disko-install` against the `neogaia` `Host` with the chaotic substituter passed to the install-time Nix daemon (or it compiles the CachyOS kernel from source on the USB stick). Then set the bootstrap password by hand via `nixos-enter` — never committed to the public repo — and reboot. + +Note the bootstrap ordering (the flake must exist on Gitea before the install can consume it) and that moving the password to a `hashedPasswordFile` backed by a sops secret is the first post-boot task, out of scope here (per ADR 0001, an age key does not exist until the first install generates the SSH host key). + +## Acceptance criteria + +- [x] The install procedure is documented end to end: push to Gitea → join wifi on the live ISO → clone locally → `disko-install` against `neogaia` → set bootstrap password via `nixos-enter` → reboot. +- [x] The docs state that the install-time Nix daemon must have the chaotic substituter configured, or the kernel compiles from source on the USB stick. +- [x] The docs explain that the local clone avoids self-signed-TLS/auth problems with flake fetching during install. +- [x] The bootstrap password is set by hand and never committed; the docs flag the sops-backed `hashedPasswordFile` migration as the first post-boot follow-up. + +## Implementation Notes + +The runbook lives at `docs/install.md`. + +Every documented command was checked against the actual pinned tooling rather than written from memory: + +- The `disko-install` and `disko` flags (`--flake`, `--disk NAME DEVICE`, `--write-efi-boot-entries`, `--option`, `--mode mount`) were read out of the pinned disko revision's wrapped scripts (the disko rev in `flake.lock`). +- A consequence surfaced there and shaped the doc: `disko-install` traps `EXIT` and **unmounts** the target, so the "set the bootstrap password" step must first remount with `disko --mode mount` before `nixos-enter`. + A naive `nixos-enter --root /mnt` straight after the install would have found nothing mounted. +- The chaotic substituter URL and trusted key are quoted verbatim from `system/default.nix`, and `--disk main /dev/nvme0n1` matches `hosts/neogaia/disk.nix`. + +Two secrets are set by hand at install time, not one: the doc distinguishes the **LUKS passphrase** (prompted by disko at format, typed at every boot) from the **bootstrap login password** (set via `nixos-enter passwd`). +The task named only the login password; the LUKS passphrase is an unavoidable part of the same by-hand flow, so it is documented alongside for a complete runbook. + +Beyond the task's terse list, the doc adds: a minimal-vs-graphical ISO split for joining wifi, and — from review — an SSH-key caveat for the clone plus an HTTPS-with-`sslVerify=false` fallback (which also reinforces the "git can skip verification where the flake fetcher can't" point behind the local-clone requirement). +No criteria were dropped. diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 0000000..2b9e6e2 --- /dev/null +++ b/docs/install.md @@ -0,0 +1,153 @@ +# Installing `neogaia` + +This is the one-shot procedure that turns the `neogaia` `Host` in this flake into a running, encrypted Dell XPS 13 laptop, installed from the NixOS live ISO. + +It is destructive: it formats `/dev/nvme0n1` in full. +Read it end to end before starting, because the laptop is the only machine and the reimage is irreversible. + +The whole install is a single `disko-install` against the `neogaia` `Host`, followed by setting a bootstrap login password by hand. +Everything the installed system needs — the LUKS layout, the CachyOS kernel, the wifi firmware, the user, and the terminal tooling — is already declared in the flake, so this document is only the mechanics of getting that flake onto the disk. + +## Bootstrap ordering + +The install consumes the flake from Gitea, so **the flake must already be on Gitea before you start** — the repo cannot pull a config that hasn't been pushed. +The bootstrap login password is likewise set by hand at the end and is **never committed**, which is what keeps the public repo free of any secret while still yielding a working login on first boot. + +Two secrets are set by hand during this install, both entered interactively and neither stored in the repo: + +1. The **LUKS passphrase** that encrypts the disk, entered when `disko-install` formats it and again at every boot. +2. The **bootstrap login password** for the `alexion` user, set through `nixos-enter` just before the reboot. + +## 0. Push the repo to Gitea + +From your working checkout, make sure `main` is committed and pushed to the Gitea remote: + +```console +$ git push origin main +``` + +The install reads only committed, git-tracked content, so anything uncommitted will not make it onto the laptop. + +## 1. Boot the live ISO and join wifi + +Boot the machine from a NixOS live ISO (the minimal installer is enough). +The installer logs in as the `nixos` user, who has passwordless `sudo`. + +On the minimal ISO, bring up wifi with `wpa_supplicant`: + +```console +$ sudo systemctl start wpa_supplicant +$ wpa_cli +> add_network +0 +> set_network 0 ssid "YOUR_SSID" +> set_network 0 psk "YOUR_WIFI_PASSWORD" +> enable_network 0 +> quit +``` + +On the graphical ISO, which ships NetworkManager, use `nmcli` instead: + +```console +$ nmcli device wifi connect "YOUR_SSID" password "YOUR_WIFI_PASSWORD" +``` + +Confirm you have connectivity (`ping -c1 github.com`) before continuing. + +## 2. Clone the repo locally + +Clone this repo onto the live ISO and work from that local checkout: + +```console +$ git clone ssh://gitea@git.alexion.dev:2022/alexion/dotfiles-nixos.git +$ cd dotfiles-nixos +``` + +Cloning over SSH needs your Gitea SSH key present in the live session, since the ISO starts with none. +If getting the key onto the ISO is inconvenient, clone over HTTPS instead and tell git to skip the self-signed certificate: + +```console +$ git -c http.sslVerify=false clone https://git.alexion.dev/alexion/dotfiles-nixos.git +$ cd dotfiles-nixos +``` + +Do **not** point `disko-install` straight at the Gitea flake URL. +Gitea serves HTTPS with a self-signed certificate and expects authentication, and Nix's flake fetcher has no easy way to skip certificate verification or supply those credentials mid-install. +A plain `git clone` sidesteps that entirely — over SSH there is no TLS, and over HTTPS git takes the `sslVerify=false` above that the flake fetcher won't — and then `disko-install` consumes the flake from a local path, where no fetch of our repo happens during the build. +(The public flake inputs — `nixpkgs`, `chaotic`, `disko` — are still fetched from GitHub over ordinary, valid TLS; only our own repo is the problem the local clone solves.) + +## 3. Run `disko-install` + +Run the install as root from inside the clone: + +```console +$ sudo nix --extra-experimental-features 'nix-command flakes' run \ + github:nix-community/disko/latest#disko-install -- \ + --flake .#neogaia \ + --disk main /dev/nvme0n1 \ + --write-efi-boot-entries \ + --option extra-substituters https://nyx-cache.chaotic.cx/ \ + --option extra-trusted-public-keys nyx-cache.chaotic.cx:dJxTrgMC3V3cFfyIiBQDQorG6k1LsqurH/srpMSq7qk= +``` + +What each part does: + +- `--flake .#neogaia` installs the `neogaia` `Host` from the local clone. +- `--disk main /dev/nvme0n1` maps disko's `main` disk to the NVMe device; it matches the device declared in `hosts/neogaia/disk.nix` and is stated explicitly so there is no doubt about the target. +- `--write-efi-boot-entries` writes the systemd-boot entry into this machine's NVRAM, because the disk stays in the laptop it was installed from. +- The two `--option` lines are the important part: they hand the **chaotic binary cache** to the install-time Nix daemon on the live ISO. + +The chaotic substituter must be passed here explicitly. +The `nix.settings` in the flake configure the substituters of the *installed* system, not the live ISO's daemon that runs this build; the ISO's daemon has no `substituters` beyond `cache.nixos.org`. +Without these two `--option` flags, the build cannot fetch the prebuilt CachyOS kernel and **compiles `linuxPackages_cachyos` (and its toolchain) from source on the USB stick** — a very long detour that the cache avoids. +Because the install runs as root, and root is a trusted Nix user, the daemon honours these client-supplied substituter settings. + +Partway through, disko formats the LUKS container and **prompts for a disk-encryption passphrase**. +This is the passphrase you will type at every boot to unlock the disk; choose it deliberately. + +When it finishes it prints `disko-install succeeded`. +`disko-install` unmounts the target filesystem on exit, so nothing is mounted at this point — step 4 remounts it. + +## 4. Set the bootstrap login password + +The installed system was written with no login password (`nixos-install --no-root-password`, and the flake sets none for `alexion`), so it cannot yet be logged into. +Set a bootstrap password by hand before rebooting. + +First remount the just-installed system with disko, which reopens the LUKS container (prompting for the passphrase from step 3) and mounts the subvolumes under `/mnt`: + +```console +$ sudo nix --extra-experimental-features 'nix-command flakes' run \ + github:nix-community/disko/latest#disko -- \ + --mode mount --flake .#neogaia +``` + +Then enter the installed system and set the password for your user: + +```console +$ sudo nixos-enter --root /mnt +[nixos-enter]# passwd alexion +[nixos-enter]# exit +``` + +This password lives only on the laptop's disk; it is **never committed** anywhere. + +## 5. Reboot + +Unmount and reboot into the installed system: + +```console +$ sudo umount -R /mnt +$ sudo reboot +``` + +Remove the USB stick. +At boot you are prompted for the LUKS passphrase from step 3; after unlocking, log in at the console as `alexion` with the bootstrap password from step 4 and you have a working system with fish, tmux, nvim, and Claude Code. + +## First post-boot task + +Setting the login password by hand is a bootstrap shortcut, not the end state. +The first thing to do on the running laptop is to move that password to a `hashedPasswordFile` backed by a `sops-nix` secret, so it is declared and reproducible like everything else. + +This is deliberately out of scope for the install itself. +Per ADR 0001, each `Host`'s secrets are encrypted to an age key derived from that `Host`'s SSH host key — and that host key does not exist until this first install generates it. +So the sops wiring can only happen *after* the machine is up, which is exactly why it is the first follow-up rather than part of this procedure.