feat(headscale): add self-hosted Tailscale control server
New module: modules/headscale.nix - Headscale service listening on localhost with Nginx reverse proxy - SQLite database (appropriate for personal use) - Tailscale public DERP relays for NAT traversal fallback - MagicDNS enabled with Mullvad/Quad9 upstream resolvers - Optional OIDC authentication (Google, GitHub, etc.) - Default auth: pre-shared API keys (headscale apikeys create) - Added to backup paths (SQLite DB) - headscale CLI tool added to system packages Configuration: - Domain: vpn.ashisgreat.xyz - OIDC disabled by default (documented how to enable in configuration.nix) To register a device after deploying: sudo headscale apikeys create tailscale up --login-server=https://vpn.ashisgreat.xyz --authkey=<key> DNS record needed: vpn.ashisgreat.xyz → VPS IP
This commit is contained in:
parent
2bc375ab86
commit
1c28db5f8e
3 changed files with 170 additions and 0 deletions
|
|
@ -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=<key>
|
||||
#
|
||||
# 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;
|
||||
|
|
|
|||
|
|
@ -11,5 +11,6 @@
|
|||
./backup.nix
|
||||
./adguard.nix
|
||||
./forgejo.nix
|
||||
./headscale.nix
|
||||
];
|
||||
}
|
||||
|
|
|
|||
152
modules/headscale.nix
Normal file
152
modules/headscale.nix
Normal file
|
|
@ -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=<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 = [
|
||||
"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 = { };
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue