KEY (hub's SSH public key) is now stored in variables.nix as beszelHubKey
and set by enable-agent.nix — no host file ever needs to touch it.
TOKEN was already universal; this removes the last per-host beszel config.
Importing enable-agent.nix in a build type is now the only step needed to
add a new host to beszel monitoring.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace per-host host-token.nix imports with a single beszel-token secret
in secrets/common.yaml, wired once in enable-agent.nix. Host files now only
need services.beszel.agent.environment.KEY — no imports block required.
Delete modules/beszel/host-token.nix (no longer referenced anywhere).
Action needed: run `sops secrets/common.yaml` and add `beszel-token: <value>`
from the beszel hub UI before deploying.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documentation fixes:
- README/AGENTS: rename tailscale-exit-node → tailscale-router, add ha-server
build type and proxmox-ha-server-{1,2} host table rows, add baremetal to
platform list, remove references to non-existent flake-target-refactor-spec.md
and remove-sensetive-info-refactor.md
- docs/auto-installer.md: fix lxc-tailscale-exit-node → lxc-tailscale-router,
add pxe-minimal to the flake outputs list
- variables.nix: fix domainControllerIp comment — IPA is the authoritative DNS
at .253 (Pi-hole is gone), not a forwarding intermediary
Code deduplication:
- Extract duplicate SSH host-key preservation activation scripts from
modules/platforms/lxc.nix and modules/platforms/proxmox.nix into a shared
modules/common/preserve-ssh-host-key.nix; both platforms now import it
- Replace 8-line hand-enumerated NFS export lists in server.nix and ha-server.nix
with a mkNfsExports helper that generates exports from vars.nfsShares — adding
a share to variables.nix now propagates to both exporters automatically
Dead code removal:
- modules/common/configuration.nix: remove leftover NixOS skeleton comments
(hardware-configuration import, grub lines) that were never used
- modules/docker/enable-service.nix: remove commented-out listenOptions and
daemon.settings blocks
- hosts/server/host.nix, hosts/nix-cache/host.nix: remove #DOCKER_HOST comments
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SSSD's dyndns_iface defaults to "*" which registers every interface's IP in
IPA DNS, including all Docker bridge networks (172.x.x.x). This caused
docker.sweet.home to resolve to Docker bridge IPs instead of 192.168.2.225.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
modules/ipa/client.nix is now a self-contained NixOS module (no longer a
parameterized function): it checks builtins.pathExists for
secrets/<hostname>.keytab at eval time and enables itself automatically if
found, making it a no-op for hosts without a keytab.
modules/common/configuration.nix imports it so every host in the flake is
a candidate for IPA enrollment — no per-host wiring needed. Adding a
keytab (via scripts/ipa/create-nixos-ipa-host-account.sh) is now the
only step required to enroll a host.
The module also sets networking.domain and networking.nameservers via
mkDefault when active, so new hosts don't need those set explicitly.
Also:
- Remove explicit IPA imports from hosts/nix-cache and hosts/tailscale-router
- Add secrets/pxe-boot.keytab + creation rule; remove incorrect
secrets/nixos.sweet.home.keytab and its creation rule
- Add .sops.yaml creation rules for all remaining host keytabs
(server, docker, tor-relay, nix-minimal, nixos) so the creation script
can target them without manual .sops.yaml edits
- Fix duplicate tailscale-router.keytab rule and corrupted gui.yaml comment
block in .sops.yaml
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- modules/ipa/client.nix: add AuthorizedKeysCommand so sshd fetches SSH
public keys from IPA via sss_ssh_authorizedkeys, enabling pubkey login
without per-host authorized_keys files
- hosts/tailscale-router/host.nix: add IPA client module + networking.domain
so SSSD runs and wayne can authenticate on this host
- secrets/tailscale-router.keytab: sops-encrypted keytab for
tailscale-router.sweet.home (generated by create-nixos-ipa-host-account.sh)
- .sops.yaml: creation rule for secrets/tailscale-router.keytab
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds modules/ipa/client.nix — a parameterized module that joins a NixOS host
to the sweet.home FreeIPA domain without ipa-client-install. It configures
security.ipa (SSSD, Kerberos, PAM, NSSwitch) and places a pre-provisioned host
keytab via sops-nix binary secret so enrollment is fully reproducible from the
flake.
- variables.nix: adds ipaServer (FQDN of the FreeIPA KDC; security.ipa.server
requires a hostname, not an IP, for Kerberos/TLS)
- certs/ipa-ca.crt: placeholder for the IPA CA public certificate (operator
replaces with: curl http://<ipa-server>/ipa/config/ca.crt)
- secrets/nix-cache.keytab: placeholder binary sops file (operator replaces
with the encrypted keytab after ipa host-add + ipa-getkeytab)
- .sops.yaml: adds creation rule for secrets/nix-cache.keytab (same recipients
as secrets/nix-cache.yaml)
- hosts/nix-cache/host.nix: imports the IPA client module; adds
networking.domain so the host's FQDN resolves correctly
Module header documents the three operator steps needed per host before deploy.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds static IP configuration to every NixOS host in the flake that
has a fixed LAN address, and centralises all network primitives
(IPs, gateway, prefix length, interface names) in variables.nix so
there is one place to update if any of them change.
variables.nix additions:
- lanGateway / lanPrefixLength — LAN gateway and /24 prefix, replacing
every hardcoded 192.168.2.254 / 24 across host files
- lxcLanInterface / vmLanInterface / vmStorageInterface — NIC names for
LXC containers (eth0), Proxmox VMs (ens18), and the HA storage NIC
(ens19), used as attribute keys so changing the name is a one-line edit
- haStoragePrefixLength — /29 for the storage subnet, mirrors haStorageCidr
- Per-host IP variables: nixCacheIp (.224), tailscaleRouterIp (.222),
torRelayIp (.221), serverIp (.226), dockerIp (.225)
host.nix changes:
- tailscale-router, tor-relay, nix-cache, pxe-boot: useDHCP = false,
static address on eth0 (lxcLanInterface), struct-form defaultGateway
(required when using systemd-networkd which LXC containers use)
- server, docker: useDHCP = false, static address on ens18 (vmLanInterface),
struct-form defaultGateway (works for both scripted networking and networkd)
- ha-server-1, ha-server-2: replace hardcoded 192.168.2.254 / 24 / 29
with the new variables; no functional change for these hosts
modules/build-types/pxe-boot.nix:
- Domain-controller kickstart template: replace hardcoded 192.168.2.138
and 192.168.2.254 with vars.domainControllerIp / vars.lanGateway /
vars.lanPrefixLength / vars.homeDomain so the template stays correct
if the DC IP or domain is ever changed again
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ULXzafSDwGhmFGnn3LtDSQ
Adds proxmox-ha-server-1 and proxmox-ha-server-2 as real mkTarget entries
alongside the existing proxmox-server, backed by a new ha-server build type.
New modules
modules/ha/cluster-config.nix — DRBD resource + corosync nodelist sourced
from vars (haServer1Host/Ip, haServer2Host/Ip); resource-only fencing for
production STONITH; HA port firewall rules for DRBD, iSCSI, Corosync, pcsd
modules/build-types/ha-server.nix — imports pacemaker-stack + iscsi-target
+ cluster-config + beszel; NFS exports from vars.haStorageRoot (XFS-over-DRBD
mount); nfs-server.service.wantedBy force-cleared so Pacemaker controls
start/stop on the Active node only
New hosts
hosts/ha-server-{1,2}/host.nix — static IP from vars, unique hostId; sops
secrets (beszel, corosync authkey) are TODOs pending sync-host-keys.sh
variables.nix
haServer1/2Host, haServer1/2Ip, haServerVip, haStorageRoot, haIscsiIqn
ports.haServerDrbd/Iscsi/Corosync{1,2,Crypto}/PacemakerRemoted/Pcsd
scripts/ha/ (migrated + updated from test-lab/ha/)
cluster-init.sh — generates corosync authkey, initialises DRBD/XFS/iSCSI,
creates NFS dataset dirs, configures Pacemaker with DRBD + XFS + iSCSI
+ nfs-server + VIP; STONITH disabled initially (enable separately)
cluster-enable-stonith.sh — enables fence_pve_ssh STONITH after key deploy
fence-pve-ssh.py — Proxmox SSH fence agent (node names updated to ha-server-1/2)
acceptance-tests.sh — T1–T7 production acceptance tests
test-lab/ha/ removed — all Nix config moved to modules/ha/ and
modules/build-types/; scripts moved to scripts/ha/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HaH1cSGvhogRP5ExoF6nD8
SOPS_AGE_KEY_FILE was set in hosts/nixos/home.nix sessionVariables with a
literal ~ that Home Manager injects as-is into the environment. In bash,
tilde expansion does not happen inside double-quoted variable references, so
DEFAULT_SOPS_AGE_KEY_FILE resolved to ~/... literally and the -s file-existence
check in ensure_admin_decrypt_key silently failed. The script then generated
a brand-new age key (to ~/... relative to the repo root) while the real admin
key at ~/.config/sops/age/keys.txt went untouched -- making it appear the key
was lost when it was actually still intact.
Fix the home.nix root cause by using config.home.homeDirectory so the path
is fully resolved. Add tilde expansion in ensure_admin_decrypt_key as a
belt-and-suspenders guard for any caller whose environment has the same issue.
Also replace the auto-generate-a-new-key fallback with a hard failure: auto-
generating a new admin key is never useful (it cannot decrypt existing secrets)
and created serious confusion about whether the original key was lost.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wires beszel-agent into all tailscale-router variants (lxc/linode/proxmox)
by importing enable-agent.nix in the build type and host-token.nix in the
host file. Adds the sops creation rule for secrets/tailscale-router.yaml
(all three platform variants as recipients). The secrets file must be
created manually before deploying — see instructions in PR.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The directory name was tailscale-subnet-router but networking.hostName
was already tailscale-router, causing a mismatch that confused scripts
comparing directory paths against actual hostnames. Flake attribute
names (linode/proxmox/lxc-tailscale-subnet-router) and .sops.yaml
anchors are unchanged — they describe the build type, not the hostname.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The host was always intended as a LAN subnet router (--advertise-routes),
not a full exit node (--advertise-exit-node). Rename every trace of
"exit-node" to "subnet-router" and remove the --advertise-exit-node flag
from extraSetFlags; the operator supplies --advertise-routes at first
tailscale up and Tailscale persists it in state across reboots.
Routing sysctls (useRoutingFeatures = "server"), openFirewall, and
trustedInterfaces = ["tailscale0"] are still required for subnet routing
to work, so the module is kept — just correctly named.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TF2dsuKZAiyZWQ1D7CuHJm
- Add hosts/tor-relay/host.nix import of host-token.nix so the agent
gets its TOKEN from a sops-managed environment file
- Add secrets/tor-relay.yaml (encrypted beszel token for this host)
- Add creation_rules entry for secrets/tor-relay.yaml in .sops.yaml
- Update &lxc-tor-relay age key to the host's actual current key
(old key was from a prior LXC incarnation; new key extracted from
Switch-nix output: age1gl5ujmhd2pe37...)
- Re-encrypt secrets/common.yaml via sops updatekeys to swap in the
new key, so the host can decrypt its password hash on next boot
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wires everything staged so far into a real flake target:
- modules/platforms/baremetal.nix (new): the bare-metal platform module,
composed from a real nixos-generate-config run on the actual gui-host
hardware (AMD CPU, ahci/xhci/usb storage -- modules/hardware-configuration/baremetal.nix).
Enables hardware.enableRedistributableFirmware (real wifi/GPU/microcode
firmware VMs never needed), amdgpu as the Xorg video driver plus
hardware.graphics for Mesa OpenGL/Vulkan, and imports the ZFS RAID0 disko
layout + modules/services/zfs/enable-service.nix for root-on-ZFS boot
support.
- flake.nix: new baremetal-gui target, reusing hosts/nixos/host.nix (same
identity already shared across linode/proxmox/lxc-gui).
- hosts/nixos/host.nix: added networking.hostId, required now that a ZFS
root pool is in the picture.
- variables.nix: guiRootDisk1/guiRootDisk2 filled in (/dev/sda, /dev/sdb --
only used transiently at disko-format time, same as modules/disko/proxmox.nix's
own plain device path). wifiPassword removed.
- modules/networking/wifi.nix: reworked to pull the wifi password from a
new sops secret (secrets/gui.yaml, wifi-password) instead of a plaintext
variable -- NetworkManager's ensureProfiles renders `psk = "$WIFI_PASSWORD"`
literally (nixpkgs' own documented pattern for this) and envsubst-expands
it from a sops-rendered EnvironmentFile at activation, so the real value
never touches the Nix store, only /run.
- .sops.yaml: new secrets/gui\.yaml rule, admin + the currently-registered
lxc-gui recipient (the only gui variant with a provisioned host key so
far -- whichever variant is actually deployed next still needs
scripts/secrets/sync-host-keys.sh run for its own recipient).
- README.md/CLAUDE.md: documented the new platform/target and its module
layout, per this repo's own drift-prevention note.
Verified end-to-end: nix eval of every existing target (nothing broke),
a temporary real nixosSystem build against the actual disko.nixosModules.disko
confirming the generated zpool create has no mirror/raidz keyword (genuine
stripe), and a temporary test SSID confirming the sops secret/template/
ensureProfiles chain renders correctly before reverting to blank/real values.
Full scripts/codex-maintenance.sh (secret-grep, fmt, statix, full-fallback
eval of every host/package) passes clean.
Adds modules/networking/wifi.nix using NetworkManager's ensureProfiles
mechanism so the gui host associates to a known SSID on first boot with
no manual nmtui step. Credentials are placeholders in variables.nix
(wifiSsid/wifiPassword, both empty) to be filled in once the bare-metal
hardware profile is wired up — the module is a no-op until then.
New tor-relay build type (currently lxc-only) running a plain Tor
middle relay via modules/tor/enable-relay.nix, plus nyx for
interactive monitoring over the relay's control socket.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New build type dedicated to Tailscale exit-node capability, wired up for
linode/proxmox/lxc like every other build type (the lxc variant is the one
actually intended for deployment). Kept separate from the "server" host
rather than bundling exit-node capability onto it.
Trimmed modules/tailscale/exit-node.nix down to pure exit-node behavior:
dropped the old --advertise-routes=${vars.lanCidr} bundling (meaningless
for a Linode-hosted VPS with no path to the LAN), and switched
extraUpFlags -> extraSetFlags. Confirmed against nixpkgs' tailscale.nix
that extraUpFlags is only applied by tailscaled-autoconnect, which itself
only runs when services.tailscale.authKeyFile is set -- nothing in this
repo sets one, so the old flags would never have actually been applied.
extraSetFlags runs unconditionally via tailscaled-set on every boot, so
--advertise-exit-node self-reapplies once the operator has done the
one-time manual `tailscale up` auth.
Verified: all three new targets eval cleanly, nixpkgs-fmt/statix clean,
and a dry-run build of lxc-tailscale-exit-node's tarball resolves its full
closure including tailscaled-set.service.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01La55Nsss8jZ7ZuzUV9mfot
Adds nested vars.nfsShares (subpath + mountpoint per dataset, previously
duplicated independently across server.nix's NFS exports, mount-data.nix's
client mounts, docker.nix's tmpfiles rules, traefik's log rotation path,
and hosts/server/host.nix's beszel config), vars.ports (every literal port
in modules/ and hosts/, kept as separate entries per service even where
numbers coincide so changing one can't silently change another), plus
vars.proxmoxImageSize, vars.nixCacheGcMaxAge, vars.traefikLogRotate, and
raspberryPiHost/raspiNfsPath/raspiMountpoint for the Pi's own NFS export.
Also fixes docker.nix/minimal.nix/gui.nix hardcoding the literal "nixos"
username instead of the existing vars.primaryUser, found during the sweep.
system.stateVersion is deliberately left untouched everywhere -- per
NixOS's own docs that value must stay fixed from first install, not
follow any shared variable.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01La55Nsss8jZ7ZuzUV9mfot
Zero W20 (repeated attribute keys), W10 (empty { ... }: variadic
pattern, use _: instead), and W04 (a = x.a instead of inherit)
warnings remain anywhere in the tree, and nixpkgs-fmt --check is
clean on all 46 .nix files.
Repeated-key merges go as deep as statix actually flags per file
(e.g. boot.loader.* nested under boot.loader = { ... } once the
outer boot.* merge exposed it as its own repeat) — every merge is a
pure attribute-path restructuring with no value changes, verified by
comparing config.system.build.toplevel.drvPath before/after for a
representative host per changed module plus a full 19-host + 4-package
eval sweep.
One indentation slip caught and fixed during this pass: nesting
modules/installer/common.nix's environment.etc."auto-install.sh".text
under an environment = { ... } block initially normalized the
script's shebang/set line indentation, which actually changes the
rendered file (Nix's '' string dedent treats it as real content, not
cosmetic whitespace) — reproduced the original's exact indentation
and reverified the rendered script is byte-identical to before.
modules/services/zfs/auto-mount-volumes.nix picked up formatting too;
worth noting it isn't imported by anything in this flake at all.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01La55Nsss8jZ7ZuzUV9mfot
hosts/server/host.nix and hosts/nix-cache/host.nix each hand-rolled the
same sops secret/template/environmentFile wiring for the beszel agent
token, differing only in the sops file path and template name. Factor
it into modules/beszel/host-token.nix ({ name, sopsFile }) so a third
host can adopt it without copy-pasting the boilerplate again. Also
drops two dead, stale commented-out HUB_URL lines left over from
before variables.nix grew a homeDomain var.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01La55Nsss8jZ7ZuzUV9mfot
Pulls the beszel hub / PVE / PBS ports, the docker-compose host's LAN
name, and the remote-builder client SSH keys out of scattered inline
literals across modules/hosts and into variables.nix as the single
source of truth, matching the existing pattern for other cross-host
references (nixCacheHost, nfsServerHost).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01La55Nsss8jZ7ZuzUV9mfot
linode-minimal, proxmox-minimal, and lxc-minimal now all share
hosts/nix-minimal/host.nix instead of three separate per-platform
host files with different hostnames — every minimal-buildtype host is
named nix-minimal regardless of which platform it runs on.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
One file (variables.nix) holding every value that was previously
hardcoded and repeated across modules: LAN domain/CIDR, home/tailnet
domains, cross-host references (nix-cache substituter hostname, NFS
server hostname, remote-builder user), PXE/PBS IPs, timezone, and the
primary username.
Wired in via flake.nix's specialArgs (and home-manager's
extraSpecialArgs for the two home.nix files), so any module picks it
up by just adding `vars` to its function arguments — no explicit
import needed. Two hosts (nix-cache, server) now derive their own
networking.hostName from the same variable other hosts use to reach
them, so there's exactly one place to change either identifier.
Purely mechanical: every substituted value matches what was already
there, confirmed by identical toplevel .drv paths for all 17 targets
before and after.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Audited the working tree and full git history for committed secrets
(gitleaks + trufflehog + manual grep, see secrets-inventory.md, kept
local/gitignored per the spec). Found: a password hash shared by root
and the nixos user across every host, two live Beszel monitoring
tokens, and a GitHub fine-grained PAT embedded in a home-manager
nix.conf.
Migrates all of them to sops-nix:
- .sops.yaml + secrets/*.yaml, encrypted for admin + the age keys
derived (via ssh-to-age) from each live host's existing SSH host
key — no new key material transferred to any machine.
- users.users.{root,nixos}.hashedPasswordFile replaces the inline
hashedPassword shared by every target.
- The GitHub PAT moves from a home-manager-managed, store-visible
nix.conf to a sops.templates-rendered file included via nix.conf's
native !include, system-wide instead of per-user.
- Beszel TOKEN moves from `environment` (store-visible) to
`environmentFile` (runtime-only via sops.templates); the dead
commented-out docker token is removed from the tree entirely.
Added a tracked pre-commit hook (gitleaks protect --staged, wired via
core.hooksPath) so a secret can't be committed by accident again, and
documented the sops workflow in README.md.
Structural verification only: all 17 flake targets evaluate, and
`nix build --dry-run --no-link` succeeds for the three currently
deployed hosts. Per CLAUDE.md, actual `nixos-rebuild switch` — the
step that confirms secrets decrypt and services start on a real
machine — is left for manual verification.
Git history still contains the original plaintext secrets; scrubbing
history (Milestone 3) and rotating every credential (Milestone 4) are
separate, deliberately gated steps per remove-sensetive-info-refactor.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Generates all nixosConfigurations from mkTarget(platform, buildType,
hostPath) instead of hand-written per-host blocks, so adding a new
platform or build type is a one-line addition. Per-machine identity
(hostname, hostId, secrets, stateVersion) moves into hosts/<name>/host.nix;
platform-specific config (hardware, boot, networking) into
modules/platforms/*.nix; build-type config (minimal/server/docker/gui/
nix-cache/pxe-boot) into modules/build-types/*.nix.
Old flat targets (nixos, docker, server, nix-cache, nix-minimal, pxe-boot)
are replaced by the 17-target <platform>-<buildtype> matrix; each new
target was verified to evaluate before its old counterpart was removed.
CI workflows and docs/aliases now discover hosts dynamically via
nixosConfigurations attrNames and /etc/flake-target instead of hardcoded
lists, so they can't drift from flake.nix again.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>