86 lines
4.2 KiB
Nix
86 lines
4.2 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.
|
|
# TLS terminates at Caddy; headscale itself only listens on localhost. Changing
|
|
# base_domain below also means updating this repo's Caddy vhosts and neptun's
|
|
# dnsmasq stub, which assume "orbit.sol".
|
|
{
|
|
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 = {
|
|
# Deliberately outside mgaction.town: that zone has a wildcard A+AAAA at
|
|
# neptun, so a name under it would resolve publicly to neptun and Caddy
|
|
# would proxy to itself. Nested under `.sol` (pihole's LAN domain) so
|
|
# jupiter.sol (LAN) and jupiter.orbit.sol (tailnet) resolve unambiguously
|
|
# — tailscale matches by longest suffix. Never name a LAN host `orbit`:
|
|
# pihole's `address=/<host>.sol/<ip>` would swallow this whole zone.
|
|
base_domain = "orbit.sol";
|
|
# pihole on mercury, over the tailnet, so roaming devices get ad blocking
|
|
# and .sol names everywhere. Deliberately no public fallback — tailscale
|
|
# treats this as a set, so adding one would let queries slip past the
|
|
# filter whenever mercury is briefly slow, at the cost of mercury being a
|
|
# single point of failure for tailnet DNS.
|
|
# ⚠️ Hardcoded tailnet address — check `headscale nodes list` if it
|
|
# changes (mercury re-enrolled) and DNS dies tailnet-wide.
|
|
nameservers.global = [ "100.64.0.4" ];
|
|
|
|
# Must be set here, not via the module's `dns.split` option — nixpkgs
|
|
# renders that one level too high, but headscale (and headplane) read
|
|
# dns.nameservers.split; the missing key crashes headplane's DNS page.
|
|
nameservers.split = { };
|
|
|
|
# Routes every node's resolver through MagicDNS to the global nameserver
|
|
# above — the only way pihole reaches a roaming device (otherwise it
|
|
# lands in netmap's FallbackResolvers and carrier DNS never consults it).
|
|
# Cost: all DNS now depends on mercury and the home connection; neptun
|
|
# and mercury opt out individually with --accept-dns=false.
|
|
override_local_dns = true;
|
|
};
|
|
|
|
# Authentik as the login provider (own application, slug `headscale`,
|
|
# separate from headplane's) so `tailscale up --login-server ...` opens a
|
|
# browser instead of needing a pre-auth key; headless hosts still use those.
|
|
# ⚠️ headscale does OIDC discovery at startup and a failure is fatal — it
|
|
# won't boot, taking the whole control plane with it. Never point `issuer`
|
|
# at an application that doesn't exist yet; verify with
|
|
# curl -s <issuer>.well-known/openid-configuration
|
|
# Users created here are matched by OIDC `sub`, so `headscale users
|
|
# create`-made users never link to one (0.28 dropped map_legacy_users).
|
|
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: the default
|
|
# fetches that map at startup and treats a failure as fatal, so a DNS blip
|
|
# or Tailscale outage would stop this control server from booting at all.
|
|
# The relay rides Caddy on :443 (hence 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 ];
|
|
}
|