Skip to content

Latest commit

 

History

History
46 lines (39 loc) · 12 KB

File metadata and controls

46 lines (39 loc) · 12 KB

AGENTS.md

Nix flake managing NixOS, nix-darwin, and Home Manager configs (repo DavSanchez/nix-dotfiles, default branch master).

Layout

  • hosts/nixos/<host>.nix / hosts/darwin/<host>.nix — machine entrypoints. eter is x86_64-linux; all darwin hosts are aarch64-darwin. eter also has a per-host dir hosts/nixos/eter/ (fs_share.nix, media.nix, monitoring.nix, zfs.nix, …) imported alongside shared hosts/nixos/modules/.
  • Raspberry Pi hosts (mora = Pi 5, bruma = Pi 3B, duende = Pi 3B+) are aarch64-linux: a nixos-hardware board profile (inputs.hardware.nixosModules.raspberry-pi-5 / raspberry-pi-3) supplies the downstream kernel + config.txt, and hosts/nixos/modules/raspberry-pi.nix matches the layout nixpkgs' own sd-image-aarch64.nix produces (by-label/NIXOS_SD root, by-label/FIRMWARE FAT) — see lib/nixos-sd-image.nix for how each host's custom image is built. mora also has a per-host dir hosts/nixos/mora/ (services.nix, livedns.nix, monitoring.nix, dashboards/); bruma has hosts/nixos/bruma/livedns.nix, and duende has hosts/nixos/duende/livedns.nix plus hosts/nixos/duende/retro-gaming.nix (Bluetooth + RetroArch + services.cage kiosk).
  • home/darwin/*.nix — Home Manager entrypoints (sierpe, solio, home-nr.nix). All are aarch64-darwin only.
  • home/modules/ — per-user Home Manager modules (internal to this machine set). modules/{nixos,darwin}/ — reusable modules exported from the flake (self.nixosModules, self.darwinModules); self.darwinModules.networking and self.darwinModules.stevenblack are custom and power the /etc/hosts tests.
  • pkgs/ — custom packages (kontroll, omniwm); overlays/; tests/darwin/ + lib/darwin-tests.nix — module test harness.
  • scripts/ — bash scripts the Justfile wraps for its multi-step recipes (sd-image, flash-image, host-key, linux-builder, config-diff, build-pkg/eval-config); config-attr.sh resolves a config name to its flake namespace. They are shellchecked in CI and expect their tools from the dev shell.
  • The nr machine is keyed by Apple serial: darwin config name is V9X576T260, home config is davidsanchez@V9X576T260 (host file is hosts/darwin/nr.nix).

Commands

  • Format all Nix: nix fmt (formatter is nixfmt-tree).
  • Check the flake: nix flake check -L --keep-going. Only run this on Linux; darwin configs don't evaluate on Linux. On macOS, build darwin checks individually (see tests) — CI also skips the deploy-activate/deploy-schema checks there.
  • Dev shell: nix develop exposes every tool the Justfile, scripts/ and CI workflows shell out to (scriptTools in flake.nix) — just, sops, ssh-to-age, ssh-keygen, jq, zstd, debugfs, nc, dix, nix-diff, shellcheck, … The same list is built as the dev-shell check, so a nixpkgs bump that breaks one of those packages fails CI instead of a recipe at runtime.
  • Darwin module tests: nix build .#checks.aarch64-darwin.<test>. Every .nix file in tests/darwin/ becomes a check automatically.
  • Build a package inside a config's pkgs: just build-pkg <host> <pkg> (auto-detects nixos/darwin/home); just build-pkg-dry for dry-run. Raw escape hatch: just build-attr <attr>.
  • Eval any config sub-attr as JSON: just eval-config <host|user@host> <attr-path> (needed for quoted names like david@sierpe).
  • Raspberry Pi SD cards: just sd-image <host> builds a custom image (via lib/nixos-sd-image.nix) that boots straight into that host's real config — prints the image path (local/images/<host>.img.zst with the host key injected, else the plain store path); just flash-image <image> <disk> writes it, macOS-only (refuses non-removable disks and asks for confirmation — writing erases the card), or on Linux zstd -dc <image> | sudo dd of=/dev/sdX bs=4m status=progress. SSH access needs no console session: hosts/nixos/modules/user.nix bakes your davidslt+ssh@pm.me keys in, so you can ssh david@<host>.local right after boot. To also have sops secrets work from the first boot, pre-seed the age identity first: just host-key <host> → add the printed recipient to .sops.yaml → just update-sops → just sd-image <host> (it injects the key into the image copy when it exists; see the gotcha below).
  • Apply configs:
    • NixOS: sudo nixos-rebuild switch --flake .#eter
    • nix-darwin: darwin-rebuild switch --flake .#sierpe (or .#V9X576T260 for nr)
    • Home Manager: nix run home-manager/master -- switch --flake .#david@sierpe
  • Deploy the deploy-rs nodes in flake.nix with deploy .#<host>: eter, mora, bruma (duende is currently commented out in deploy.nodes). Nodes are addressed by Tailscale MagicDNS name, so deploy works from anywhere the tailnet is up (Tailscale running on the client, target already joined). A Pi flashed with just sd-image <host> already boots as david with the repo's keys, but until it has joined the tailnet (tailscale up) MagicDNS won't resolve — reach it over the LAN first with deploy --hostname <host>.local .#<host>. A Pi flashed some other way (e.g. Hydra's generic nixos.sd_image.aarch64-linux) boots as root/nixos instead, so its first rollout needs deploy --ssh-user root --hostname <ip> .#<host>.
  • Linux builder VM: always up with its daemon, and its memory is a ceiling the host never gets back — just stop-linux-builder releases it (RAM + disk), just start-linux-builder brings it back, just restart-linux-builder re-spins it after a nix.linux-builder.* change.

Gotchas

  • Raspberry Pi kernels build from source: nixos-hardware's linux-rpi is not in cache.nixos.org, nor is anything built against it, so the first CI run (native aarch64 runner) or Mac build (through the linux-builder VM) compiles it; later builds hit the local store or the davsanchez cachix cache.
  • One ssh host key per Pi: sops decrypts with the host key (age.sshKeyPaths = /etc/ssh/ssh_host_ed25519_key, in hosts/nixos/*.nix). Left to itself sshd generates that key on first boot, so a freshly flashed card no longer matches its recipient in .sops.yaml — you'd have to add the new key and just update-sops, or that host's secrets (Wi-Fi PSK, Gandi PAT) fail to decrypt. To skip that bootstrap, pre-generate the key instead: just host-key <host> writes a key under the git-ignored local/host-keys/ and prints its age recipient; add it to .sops.yaml, just update-sops, then just sd-image <host>, which injects the key into a non-store copy of the image at local/images/<host>.img.zst with debugfs (see lib/nixos-sd-image.nix). The key is deliberately kept out of the Nix store: pkgs.writeText (or inlining it) would make the private key world-readable there, and since the encrypted secrets.yaml is in the store too, any local user could decrypt that host's secrets. Never commit local/host-keys/ — the repo is public and these are the decryption identities. The injected copies under local/images/ embed that same private key, so treat them as secrets too (don't share or upload them). All three Pis share the home Wi-Fi PSK dome_wifi (SSID TP-Link_83A4) and each has its own age recipient in .sops.yaml, so all three associate on first boot without a per-host secret.
  • Legacy Pi key rotation: before the out-of-store image change, keyed SD builds could put the host key in a world-readable Nix store path. If any image was built using that flow, treat its SSH/age key as disclosed; changing the build code or garbage-collecting the store does not revoke copies. Rotate each affected identity in a staged order: add the replacement recipient and re-encrypt, securely install the new key on the host (or reflash), then remove the old recipient and re-encrypt again. Every Pi recipient is authorized for the shared secrets/secrets.yaml, so a leaked Pi key may decrypt the whole file. just host-key reuses an existing key; it does not rotate one. Remove obsolete store generations and image archives after migration.
  • The firmware module owns the FAT partition: each switch rewrites config.txt, copies device trees/overlays and prunes stale entries — don't hand-edit files there. Its copy is ~26 MB of the stock 30 MB partition; if a switch fails with No space left on device, drop firmware.enable or enlarge the partition.
  • No EmulationStation in nixpkgs: duende's frontend is RetroArch's own menu (services.cage boots straight into it) — there's no separate launcher to install. PS4/PS5 controllers pair over Bluetooth via bluetoothctl (scan on, pair, trust, connect) — a one-time interactive step needing a physical button press on the controller, not something Nix declares.
  • Secrets: secrets/secrets.yaml is age-encrypted (sops). Decryption needs the key at ~/.config/sops/age/keys.txt; after adding keys, re-encrypt with just update-sops. git diff shows decrypted content via the diff=sopsdiffer textconv (sops decrypt). Never commit or echo decrypted values.
  • Observability: every NixOS host runs node_exporter via hosts/nixos/modules/node-exporter.nix (port 9100, firewall-opened only on tailscale0); sierpe/solio run it via hosts/darwin/modules/prometheus-node.nix (nr is deliberately excluded — work machine). mora hosts Prometheus + Grafana (hosts/nixos/mora/monitoring.nix): Prometheus scrapes the other hosts by Tailscale MagicDNS name (so scrapes need Tailscale up even though the Grafana UI is reachable on the LAN), retains 30d with a 5GB cap (SD-card friendly, 1m scrape), and Grafana serves https://grafana.mora.davidslt.es through Caddy (covered by the existing *.mora LiveDNS record, which carries both LAN and tailnet IPs). Grafana's admin_password/secret_key come from the grafana_admin_password/grafana_secret_key sops secrets via Grafana's $__file{} provider (the nixpkgs module asserts secret_key is set and rejects a store value). Dashboards are vendored JSON under hosts/nixos/mora/dashboards/ with the datasource UID pinned to prometheus (provisioned dashboards don't run the import wizard, so ${DS_*} placeholders must be resolved).
  • Least-privilege networking: don't leave a service reachable on every interface unless it's meant to be public. For tailnet-only services open the port on networking.firewall.interfaces."tailscale0" (see hosts/nixos/modules/node-exporter.nix); for LAN access openssh and samba use their default openFirewall (open on every interface, backed by app-level restrictions — key-only SSH, Samba hosts allow) because source-scoping via networking.firewall.extraInputRules is nftables-only and these hosts run the iptables backend (Docker/libvirt/VMs still need it). qbittorrent is Caddy-only (openFirewall = false). Public eter Caddy vhosts carry a @untrusted not remote_ip <privateNets> guard. macOS hosts enable networking.applicationFirewall (signed apps allowed, stealth mode on) and whitelist the unsigned node_exporter binary through socketfilterfw at activation (hosts/darwin/modules/application-firewall.nix).
  • Tailscale ACLs are out-of-band: the tailnet access policy lives in the Tailscale admin console, not this repo. Treat the host firewalls above as the second layer — don't rely on tailnet membership alone to gate a listener.
  • macOS builds x86_64-linux derivations via a nix.linux-builder (darwin-builder VM). CI activates the throwaway linux-builder-bootstrap darwin config first; the real darwin hosts configure their own builder.
  • The darwin networking.enableHosts module writes /etc/hosts as a regular file during activation (macOS Network framework can't resolve symlinks there); upstreaming intent is documented in tests/darwin/UPSTREAMING.md.
  • Dependency bumps and lockfile maintenance are automated by Renovate (automerge, conventional-commit PRs like chore(deps): lock file maintenance) — don't hand-bump inputs.
  • Commit style is Conventional Commits (chore:, fix(nix):, pkg: update X).
  • CI gates each workflow on a per-workflow path PATTERN (in .github/workflows/*.yml); flake-check runs on any .nix or flake.lock change.