Nix flake managing NixOS, nix-darwin, and Home Manager configs (repo DavSanchez/nix-dotfiles, default branch master).
hosts/nixos/<host>.nix/hosts/darwin/<host>.nix— machine entrypoints.eteris x86_64-linux; all darwin hosts are aarch64-darwin.eteralso has a per-host dirhosts/nixos/eter/(fs_share.nix,media.nix,monitoring.nix,zfs.nix, …) imported alongside sharedhosts/nixos/modules/.- Raspberry Pi hosts (
mora= Pi 5,bruma= Pi 3B,duende= Pi 3B+) are aarch64-linux: anixos-hardwareboard profile (inputs.hardware.nixosModules.raspberry-pi-5/raspberry-pi-3) supplies the downstream kernel +config.txt, andhosts/nixos/modules/raspberry-pi.nixmatches the layout nixpkgs' ownsd-image-aarch64.nixproduces (by-label/NIXOS_SDroot,by-label/FIRMWAREFAT) — seelib/nixos-sd-image.nixfor how each host's custom image is built.moraalso has a per-host dirhosts/nixos/mora/(services.nix,livedns.nix,monitoring.nix,dashboards/);brumahashosts/nixos/bruma/livedns.nix, andduendehashosts/nixos/duende/livedns.nixplushosts/nixos/duende/retro-gaming.nix(Bluetooth + RetroArch +services.cagekiosk). 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.networkingandself.darwinModules.stevenblackare custom and power the/etc/hoststests.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.shresolves a config name to its flake namespace. They are shellchecked in CI and expect their tools from the dev shell.- The
nrmachine is keyed by Apple serial: darwin config name isV9X576T260, home config isdavidsanchez@V9X576T260(host file ishosts/darwin/nr.nix).
- Format all Nix:
nix fmt(formatter isnixfmt-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 thedeploy-activate/deploy-schemachecks there. - Dev shell:
nix developexposes every tool the Justfile,scripts/and CI workflows shell out to (scriptToolsinflake.nix) —just,sops,ssh-to-age,ssh-keygen,jq,zstd,debugfs,nc,dix,nix-diff,shellcheck, … The same list is built as thedev-shellcheck, 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.nixfile intests/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-dryfor 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 likedavid@sierpe). - Raspberry Pi SD cards:
just sd-image <host>builds a custom image (vialib/nixos-sd-image.nix) that boots straight into that host's real config — prints the image path (local/images/<host>.img.zstwith 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 Linuxzstd -dc <image> | sudo dd of=/dev/sdX bs=4m status=progress. SSH access needs no console session:hosts/nixos/modules/user.nixbakes yourdavidslt+ssh@pm.mekeys in, so you canssh david@<host>.localright 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.#V9X576T260for nr) - Home Manager:
nix run home-manager/master -- switch --flake .#david@sierpe
- NixOS:
- Deploy the
deploy-rsnodes inflake.nixwithdeploy .#<host>:eter,mora,bruma(duendeis currently commented out indeploy.nodes). Nodes are addressed by Tailscale MagicDNS name, sodeployworks from anywhere the tailnet is up (Tailscale running on the client, target already joined). A Pi flashed withjust sd-image <host>already boots asdavidwith the repo's keys, but until it has joined the tailnet (tailscale up) MagicDNS won't resolve — reach it over the LAN first withdeploy --hostname <host>.local .#<host>. A Pi flashed some other way (e.g. Hydra's genericnixos.sd_image.aarch64-linux) boots asroot/nixosinstead, so its first rollout needsdeploy --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-builderreleases it (RAM + disk),just start-linux-builderbrings it back,just restart-linux-builderre-spins it after anix.linux-builder.*change.
- Raspberry Pi kernels build from source:
nixos-hardware'slinux-rpiis not incache.nixos.org, nor is anything built against it, so the first CI run (native aarch64 runner) or Mac build (through thelinux-builderVM) compiles it; later builds hit the local store or thedavsanchezcachix cache. - One ssh host key per Pi:
sopsdecrypts with the host key (age.sshKeyPaths = /etc/ssh/ssh_host_ed25519_key, inhosts/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 andjust 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-ignoredlocal/host-keys/and prints its age recipient; add it to.sops.yaml,just update-sops, thenjust sd-image <host>, which injects the key into a non-store copy of the image atlocal/images/<host>.img.zstwithdebugfs(seelib/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 encryptedsecrets.yamlis in the store too, any local user could decrypt that host's secrets. Never commitlocal/host-keys/— the repo is public and these are the decryption identities. The injected copies underlocal/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 PSKdome_wifi(SSIDTP-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-keyreuses 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 withNo space left on device, dropfirmware.enableor enlarge the partition. - No EmulationStation in nixpkgs:
duende's frontend is RetroArch's own menu (services.cageboots straight into it) — there's no separate launcher to install. PS4/PS5 controllers pair over Bluetooth viabluetoothctl(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.yamlis age-encrypted (sops). Decryption needs the key at~/.config/sops/age/keys.txt; after adding keys, re-encrypt withjust update-sops.git diffshows decrypted content via thediff=sopsdiffertextconv (sops decrypt). Never commit or echo decrypted values. - Observability: every NixOS host runs
node_exporterviahosts/nixos/modules/node-exporter.nix(port 9100, firewall-opened only ontailscale0);sierpe/soliorun it viahosts/darwin/modules/prometheus-node.nix(nris deliberately excluded — work machine).morahosts 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 serveshttps://grafana.mora.davidslt.esthrough Caddy (covered by the existing*.moraLiveDNS record, which carries both LAN and tailnet IPs). Grafana'sadmin_password/secret_keycome from thegrafana_admin_password/grafana_secret_keysops secrets via Grafana's$__file{}provider (the nixpkgs module assertssecret_keyis set and rejects a store value). Dashboards are vendored JSON underhosts/nixos/mora/dashboards/with the datasource UID pinned toprometheus(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"(seehosts/nixos/modules/node-exporter.nix); for LAN accessopensshandsambause their defaultopenFirewall(open on every interface, backed by app-level restrictions — key-only SSH, Sambahosts allow) because source-scoping vianetworking.firewall.extraInputRulesis nftables-only and these hosts run the iptables backend (Docker/libvirt/VMs still need it).qbittorrentis Caddy-only (openFirewall = false). PubliceterCaddy vhosts carry a@untrusted not remote_ip <privateNets>guard. macOS hosts enablenetworking.applicationFirewall(signed apps allowed, stealth mode on) and whitelist the unsigned node_exporter binary throughsocketfilterfwat 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 throwawaylinux-builder-bootstrapdarwin config first; the real darwin hosts configure their own builder. - The darwin
networking.enableHostsmodule writes/etc/hostsas a regular file during activation (macOS Network framework can't resolve symlinks there); upstreaming intent is documented intests/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-checkruns on any.nixorflake.lockchange.