- Configure Headscale to use the VPS Tailscale IP (100.64.0.3) as the global DNS server instead of external providers. - Add firewall rules to allow DNS requests over the `tailscale0` interface. - Add iptables PREROUTING rules to redirect standard DNS (port 53) from Tailscale clients to AdGuard Home (port 5353) to resolve port conflicts with `aardvark-dns`.
151 lines
4 KiB
Nix
151 lines
4 KiB
Nix
# 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=<key>
|
|
|
|
{
|
|
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 = { };
|
|
};
|
|
}
|