Files
homelab/services/vpn/headscale.nix
T
darmanandClaude Opus 4.8 564dfb16b8 headscale: add OIDC login, stop overriding clients' local DNS
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>
2026-07-20 21:12:31 +02:00

87 lines
3.9 KiB
Nix

{ 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 "hosts.mgaction.town" matches the
# "jupiter.hosts.mgaction.town" names used in this repo's Caddy vhosts
# (e.g. hosts/neptun/configuration.nix) — don't change one without the other.
#
# TLS terminates at Caddy (see the host's configuration.nix); headscale
# itself only listens on localhost.
{
services.headscale = {
enable = true;
port = 8082; # off the default 8080 to stay clear of other web apps
settings = {
server_url = "https://vpn.mgaction.town";
dns = {
base_domain = "hosts.mgaction.town";
nameservers.global = [ "1.1.1.1" "9.9.9.9" ];
# Leave each client's own resolvers alone; only route base_domain to
# MagicDNS. Upstream defaults this to true, which replaces resolv.conf
# with 100.100.100.100 on every node — that silently breaks the LAN's
# `.sol` names (pihole on mercury serves those, and the global
# nameservers above return NXDOMAIN for them) and takes ad blocking
# with it. It also makes a node's entire DNS depend on tailscaled
# being up, which is what forced --accept-dns=false onto neptun and
# mercury individually.
override_local_dns = false;
};
# 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:
# 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.
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.
derp = {
urls = [ ];
auto_update_enabled = false;
server = {
enabled = true;
region_id = 999; # 900-999 is the custom range
region_code = "neptun";
region_name = "neptun";
stun_listen_addr = "0.0.0.0:3478";
automatically_add_embedded_derp_region = true;
};
};
};
};
# STUN for the embedded DERP server above. Also needs a matching inbound-UDP
# rule in netcup's edge firewall — it is stateless and defaults to denying
# inbound UDP outright, which silently kills every DNS/NTP reply too.
networking.firewall.allowedUDPPorts = [ 3478 ];
}