docs: condense comments across the repo
Comments had drifted into multi-paragraph narrative (git commit lineage, debugging stories, restated code) in several hot spots (scripts/deploy, hermes-agent.nix, flake.nix, gitea.nix, headscale.nix). Trim every comment to its load-bearing "why" — gotchas, safety warnings, and non-obvious rationale survive verbatim in substance, just tightened to 1-2 sentences; historical narrative and anything already covered in CLAUDE.md is cut. No code/logic changed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UJqEmY1y3AYX3JoX4Y6b21
This commit is contained in:
+12
-20
@@ -1,25 +1,20 @@
|
||||
{ config, ... }:
|
||||
|
||||
# Headplane — web UI for headscale (services/vpn/headscale.nix; must be enabled
|
||||
# first), running as headscale's own OS user.
|
||||
# Headplane — web UI for headscale (services/vpn/headscale.nix; enable first),
|
||||
# running as headscale's OS user.
|
||||
#
|
||||
# It reads headscale's config from the nix store, which is read-only — so the
|
||||
# UI DISPLAYS the settings but can't change them. That's the intended shape
|
||||
# for a declaratively-configured box (config_strict already defaults off
|
||||
# upstream for exactly this reason); edit them here and rebuild instead.
|
||||
# DNS extra-records are the one thing worth making editable, since they're
|
||||
# data rather than config — hence the writable extra_records file below,
|
||||
# which also spares headplane from restarting headscale on every change.
|
||||
# It reads headscale's config from the nix store, so the UI DISPLAYS settings
|
||||
# but can't change them (edit here and rebuild instead) — except DNS
|
||||
# extra-records, which are data rather than config, hence the writable
|
||||
# extra_records file below.
|
||||
#
|
||||
# Served at vpn.mgaction.town/admin (path-routed alongside headscale itself,
|
||||
# see hosts/neptun/configuration.nix). base_url is the site root WITHOUT the
|
||||
# /admin prefix — Headplane appends that itself, including for the OIDC
|
||||
# callback.
|
||||
# Served at vpn.mgaction.town/admin (path-routed with headscale, see
|
||||
# hosts/neptun/configuration.nix); base_url excludes the /admin prefix, which
|
||||
# Headplane appends itself including for the OIDC callback.
|
||||
#
|
||||
# Auth is Authentik (services/identity/authentik.nix) via OIDC. client_id,
|
||||
# client_secret, and the headscale API key can't be known until
|
||||
# Authentik/headscale are actually deployed, so they're placeholders below;
|
||||
# direct API-key login still works as a fallback until then. Once live:
|
||||
# Auth is Authentik via OIDC; client_id/client_secret/API key are placeholders
|
||||
# until Authentik/headscale are deployed (direct API-key login works as a
|
||||
# fallback until then). Once live:
|
||||
# 1. In Authentik: create an OAuth2/OpenID Provider + Application with slug
|
||||
# `headplane` and redirect URI
|
||||
# https://vpn.mgaction.town/admin/oidc/callback. Copy the generated
|
||||
@@ -28,9 +23,6 @@
|
||||
# headplane_oidc_client_secret with the provider's client secret.
|
||||
# 3. `headscale apikeys create` on the box, and replace
|
||||
# headplane_headscale_api_key the same way.
|
||||
#
|
||||
# NOTE: Authentik issues per-application, so the issuer carries the app slug —
|
||||
# it is NOT the bare host the way Zitadel's was.
|
||||
{
|
||||
# Writable DNS extra-records, shared by both services (they run as the same
|
||||
# user). tmpfiles seeds an empty JSON array — headscale won't start against
|
||||
|
||||
+39
-77
@@ -1,14 +1,10 @@
|
||||
{ config, ... }:
|
||||
|
||||
# Headscale — self-hosted control server for the tailnet. Every host's
|
||||
# services/vpn/tailscale.nix points --login-server at https://vpn.mgaction.town
|
||||
# (this host). MagicDNS base_domain "orbit.sol" matches the
|
||||
# "jupiter.orbit.sol" names used in this repo's Caddy vhosts
|
||||
# (hosts/neptun/configuration.nix) — changing base_domain means changing
|
||||
# those too, and re-pointing neptun's dnsmasq stub at the new suffix.
|
||||
#
|
||||
# TLS terminates at Caddy (see the host's configuration.nix); headscale
|
||||
# itself only listens on localhost.
|
||||
# Headscale — self-hosted control server for the tailnet; every host's
|
||||
# services/vpn/tailscale.nix points --login-server at https://vpn.mgaction.town.
|
||||
# TLS terminates at Caddy; headscale itself only listens on localhost. Changing
|
||||
# base_domain below also means updating this repo's Caddy vhosts and neptun's
|
||||
# dnsmasq stub, which assume "orbit.sol".
|
||||
{
|
||||
services.headscale = {
|
||||
enable = true;
|
||||
@@ -18,89 +14,55 @@
|
||||
server_url = "https://vpn.mgaction.town";
|
||||
|
||||
dns = {
|
||||
# Deliberately OUTSIDE mgaction.town. That zone has a wildcard A+AAAA
|
||||
# pointing at neptun, and DNS wildcards match multi-label names — so
|
||||
# with base_domain = hosts.mgaction.town, `jupiter.hosts.mgaction.town`
|
||||
# resolved publicly to NEPTUN and Caddy proxied to itself: a silent
|
||||
# loop rather than a lookup failure.
|
||||
#
|
||||
# `.sol` is the LAN domain pihole serves, so this nests the tailnet
|
||||
# inside it: planets sit on the LAN as jupiter.sol, and reach each
|
||||
# other in orbit as jupiter.orbit.sol. Resolution is unambiguous
|
||||
# because tailscale matches routes by LONGEST suffix, so orbit.sol
|
||||
# goes to MagicDNS even when everything else funnels to pihole.
|
||||
#
|
||||
# Never give a LAN host the name `orbit`: pihole's
|
||||
# `address=/<host>.sol/<ip>` lines match a name AND everything under
|
||||
# it, so an `orbit` host would swallow this entire zone.
|
||||
# Deliberately outside mgaction.town: that zone has a wildcard A+AAAA at
|
||||
# neptun, so a name under it would resolve publicly to neptun and Caddy
|
||||
# would proxy to itself. Nested under `.sol` (pihole's LAN domain) so
|
||||
# jupiter.sol (LAN) and jupiter.orbit.sol (tailnet) resolve unambiguously
|
||||
# — tailscale matches by longest suffix. Never name a LAN host `orbit`:
|
||||
# pihole's `address=/<host>.sol/<ip>` would swallow this whole zone.
|
||||
base_domain = "orbit.sol";
|
||||
# pihole on mercury, over the tailnet — so every roaming device gets
|
||||
# ad blocking and .sol names wherever it is, not just on the LAN.
|
||||
# Deliberately NO public fallback: tailscale treats the list as a set,
|
||||
# so adding 9.9.9.9 here would let queries slip past the filter
|
||||
# whenever mercury is briefly slow. Strict blocking, at the cost of
|
||||
# mercury being a single point of failure for tailnet DNS.
|
||||
#
|
||||
# ⚠️ A hardcoded tailnet address, so it changes if mercury re-enrols
|
||||
# — check `headscale nodes list` if DNS dies tailnet-wide.
|
||||
# pihole on mercury, over the tailnet, so roaming devices get ad blocking
|
||||
# and .sol names everywhere. Deliberately no public fallback — tailscale
|
||||
# treats this as a set, so adding one would let queries slip past the
|
||||
# filter whenever mercury is briefly slow, at the cost of mercury being a
|
||||
# single point of failure for tailnet DNS.
|
||||
# ⚠️ Hardcoded tailnet address — check `headscale nodes list` if it
|
||||
# changes (mercury re-enrolled) and DNS dies tailnet-wide.
|
||||
nameservers.global = [ "100.64.0.7" ];
|
||||
|
||||
# Must be set, and must be HERE rather than via the module's
|
||||
# `dns.split` option. nixpkgs renders that option one level too high
|
||||
# (a sibling of `nameservers:`), but headscale reads
|
||||
# dns.nameservers.split (hscontrol/types/config.go:722) and so does
|
||||
# headplane. So the module's option is dead, and the missing key makes
|
||||
# headplane's DNS page die with
|
||||
# TypeError: Cannot convert undefined or null to object
|
||||
# from Object.keys(config.dns.nameservers.split).
|
||||
# Must be set here, not via the module's `dns.split` option — nixpkgs
|
||||
# renders that one level too high, but headscale (and headplane) read
|
||||
# dns.nameservers.split; the missing key crashes headplane's DNS page.
|
||||
nameservers.split = { };
|
||||
|
||||
# Point every node's resolver at MagicDNS, which forwards on to the
|
||||
# global nameserver above. That is the only way to get pihole onto a
|
||||
# roaming device: with this false, globalResolvers land in the
|
||||
# netmap's FallbackResolvers (hscontrol/types/config.go:826-830) and a
|
||||
# phone with carrier DNS never consults them.
|
||||
#
|
||||
# The cost is that every node's DNS now depends on mercury and on the
|
||||
# home connection, so mercury going down costs name resolution
|
||||
# everywhere, not just `.sol`. neptun and mercury opt out of this
|
||||
# individually with --accept-dns=false — see their configuration.nix.
|
||||
# Routes every node's resolver through MagicDNS to the global nameserver
|
||||
# above — the only way pihole reaches a roaming device (otherwise it
|
||||
# lands in netmap's FallbackResolvers and carrier DNS never consults it).
|
||||
# Cost: all DNS now depends on mercury and the home connection; neptun
|
||||
# and mercury opt out individually with --accept-dns=false.
|
||||
override_local_dns = true;
|
||||
};
|
||||
|
||||
# Authentik as the login provider, so `tailscale up --login-server ...`
|
||||
# sends you to a browser instead of needing a pre-auth key. This is a
|
||||
# SEPARATE Authentik application from headplane's — its own provider,
|
||||
# slug `headscale`, redirect https://vpn.mgaction.town/oidc/callback
|
||||
# (headscale's own callback; headplane's is under /admin).
|
||||
#
|
||||
# ⚠️ headscale performs OIDC discovery at STARTUP and a failure is
|
||||
# FATAL ("creating OIDC provider from issuer config: 404 Not Found") —
|
||||
# it will not boot, taking the whole tailnet's control plane with it.
|
||||
# Never point `issuer` at an application that doesn't exist yet; verify
|
||||
# with:
|
||||
# Authentik as the login provider (own application, slug `headscale`,
|
||||
# separate from headplane's) so `tailscale up --login-server ...` opens a
|
||||
# browser instead of needing a pre-auth key; headless hosts still use those.
|
||||
# ⚠️ headscale does OIDC discovery at startup and a failure is fatal — it
|
||||
# won't boot, taking the whole control plane with it. Never point `issuer`
|
||||
# at an application that doesn't exist yet; verify with
|
||||
# curl -s <issuer>.well-known/openid-configuration
|
||||
#
|
||||
# Headless hosts still enrol with pre-auth keys. Note also that users
|
||||
# created here are distinct from `headscale users create` ones: matching
|
||||
# is by the OIDC `sub` claim against the user's providerId, and 0.28
|
||||
# dropped map_legacy_users, so CLI-made users never gain one.
|
||||
# Users created here are matched by OIDC `sub`, so `headscale users
|
||||
# create`-made users never link to one (0.28 dropped map_legacy_users).
|
||||
oidc = {
|
||||
issuer = "https://auth.mgaction.town/application/o/headscale/";
|
||||
client_id = "14vhRYaLiONHmI2YFIxbQEveJDLu5cCvzSkTb9oq";
|
||||
client_secret_path = config.sops.secrets.headscale_oidc_client_secret.path;
|
||||
};
|
||||
|
||||
# Run our own DERP relay instead of pulling Tailscale's map.
|
||||
#
|
||||
# With the default (urls = [controlplane.tailscale.com/derpmap/default],
|
||||
# auto_update_enabled = true) headscale fetches that map at startup and
|
||||
# treats failure as FATAL — so a DNS blip or a Tailscale outage stops the
|
||||
# control server from booting at all. A self-hosted control plane that
|
||||
# can't start without Tailscale's infrastructure rather misses the point.
|
||||
#
|
||||
# The relay itself rides Caddy on :443 (hence the flush_interval -1 on
|
||||
# that vhost); only STUN needs its own UDP port.
|
||||
# Run our own DERP relay instead of pulling Tailscale's map: the default
|
||||
# fetches that map at startup and treats a failure as fatal, so a DNS blip
|
||||
# or Tailscale outage would stop this control server from booting at all.
|
||||
# The relay rides Caddy on :443 (hence flush_interval -1 on that vhost);
|
||||
# only STUN needs its own UDP port.
|
||||
derp = {
|
||||
urls = [ ];
|
||||
auto_update_enabled = false;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{ config, ... }:
|
||||
|
||||
# Tailscale node joined to the self-hosted headscale control server.
|
||||
# Auto-registers on boot from a sops pre-auth key. Requires the importing host
|
||||
# to declare `sops.secrets.tailscale_authkey` (see each host's secrets.nix).
|
||||
# Not for the VM (no sops).
|
||||
# Tailscale node joined to the self-hosted headscale control server,
|
||||
# auto-registering on boot from a sops pre-auth key — importing hosts must
|
||||
# declare `sops.secrets.tailscale_authkey` (see each host's secrets.nix).
|
||||
# Not used by the VM target (no sops there).
|
||||
{
|
||||
services.tailscale = {
|
||||
enable = true;
|
||||
@@ -14,11 +14,11 @@
|
||||
# Reach the host's services over the tailnet without opening LAN ports.
|
||||
networking.firewall.trustedInterfaces = [ "tailscale0" ];
|
||||
|
||||
# The upstream unit is a one-shot with no Restart, so a login attempt made
|
||||
# before the control server is reachable fails permanently until someone
|
||||
# starts it by hand. That's the norm on a first boot — neptun hosts headscale
|
||||
# itself, and the other hosts race it. 30s spacing also keeps restarts clear
|
||||
# of systemd's default start limit (5 within 10s).
|
||||
# The upstream unit is a one-shot with no Restart, so a login attempted
|
||||
# before the control server is up fails permanently until restarted by
|
||||
# hand — the norm on first boot, since neptun hosts headscale itself and
|
||||
# other hosts race it. 30s spacing keeps retries clear of systemd's default
|
||||
# start limit (5 within 10s).
|
||||
systemd.services.tailscaled-autoconnect.serviceConfig = {
|
||||
Restart = "on-failure";
|
||||
RestartSec = 30;
|
||||
|
||||
Reference in New Issue
Block a user