Three connected changes, all triggered by the same outage.
base_domain leaves mgaction.town. That zone has a wildcard A+AAAA pointing
at neptun, and DNS wildcards match multi-label names, so
jupiter.hosts.mgaction.town resolved publicly to NEPTUN and Caddy proxied
to itself -- a silent loop rather than a lookup failure. Nesting the
tailnet inside the LAN domain as orbit.sol keeps the theme and resolves
unambiguously, since tailscale matches routes by longest suffix.
override_local_dns = true with pihole as the only global nameserver, so
roaming devices get ad blocking and .sol names off-LAN. With it false,
globalResolvers land in the netmap's FallbackResolvers, which a phone
with carrier DNS never consults. No public fallback is listed on purpose:
tailscale treats the list as a set, so a second entry would let queries
slip past the filter whenever mercury is slow. The cost is that mercury
is now a single point of failure for tailnet DNS.
neptun and mercury opt out individually. mercury would otherwise resolve
through itself. neptun must not depend on a Pi behind a domestic line to
renew the certificates for the control server every other node needs --
and it is circular besides, since tailscaled has to resolve
vpn.mgaction.town to connect at all. Instead neptun runs a dnsmasq stub
forwarding just orbit.sol to MagicDNS on 100.100.100.100, which tailscaled
answers whenever it is running regardless of --accept-dns. That resolves
jupiter live, so the hardcoded /etc/hosts pin is gone.
Also sets dns.nameservers.split explicitly: nixpkgs renders its own
dns.split option one level too high, but headscale reads
dns.nameservers.split (hscontrol/types/config.go:722) and so does
headplane, whose DNS page dies on the missing key with "Cannot convert
undefined or null to object". The module's option is dead as written.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two changes to the control server, plus the fallout on the hosts.
OIDC via Authentik, so `tailscale up --login-server ...` opens a browser
instead of needing a pre-auth key. This is a second Authentik application,
separate from headplane's, with headscale's own /oidc/callback redirect.
Headless hosts keep using pre-auth keys. Note that headscale runs OIDC
discovery at startup and a failure is FATAL -- pointing `issuer` at an
application that does not exist yet means the control server will not
boot, so verify the discovery document before deploying.
override_local_dns = false, because the upstream default of true replaces
resolv.conf with 100.100.100.100 on every node. That silently broke the
LAN's `.sol` names -- pihole serves those and the global nameservers
return NXDOMAIN for them -- and took ad blocking down with them. It also
made each node's entire DNS depend on tailscaled, which is what had
forced --accept-dns=false onto neptun and mercury individually; both of
those workarounds are now removed, and with MagicDNS resolving properly
again neptun no longer needs its hardcoded /etc/hosts pin for jupiter.
Also serves jellyfin and seerr from jupiter, matching the ports they
already use on its LAN vhosts, and rotates the tailnet pre-auth keys.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mercury was the only host with no tailscale at all -- no module import,
no secret, no key in its sops file. It had been enrolled before the NixOS
migration and silently dropped off the tailnet when it was reflashed with
a config that omitted it.
--accept-dns=false, as on neptun and for a sharper reason: headscale
pushes override_local_dns, so accepting MagicDNS would repoint the LAN's
own DNS server at 100.100.100.100 and make house-wide name resolution
depend on tailscaled being up. This host has already deadlocked once on
boot-time DNS (see CLAUDE.md).
darman_password is also rotated: the account had "!" in /etc/shadow,
because on mercury's first boot the secret wasn't readable yet and
update-users-groups.pl falls back to a locked account. mutableUsers is
true, so no later rebuild ever revisited it and the lock was permanent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Group service modules by category (media, network, vpn, identity,
dev, desktop) to make the growing services/ dir easier to navigate.
containers.nix stays at the top level since it's a shared backend,
not a single-category service.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Shorten verbose multi-paragraph comments to essentials, and drop a
stale claim in common.nix that jupiter kept its own copy of the base
config (it now imports common.nix directly).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- services/pihole.nix: official pihole/pihole:2026.07.2 via podman, host net,
caps NET_ADMIN/NET_RAW/SYS_NICE/CHOWN; FTLCONF_* env config (upstream unbound,
DHCP 50-200, static lease jupiter, .sol domain, local records)
- unbound: resolveLocalQueries=false (was hijacking resolv.conf to :53 -> boot
DNS deadlock; the real root cause of the earlier failures too)
- password via sops FTLCONF env file; /var/lib/pihole created via tmpfiles
- VM-verified: mercury.sol/jupiter.sol/external all resolve, 0 restarts
Pi's vfat partition isn't mounted at runtime (u-boot reads it pre-boot), so
/boot/firmware doesn't exist -> keyFile moved to the always-mounted root fs.
deploy flash now drops it on the ext4 root partition.
pihole.toml is nix-managed read-only so 'pihole setpassword' fails. pihole-FTL
reads FTLCONF_* env vars (override the toml) — render an env file from the sops
secret pihole_webpassword and feed it via EnvironmentFile. Password stays out of
repo/store. VERIFIED in VM: FTLCONF_webserver_api_password -> API login works.
- after dd, if ~/.config/homelab/<config>/age.txt exists, mount the FAT boot
partition and drop it as sops-age.txt (mercury). Key stays off-repo + out of
the store + out of the image; no manual mount step.
- per-host darman_password (distinct hash) in secrets/{jupiter,vps,mercury}.yaml
-> hashedPasswordFile; different console password per host (ssh still key-only)
- mercury: dedicated age key (on boot partition post-flash), sops-nix wired
- AdGuard: module has no secret hook + writable config -> mutableSettings=true,
admin password set via web setup on first boot (never in repo/store)
- nixosConfigurations.mercury: aarch64, sd-image-aarch64, imports common.nix
- static net placeholders (CHANGE-ME), hostname mercury
- DNS service left undecided: commented pihole (services.pihole-ftl) + adguardhome
- no sops yet (add with the service if it needs a secret)