nixpkgs only carries Zitadel 2.71, which predates the login-v2 split and
cannot take a v3/v4 database (its migrations are forward-only), so the
instance running on the old Debian VPS could never have moved onto it.
authentik-nix ships 2026.5.4 and tracks upstream closely.
The authentik-nix input deliberately does not follow our nixpkgs, per
upstream's warning that overriding it breaks their pinned python
dependency set. That costs a second nixpkgs in the lock, so add
nix-community's Cachix to common.nix -- without it the closure is ~400
local derivations (npm, rust, python). The laptop that runs
scripts/deploy needs the same two lines in /etc/nix/nix.custom.conf.
Authentik's own module creates the database and orders its units against
postgresql.target, and recent versions need no redis, so the wiring is
just the module plus a secret. Pin postgresql explicitly so that editing
system.stateVersion can never silently demand a pg_upgrade of the
identity store.
Secret ownership is not uniform and the difference matters: authentik
and caddy take a systemd EnvironmentFile, which PID 1 reads as root
before dropping privileges, so root:root 0400 is correct. Headplane
opens its secret paths itself while already running as the headscale
user, so those three need an explicit owner or they fail to start.
Also on neptun:
- Pass Caddy's ACME account email through the same EnvironmentFile
mechanism and reference it with the Caddyfile {$VAR} placeholder.
services.caddy.email would render the address into the world-readable
store.
- Stop accepting MagicDNS from our own control server. headscale pushes
override_local_dns, so joining the tailnet would point neptun's
resolv.conf at a MagicDNS served by the tailscaled neptun itself hosts
-- a tailscaled failure would then also take out DNS, ACME renewal and
finally the certs for the control server every other node needs in
order to recover.
- Give headplane a writable DNS extra-records file. Its view of
headscale's config stays read-only, which is the right outcome for a
declarative box; records are data rather than config.
- Require a password for sudo. Deploys become interactive, but darman's
key is otherwise the only thing between the public internet and root.
- Enable zram (8 GB, and disko leaves no room for a swap device), and let
tailscaled-autoconnect retry instead of failing permanently when the
control server isn't up yet on a first boot.
networking.hosts still carries a PLACEHOLDER address for jupiter --
replace it from `headscale nodes list` once jupiter first enrols.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
133 lines
5.9 KiB
Markdown
133 lines
5.9 KiB
Markdown
# homelab
|
|
|
|
Flake-based NixOS config. Hosts: `jupiter` (ZimaBlade, NAS + services),
|
|
`neptun` (netcup VPS: public reverse proxy, Authentik, headscale),
|
|
`mercury` (Raspberry Pi 3B+, DNS/DHCP), `terra` (desktop).
|
|
|
|
## Structure
|
|
|
|
```
|
|
flake.nix # inputs + nixosConfigurations (jupiter, neptun, kexec, ...)
|
|
common.nix # shared base: user, ssh, nix, firewall, timezone
|
|
services/ # one reusable module per service, by category
|
|
media/ jellyfin, audiobookshelf, the *arrs, sabnzbd, seerr, ...
|
|
network/ caddy, samba, avahi, pihole, unbound
|
|
vpn/ tailscale, headscale (control server), headplane (its web UI)
|
|
identity/ authentik (OIDC provider, from the authentik-nix flake)
|
|
dev/ gitea
|
|
desktop/ hyprland
|
|
containers.nix # podman backend, shared across categories
|
|
hosts/
|
|
jupiter/ # ZimaBlade NAS
|
|
configuration.nix # host bits + imports common + the services it runs
|
|
disk-config.nix # disko: eMMC partitions
|
|
hardware-configuration.nix
|
|
secrets.nix # sops-nix wiring
|
|
vm.nix # VirtualBox test image (jupiter-vbox)
|
|
neptun/ # netcup public reverse proxy + tailnet node
|
|
configuration.nix disk-config.nix hardware-configuration.nix secrets.nix
|
|
secrets/ # age-encrypted sops files (jupiter.yaml, neptun.yaml)
|
|
scripts/ # deploy, edit_secrets
|
|
```
|
|
|
|
Hosts compose by importing `common.nix` + whichever `services/*` modules they
|
|
run. Each service module opens its own firewall ports.
|
|
|
|
## Test in VirtualBox (no hardware needed)
|
|
|
|
```
|
|
nix build .#nixosConfigurations.jupiter-vbox.config.system.build.virtualBoxOVA
|
|
VBoxManage import result/*.ova --vsys 0 --vmname jupiter-vbox
|
|
VBoxManage startvm jupiter-vbox --type headless
|
|
```
|
|
Login `darman` / `test`. Forward ports with `VBoxManage modifyvm ... --natpf1`.
|
|
|
|
## First install on the ZimaBlade — nixos-anywhere + disko
|
|
|
|
Wipes the OS disk and installs the flake over SSH. No USB needed if the box
|
|
already runs Linux (ZimaOS) reachable by root SSH — nixos-anywhere kexecs into
|
|
an installer, partitions via disko, installs.
|
|
|
|
> ⚠️ The OS disk in `disk-config.nix` is WIPED. Set `device` to the OS disk
|
|
> ONLY (by-id). Back up / physically identify the NAS data disk first — it must
|
|
> NOT appear in disko. `lsblk -o NAME,SERIAL,SIZE,MODEL` to identify.
|
|
|
|
1. Set the real OS disk id in `hosts/jupiter/disk-config.nix`
|
|
(`ls -l /dev/disk/by-id`), and the data-disk mount in `configuration.nix`.
|
|
2. Add your login SSH pubkey to `users.users.darman.openssh.authorizedKeys.keys`.
|
|
3. Set the real samba password:
|
|
```
|
|
export SOPS_AGE_KEY_FILE=~/.config/sops/age/keys.txt
|
|
nix shell nixpkgs#sops -c sops secrets/jupiter.yaml # edit, commit
|
|
```
|
|
4. Stage the pre-generated host key so sops can decrypt on boot #1
|
|
(private key lives off-repo in `~/.config/homelab/jupiter/`):
|
|
```
|
|
install -Dm600 ~/.config/homelab/jupiter/ssh_host_ed25519_key \
|
|
/tmp/extra/etc/ssh/ssh_host_ed25519_key
|
|
install -Dm644 ~/.config/homelab/jupiter/ssh_host_ed25519_key.pub \
|
|
/tmp/extra/etc/ssh/ssh_host_ed25519_key.pub
|
|
```
|
|
5. Run from your laptop:
|
|
```
|
|
nix run github:nix-community/nixos-anywhere -- \
|
|
--flake .#jupiter \
|
|
--extra-files /tmp/extra \
|
|
--generate-hardware-config nixos-generate-config ./hosts/jupiter/hardware-configuration.nix \
|
|
--target-host root@<zimablade-ip>
|
|
```
|
|
`--extra-files` plants the host key before first boot (its age identity is
|
|
already a recipient in `.sops.yaml`, so `/run/secrets/samba_password`
|
|
decrypts on boot #1). `--generate-hardware-config` pulls the target's real
|
|
kernel modules into the placeholder. Commit the result. Reboot into NixOS.
|
|
|
|
Manual alternative (USB ISO): boot installer, `disko` the disk, then
|
|
`nixos-install --flake .#jupiter`.
|
|
|
|
## Deploy (the `./deploy` wrapper)
|
|
|
|
All arguments mandatory — no default host, no default config.
|
|
|
|
```
|
|
./deploy kexec <host> # headless kexec into a RAM installer (RO-root box)
|
|
./deploy install <config> <host> # first install; wipes OS disk, ships host key
|
|
./deploy switch <config> <host> # rebuild + activate on a running host
|
|
./deploy boot|test <config> <host> # stage for next boot / activate without boot entry
|
|
```
|
|
`<config>` is a `nixosConfigurations` name (`jupiter`, `neptun`). Its pre-generated
|
|
SSH host key lives at `~/.config/homelab/<config>/ssh_host_ed25519_key`.
|
|
|
|
Examples:
|
|
```
|
|
./deploy switch jupiter jupiter.sol
|
|
./deploy install neptun 159.195.64.117
|
|
```
|
|
Rollback: `nixos-rebuild switch --rollback` on the host, or pick a prior
|
|
generation at boot.
|
|
|
|
## Adding a service
|
|
|
|
Copy the `whoami` block in `oci-containers.containers`, swap image/ports/volumes.
|
|
Native NixOS module exists for many apps (Nextcloud, Jellyfin, Grafana...) —
|
|
prefer `services.<app>` over a container when available. Add a `caddy`
|
|
`virtualHosts` block to expose it.
|
|
|
|
## Notes
|
|
|
|
- Backend is Podman with `dockerCompat` — `docker` CLI works, no daemon.
|
|
- Samba keeps its own password DB. `services.samba` never sets it; a systemd
|
|
oneshot (`samba-smbpasswd`) provisions it. Host reads the password from
|
|
`/run/secrets/samba_password` (**sops-nix**); the VM falls back to plaintext
|
|
`/etc/samba/smb-password`.
|
|
- Secrets: `secrets/jupiter.yaml` is age-encrypted (safe to commit) to two
|
|
recipients in `.sops.yaml` — the **admin** key (edit on laptop,
|
|
`~/.config/sops/age/keys.txt`) and the **jupiter host** key (derived from its
|
|
SSH host key via `ssh-to-age`, decrypts at runtime). Private keys live
|
|
off-repo and are gitignored. Rotate/add recipients with `sops updatekeys`.
|
|
- Data disk: plain `fileSystems."/mnt/data"` in configuration.nix — kept out of
|
|
disko so it is never formatted. Reference by `by-id` / `by-uuid`.
|
|
- `system.stateVersion` = `26.05`, install-time schema. Do NOT bump on upgrades.
|
|
- Terraform is not used: a single bare-metal box has no provider API. disko +
|
|
nixos-anywhere cover provisioning natively.
|
|
```
|