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>
87 lines
3.9 KiB
Nix
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 ];
|
|
}
|