{ config, ... }: # Headplane — web UI for headscale (services/vpn/headscale.nix; must be enabled # first), running as headscale's own 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. # # 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. # # 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: # 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 # client ID into oidc.client_id below. # 2. `./scripts/edit_secrets secrets/neptun.yaml` and replace # 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 # a missing or unparseable file. systemd.tmpfiles.rules = [ "d /var/lib/headscale 0750 headscale headscale -" "f /var/lib/headscale/extra_records.json 0640 headscale headscale - []" ]; services.headscale.settings.dns.extra_records_path = "/var/lib/headscale/extra_records.json"; services.headplane = { enable = true; settings.headscale.dns_records_path = "/var/lib/headscale/extra_records.json"; settings.server = { cookie_secret_path = config.sops.secrets.headplane_cookie_secret.path; cookie_secure = true; # served over HTTPS via Caddy base_url = "https://vpn.mgaction.town"; }; settings.oidc = { issuer = "https://auth.mgaction.town/application/o/headplane/"; # Not a secret — Authentik hands the client ID out at the authorize # endpoint. Regenerating the provider in Authentik changes it. client_id = "NNzYUrSBlCqxyCTfxlEJRpT5v5EQWHLpfOsXBine"; client_secret_path = config.sops.secrets.headplane_oidc_client_secret.path; headscale_api_key_path = config.sops.secrets.headplane_headscale_api_key.path; }; }; }