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>
This commit is contained in:
@@ -28,19 +28,9 @@
|
||||
networking.interfaces.eth0.ipv6.addresses = [
|
||||
{ address = "fd18:df17:9078:0::10"; prefixLength = 64; }
|
||||
];
|
||||
# VERIFY with `ip route` on the Pi — assuming the router is .1.
|
||||
networking.defaultGateway = { address = "10.0.0.1"; interface = "eth0"; };
|
||||
# Resolvers for the Pi's OWN lookups (not the LAN service). Kept public so the
|
||||
# host resolves during boot without depending on its own pihole/unbound.
|
||||
networking.nameservers = [ "1.1.1.1" "9.9.9.9" ];
|
||||
|
||||
# Never take MagicDNS on THIS host. headscale pushes override_local_dns, so
|
||||
# accepting it would repoint mercury's resolv.conf at 100.100.100.100 and
|
||||
# make the LAN's DNS server depend on tailscaled to resolve anything — the
|
||||
# same boot-time DNS deadlock this host already hit once (see CLAUDE.md),
|
||||
# except a failure here takes the whole network's DNS down with it.
|
||||
services.tailscale.extraUpFlags = [ "--accept-dns=false" ];
|
||||
|
||||
# ---- pihole web admin password (from sops) ----
|
||||
# The pihole container reads FTLCONF_* env vars. Render an env file from the
|
||||
# sops secret and feed it to the container — password stays out of repo/store.
|
||||
|
||||
@@ -43,20 +43,6 @@
|
||||
networking.defaultGateway6 = { address = "fe80::1"; interface = "eth0"; };
|
||||
networking.nameservers = [ "9.9.9.9" "1.1.1.1" "2620:fe::fe" ];
|
||||
|
||||
# ---- Don't take MagicDNS from our own control server ----
|
||||
# headscale pushes override_local_dns, so joining the tailnet would point
|
||||
# neptun's resolv.conf at a MagicDNS served by the tailscaled neptun itself
|
||||
# hosts. A tailscaled failure would then also kill DNS, and with it ACME
|
||||
# renewal — expiring the certs for the very control server every other node
|
||||
# needs to recover. Keep the public resolvers above and resolve the one
|
||||
# tailnet name we proxy to from /etc/hosts instead.
|
||||
services.tailscale.extraUpFlags = [ "--accept-dns=false" ];
|
||||
|
||||
# ⚠️ PLACEHOLDER IP — replace once jupiter first enrols, with its address
|
||||
# from `headscale nodes list` on this box. Until then the audiobookshelf
|
||||
# vhost and the gitea SSH forward below both fail to connect.
|
||||
networking.hosts."100.64.0.2" = [ "jupiter.hosts.mgaction.town" ];
|
||||
|
||||
# firewall (enable + 22), caddy (80/443), tailscale (trust tailscale0 + join
|
||||
# headscale) come from ../../common.nix and ../../services/{caddy,tailscale}.nix.
|
||||
|
||||
@@ -71,21 +57,49 @@
|
||||
# ---- Public reverse proxy vhosts ----
|
||||
# Caddy gets automatic public HTTPS (Let's Encrypt) for real domains; the
|
||||
# *.mgaction.town wildcard already points every name here (A + AAAA), and
|
||||
# the module opens 80/443. Both of these live on jupiter and are reached
|
||||
# over the tailnet — see the /etc/hosts pin above.
|
||||
#
|
||||
# `abs`, not `audiobookshelf`: that's the name this has always been served
|
||||
# under, and the mobile app is configured with it.
|
||||
# the module opens 80/443. All of these live on jupiter and are reached over
|
||||
# the tailnet by their MagicDNS name, which resolves because headscale no
|
||||
# longer overrides local DNS (see services/vpn/headscale.nix).
|
||||
|
||||
# ---- Audiobookshelf ----
|
||||
services.caddy.virtualHosts."abs.mgaction.town".extraConfig = ''
|
||||
reverse_proxy http://jupiter.hosts.mgaction.town:8000
|
||||
'';
|
||||
|
||||
# ---- Seerr ----
|
||||
services.caddy.virtualHosts."seerr.mgaction.town".extraConfig = ''
|
||||
reverse_proxy http://jupiter.hosts.mgaction.town:5055
|
||||
'';
|
||||
|
||||
# ---- Jellyfin ----
|
||||
services.caddy.virtualHosts."jellyfin.mgaction.town".extraConfig = ''
|
||||
reverse_proxy http://jupiter.hosts.mgaction.town:8096
|
||||
'';
|
||||
|
||||
# ---- Gitea WebUI ----
|
||||
# Gitea's web UI and HTTPS clones (services/dev/gitea.nix, HTTP_PORT 3000).
|
||||
# Its SSH side is the separate :2222 forward further down.
|
||||
services.caddy.virtualHosts."git.mgaction.town".extraConfig = ''
|
||||
reverse_proxy http://jupiter.hosts.mgaction.town:3000
|
||||
'';
|
||||
# The apex mgaction.town is deliberately not served — it returns Caddy's
|
||||
# default 404.
|
||||
|
||||
# ---- Gitea SSH forward ----
|
||||
# Caddy only proxies HTTP; forward :2222 over the tailnet to gitea's own
|
||||
# SSH server on jupiter (services/dev/gitea.nix), so
|
||||
# `ssh://git@git.mgaction.town:2222/...` works. Also needs a matching
|
||||
# inbound-2222 rule in netcup's edge firewall panel (not managed by Nix).
|
||||
systemd.services.gitea-ssh-forward = {
|
||||
description = "Forward :2222 to jupiter's gitea SSH server over tailscale";
|
||||
after = [ "network-online.target" "tailscaled.service" ];
|
||||
wants = [ "network-online.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
DynamicUser = true;
|
||||
ExecStart = "${pkgs.socat}/bin/socat TCP-LISTEN:2222,fork,reuseaddr TCP:jupiter.hosts.mgaction.town:2222";
|
||||
Restart = "always";
|
||||
};
|
||||
};
|
||||
networking.firewall.allowedTCPPorts = [ 2222 ];
|
||||
|
||||
# ---- Authentik (identity/OIDC provider) ----
|
||||
# Runs locally on neptun (see services/identity/authentik.nix); Caddy just
|
||||
@@ -113,23 +127,5 @@
|
||||
}
|
||||
'';
|
||||
|
||||
# ---- Gitea SSH forward ----
|
||||
# Caddy only proxies HTTP; forward :2222 over the tailnet to gitea's own
|
||||
# SSH server on jupiter (services/dev/gitea.nix), so
|
||||
# `ssh://git@git.mgaction.town:2222/...` works. Also needs a matching
|
||||
# inbound-2222 rule in netcup's edge firewall panel (not managed by Nix).
|
||||
systemd.services.gitea-ssh-forward = {
|
||||
description = "Forward :2222 to jupiter's gitea SSH server over tailscale";
|
||||
after = [ "network-online.target" "tailscaled.service" ];
|
||||
wants = [ "network-online.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
DynamicUser = true;
|
||||
ExecStart = "${pkgs.socat}/bin/socat TCP-LISTEN:2222,fork,reuseaddr TCP:jupiter.hosts.mgaction.town:2222";
|
||||
Restart = "always";
|
||||
};
|
||||
};
|
||||
networking.firewall.allowedTCPPorts = [ 2222 ];
|
||||
|
||||
system.stateVersion = "26.05"; # set at install time; do NOT bump on upgrades
|
||||
}
|
||||
|
||||
@@ -43,9 +43,14 @@
|
||||
# placeholders (see services/vpn/headplane.nix) until Authentik/headscale are
|
||||
# actually deployed and those get created for real.
|
||||
#
|
||||
# owner: unlike authentik's EnvironmentFile above, headplane opens these
|
||||
# paths itself, already running as headscale's user — so root:root 0400
|
||||
# would fail and each needs an explicit owner.
|
||||
# owner: unlike authentik's EnvironmentFile above, headscale and headplane
|
||||
# open these paths themselves, already running as the headscale user — so
|
||||
# the root:root 0400 default would fail and each needs an explicit owner.
|
||||
#
|
||||
# headscale's OIDC client is a SEPARATE Authentik application from
|
||||
# headplane's (services/vpn/headscale.nix), hence the second client secret.
|
||||
sops.secrets.headscale_oidc_client_secret.owner = "headscale";
|
||||
|
||||
sops.secrets.headplane_cookie_secret.owner = "headscale";
|
||||
sops.secrets.headplane_oidc_client_secret.owner = "headscale";
|
||||
sops.secrets.headplane_headscale_api_key.owner = "headscale";
|
||||
|
||||
Reference in New Issue
Block a user