# Headscale Module # Provides: Self-hosted Tailscale control server (mesh VPN) # # Usage: # myModules.headscale = { # enable = true; # domain = "vpn.example.com"; # }; # # After deploying, generate an auth key to register devices: # headscale apikeys create # # Then on each device: # tailscale up --login-server=https://vpn.example.com --authkey= { config, lib, pkgs, ... }: let cfg = config.myModules.headscale; in { options.myModules.headscale = { enable = lib.mkEnableOption "Headscale (self-hosted Tailscale control server)"; domain = lib.mkOption { type = lib.types.str; example = "vpn.example.com"; description = "Public domain name for the Headscale control server"; }; port = lib.mkOption { type = lib.types.port; default = 8085; description = "Internal port for Headscale to listen on"; }; oidc = { enable = lib.mkEnableOption "OIDC authentication (e.g. Google, GitHub)"; issuer = lib.mkOption { type = lib.types.str; example = "https://accounts.google.com"; description = "OIDC issuer URL"; }; clientId = lib.mkOption { type = lib.types.str; description = "OIDC client ID (store in SOPS)"; }; clientSecret = lib.mkOption { type = lib.types.str; description = "OIDC client secret SOPS placeholder"; }; }; }; config = lib.mkIf cfg.enable { services.headscale = { enable = true; settings = { server_url = "https://${cfg.domain}"; listen_addr = "127.0.0.1:${toString cfg.port}"; grpc_listen_addr = "127.0.0.1:50443"; metrics_listen_addr = "127.0.0.1:9090"; # Use SQLite for simplicity (Postgres available for larger deployments) database.sqlite.path = "/var/lib/headscale/db.sqlite"; # Private key for WireGuard — auto-generated on first run private_key_path = "/var/lib/headscale/private.key"; # DERP relay servers (use Tailscale's public ones) derp = { urls = [ "https://controlplane.tailscale.com/derpmap/default" ]; auto_update = true; }; # Disable built-in DERP server (use public relays only) derp.server.enabled = false; # Authentication policy.mode = lib.mkIf cfg.oidc.enable "oidc"; oidc = lib.mkIf cfg.oidc.enable { issuer = cfg.oidc.issuer; client_id = cfg.oidc.clientId; client_secret_path = config.sops.secrets.headscale_oidc_secret.path; allowed_domains = [ ]; allowed_users = [ ]; scopes = [ "openid" "profile" "email" ]; }; # DNS configuration dns = { magic_dns = true; base_domain = "headscale.net"; domains = [ ]; nameservers = { global = [ "100.64.0.3" ]; }; override_local_dns = true; }; # Logging log.level = "info"; # Unix socket for headscale CLI unix_socket = "/run/headscale/headscale.sock"; unix_socket_permission = "0770"; }; }; # OIDC secret from SOPS sops.secrets.headscale_oidc_secret = lib.mkIf cfg.oidc.enable { }; # Nginx reverse proxy myModules.nginx.domains."${cfg.domain}" = { port = cfg.port; extraConfig = '' # Headscale needs WebSocket upgrade for client connections proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; # Larger buffers for Headscale proxy_buffer_size 128k; proxy_buffers 4 256k; ''; # Headscale doesn't serve HTML — skip CSP contentSecurityPolicy = null; }; # Add Headscale data to backups myModules.backup.paths = [ config.services.headscale.settings.database.sqlite.path ]; # Headscale needs its own group for socket permissions users.groups.headscale = { }; }; }