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>
64 lines
3.0 KiB
Nix
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;
|
|
};
|
|
};
|
|
}
|