Files
darmanandClaude Opus 4.8 d9e6b6b697 headplane: point OIDC at the real Authentik app; rotate tailnet keys
The Authentik provider and application now exist (slug "headplane", which
is what makes the issuer .../application/o/headplane/), so the client ID
is a real value rather than a placeholder, and the client secret and
headscale API key are in sops.

The tailscale pre-auth keys for neptun and jupiter are rotated because
the tailnet was recreated from scratch: the old headscale database went
with the VPS's OS disk, so every key issued against it is meaningless to
the new control server.

Note the headscale API key defaults to a 90d expiry. When it lapses
headplane stops listing nodes with no obvious cause -- `headscale apikeys
list` shows the date.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 19:41:40 +02:00

64 lines
3.0 KiB
Nix

{ 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;
};
};
}