nixos-vps/modules/headscale.nix
ashisgreat22 1c76661538 feat(network): route tailscale dns through adguard
- 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`.
2026-03-19 22:14:18 +01:00

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 = { };
};
}