From 9feecd8daa404edcaa11fd65890ab94c06d1e680 Mon Sep 17 00:00:00 2001 From: erik Date: Tue, 14 Jul 2026 13:19:35 +0200 Subject: [PATCH] docs: add CLAUDE.md (layout, deploy commands, secrets model, gotchas) --- CLAUDE.md | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..de456df --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,92 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +Flake-based NixOS config for a homelab. Hosts: **jupiter** (ZimaBlade NAS, x86_64), +**vps** (netcup public reverse proxy + tailnet node, x86_64), **mercury** (Raspberry +Pi 3B+ DNS/DHCP, aarch64). See `README.md` for the full install/deploy walkthrough. + +## Layout + +``` +flake.nix # nixosConfigurations: real hosts + test/util targets +common.nix # shared base: user darman (key-only ssh), nix settings, firewall :22, tz +services/*.nix # one reusable NixOS module per service; each opens ITS OWN firewall ports +hosts// # configuration.nix + disk-config.nix (disko) + hardware-configuration.nix + secrets.nix +secrets/.yaml # sops-nix, age-encrypted per host +scripts/deploy # config-agnostic deploy wrapper (all args mandatory) +scripts/edit_secrets +.sops.yaml # per-host encryption rules (admin key + each host's key) +``` + +A host = `common.nix` + the `services/*` modules it imports + its `hosts//configuration.nix`. +`services/` modules are engine-agnostic and shared across hosts (e.g. `tailscale.nix`, +`caddy.nix` used by jupiter and vps). + +## Commands + +Eval/verify a config before deploying (eval only checks the module tree, not +freeform config like pihole's TOML or a container's runtime): +``` +nix eval --raw .#nixosConfigurations..config.system.build.toplevel.drvPath +``` + +Deploy (from a non-NixOS laptop too — runs nixos-rebuild/nixos-anywhere via `nix run`): +``` +./scripts/deploy switch # daily rebuild + activate +./scripts/deploy install # first install (nixos-anywhere, wipes OS disk) +./scripts/deploy kexec # RO-root box (ZimaOS): kexec into a RAM installer first +./scripts/deploy image mercury # build the aarch64 SD image +./scripts/deploy flash mercury /dev/sdX # build + write SD + drop the sops age key +``` + +Secrets (needs the admin age key at `~/.config/sops/age/keys.txt`): +``` +./scripts/edit_secrets secrets/.yaml +``` + +Test a service config BEFORE touching hardware — always do this for nontrivial changes: +``` +# x86 QEMU VM of mercury's DNS/DHCP stack (fast; validates pihole/unbound at runtime) +nix build .#nixosConfigurations.mercury-vm.config.system.build.vm -o result +./result/bin/run-mercury-vm-vm # ssh -p 2223 darman@localhost (pw: test) +# jupiter services as a VirtualBox OVA +nix build .#nixosConfigurations.jupiter-vbox.config.system.build.virtualBoxOVA +``` + +## Secrets (sops-nix) + +- Each `secrets/.yaml` is encrypted to the **admin** key (edit) + that **host's** + key (runtime decrypt); rules in `.sops.yaml`. Private keys live OFF-repo: + `~/.config/sops/age/keys.txt` (admin), `~/.config/homelab//` (host keys). +- jupiter/vps decrypt with their **ssh host key** (`ssh-to-age` recipient), shipped at + install via `nixos-anywhere --extra-files`. +- mercury (SD image, no `--extra-files`) uses a **dedicated age key** at + `/var/lib/sops-nix/age.txt` — `./scripts/deploy flash` writes it to the ext4 root partition. +- A service password that must come from sops but whose module has no `passwordFile` + hook (pihole, adguard) is injected via `sops.templates` → an env file → the service + (`FTLCONF_*` for pihole). See `hosts/mercury/configuration.nix`. + +## Non-obvious gotchas (all learned the hard way) + +- **aarch64 (mercury)**: the x86 laptop needs `extra-platforms = aarch64-linux` in + `/etc/nix/nix.custom.conf` (NOT `/etc/nix/nix.conf` — Determinate Nix regenerates that) + + `qemu-user-static-binfmt`, else emulated builds fail with "platform mismatch". Or + build on the Pi with `--build-host darman@`. +- **pihole on mercury is a CONTAINER** (`services/pihole.nix`, official image, host + networking, caps NET_ADMIN/NET_RAW/SYS_NICE/CHOWN, `FTLCONF_*` env config). The native + `services.pihole-ftl` module **segfaults on the Pi 3B+ aarch64** — do not switch back. +- **`services.unbound.resolveLocalQueries = false`** is required: unbound listens on + :5335, so leaving it true points the host's resolv.conf at 127.0.0.1:**53** with nothing + there → boot-time DNS deadlock (starves image pulls / list downloads). Host resolves via + upstream `networking.nameservers`; pihole forwards to unbound explicitly at `127.0.0.1#5335`. +- **Remote deploy pushes unsigned closures**: hosts set `nix.settings.trusted-users = + [ "root" "@wheel" ]` (in common.nix) so a laptop-built closure is accepted by the target. +- **jupiter**: `boot.kernelParams = [ "reboot=pci" ]` (warm reboot hangs on that board); + eMMC initrd modules pinned in `configuration.nix` (generate-config misses them); the + 16TB×2 **RAID0** data lives on `/mnt/data` with `nofail`, kept OUT of disko (never wiped). +- **disko wipes only the OS disk** named in `hosts//disk-config.nix`; data disks are + plain `fileSystems` in `configuration.nix`. +- `nixos-anywhere`/kexec needs a writable root; **ZimaOS root is read-only**, hence the + `./scripts/deploy kexec` step that streams a RAM installer (with static cpio/gzip since + ZimaOS lacks them).