{ config, pkgs, lib, ... }: let domain = "kennys.mom"; derpPort = 3478; # Generate the exact YAML config headplane wants format = pkgs.formats.yaml {}; configStrict = lib.recursiveUpdate (builtins.removeAttrs config.services.headscale.settings [ "oidc" ]) { tls_cert_path = "/dev/null"; tls_key_path = "/dev/null"; policy.path = "/dev/null"; }; headscaleConfigFile = format.generate "headscale-strict.yml" configStrict; in { services.headscale = { enable = true; address = "0.0.0.0"; port = 8085; settings = { dns = { override_local_dns = true; base_domain = "hs.${domain}"; magic_dns = true; domains = [ "hs.${domain}" ]; nameservers = { global = [ "1.1.1.1" "9.9.9.9" ]; }; }; server_url = "https://headscale.${domain}"; metrics_listen_addr = "127.0.0.1:8095"; logtail = { enabled = false; }; log = { level = "warn"; }; derp.server = { enable = true; region_id = 999; stun_listen_addr = "0.0.0.0:${toString derpPort}"; }; ip_prefixes = [ "100.64.0.0/10" "fd7a:115c:a1e0::/48" ]; grpc_listen_addr = "127.0.0.1:50443"; # Required for Headplane communication api_key_path = "/etc/headscale/apikey"; policy.mode = "database"; }; }; # Put strict config as file for headplane environment.etc."headscale-strict.yml".source = headscaleConfigFile; services.nginx = { enable = true; virtualHosts."headscale.${domain}" = { forceSSL = true; enableACME = true; locations = { "/" = { proxyPass = "http://127.0.0.1:${toString config.services.headscale.port}"; proxyWebsockets = true; extraConfig = '' proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; ''; }; "/metrics" = { proxyPass = "http://${config.services.headscale.settings.metrics_listen_addr}/metrics"; }; }; }; # Separate virtual host for Headplane UI virtualHosts."headplane.${domain}" = { forceSSL = true; enableACME = true; locations."/" = { return = "301 http://$host/admin"; }; locations."/admin" = { proxyPass = "http://127.0.0.1:3000"; proxyWebsockets = true; extraConfig = '' proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; ''; }; # Also proxy assets and other paths to headplane locations."/assets" = { proxyPass = "http://127.0.0.1:3000"; }; locations."/api" = { proxyPass = "http://127.0.0.1:3000"; }; }; }; # configure ssl certificate options security.acme = { defaults.email = "dj@monumetric.com"; acceptTerms = true; }; # punch through firewall networking.firewall.allowedUDPPorts = [ derpPort ]; networking.firewall.allowedTCPPorts = [ 80 443 ]; # add headscale package to system environment.systemPackages = [ config.services.headscale.package ]; # Ensure headscale API key exists for Headplane systemd.services.headscale-api-key = { description = "Generate Headscale API key for Headplane"; wantedBy = [ "multi-user.target" ]; before = [ "headplane.service" ]; after = [ "headscale.service" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; script = '' if [ ! -f /etc/headscale/apikey ]; then echo "Generating Headscale API key..." mkdir -p /etc/headscale ${config.services.headscale.package}/bin/headscale apikeys create --expiration 999d > /etc/headscale/apikey chmod 600 /etc/headscale/apikey echo "API key generated at /etc/headscale/apikey" else echo "API key already exists" fi ''; }; }