diff --git a/configuration.nix b/configuration.nix index 4fcd4a4..ddb94f7 100644 --- a/configuration.nix +++ b/configuration.nix @@ -85,6 +85,7 @@ kitty.terminfo htop tmux + headscale ]; nix.settings.experimental-features = [ "nix-command" "flakes" ]; @@ -171,6 +172,22 @@ # === CrowdSec === myModules.crowdsec.enable = true; + # === Headscale (Self-hosted Tailscale) === + myModules.headscale = { + enable = true; + domain = "vpn.ashisgreat.xyz"; + # OIDC not enabled by default — use pre-shared auth keys: + # headscale apikeys create + # tailscale up --login-server=https://vpn.ashisgreat.xyz --authkey= + # + # To enable OIDC login (Google, GitHub, etc.): + # oidc.enable = true; + # oidc.issuer = "https://accounts.google.com"; + # oidc.clientId = "your-client-id"; + # oidc.clientSecret = config.sops.placeholder.headscale_oidc_secret; + # And add headscale_oidc_secret to your secrets.yaml + }; + # === Backups (Restic + B2) === myModules.backup = { enable = true; diff --git a/modules/default.nix b/modules/default.nix index d889fe1..32fc5dd 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -11,5 +11,6 @@ ./backup.nix ./adguard.nix ./forgejo.nix + ./headscale.nix ]; } diff --git a/modules/headscale.nix b/modules/headscale.nix new file mode 100644 index 0000000..704fc5c --- /dev/null +++ b/modules/headscale.nix @@ -0,0 +1,152 @@ +# 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 = [ + "https://dns.mullvad.net/dns-query" + "https://dns.quad9.net/dns-query" + ]; + }; + 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 = { }; + }; +}