{ config, lib, pkgs, ... }: with lib; let cfg = config.modules.headscale; in { options = { modules = { headscale = { enable = mkEnableOption "Deploy headscale"; oidcClientSecretPath = mkOption { type = types.str; default = "/etc/headscale/oidc_client_secret.key"; description = "Path to OIDC client secret file"; example = "config.age.secrets.headscale-oidc-key.path"; }; litestream = { enable = mkEnableOption "Enable litestream for headscale database backups"; replicas = mkOption { type = types.listOf (types.attrsOf types.anything); default = []; description = "List of litestream replica configurations"; example = [ { url = "s3://your-backup-bucket/headscale/db"; access-key-id = "$LITESTREAM_ACCESS_KEY_ID"; secret-access-key = "$LITESTREAM_SECRET_ACCESS_KEY"; region = "us-east-1"; } ]; }; backupPath = mkOption { type = types.nullOr types.str; default = null; description = "Local backup path (alternative to S3)"; example = "/backup/headscale"; }; syncInterval = mkOption { type = types.str; default = "1s"; description = "How often to sync to replicas"; }; retention = mkOption { type = types.str; default = "72h"; description = "How long to retain snapshots"; }; environmentFile = mkOption { type = types.nullOr types.path; default = null; description = "Environment file containing S3 credentials (can be agenix secret)"; example = "config.age.secrets.litestream-env.path"; }; }; }; }; }; config = mkIf cfg.enable { services.headscale = { enable = true; address = "0.0.0.0"; port = 8080; settings = { server_url = "https://headscale.nekomimi.pet"; # Metrics and gRPC metrics_listen_addr = "127.0.0.1:9090"; grpc_listen_addr = "127.0.0.1:50443"; grpc_allow_insecure = false; # Prefixes prefixes = { v4 = "100.64.0.0/10"; v6 = "fd7a:115c:a1e0::/48"; allocation = "sequential"; }; # Database database = { type = "sqlite"; sqlite = { path = "/var/lib/headscale/db.sqlite"; write_ahead_log = true; }; }; # Noise noise = { private_key_path = "/var/lib/headscale/noise_private.key"; }; # DERP derp = { urls = [ "https://controlplane.tailscale.com/derpmap/default" ]; paths = []; auto_update_enabled = true; update_frequency = "24h"; server = { enabled = false; region_id = 999; region_code = "headscale"; region_name = "Headscale Embedded DERP"; stun_listen_addr = "0.0.0.0:3478"; private_key_path = "/var/lib/headscale/derp_server_private.key"; automatically_add_embedded_derp_region = true; ipv4 = "1.2.3.4"; ipv6 = "2001:db8::1"; }; }; # DNS dns = { magic_dns = true; base_domain = "dns.sharkgirl.pet"; nameservers = { global = [ "100.64.0.7" "1.1.1.1" "1.0.0.1" "2606:4700:4700::1111" "2606:4700:4700::1001" ]; }; search_domains = []; }; # OIDC with configurable secret path oidc = { only_start_if_oidc_is_available = true; issuer = "https://pocketid.nekomimi.pet"; client_id = "f345acad-3eac-45b7-9d91-57f388987a57"; client_secret_path = cfg.oidcClientSecretPath; pkce = { enabled = true; method = "S256"; }; }; # Policy policy = { mode = "database"; }; # TLS/ACME acme_url = "https://acme-v02.api.letsencrypt.org/directory"; acme_email = ""; tls_letsencrypt_hostname = ""; tls_letsencrypt_cache_dir = "/var/lib/headscale/cache"; tls_letsencrypt_challenge_type = "HTTP-01"; tls_letsencrypt_listen = ":http"; tls_cert_path = ""; tls_key_path = ""; # Logging log = { format = "text"; level = "info"; }; # Misc settings disable_check_updates = false; ephemeral_node_inactivity_timeout = "30m"; unix_socket = "/var/run/headscale/headscale.sock"; unix_socket_permission = "0770"; logtail = { enabled = false; }; randomize_client_port = false; }; }; # Configurable Litestream for SQLite database backups services.litestream = mkIf cfg.litestream.enable { enable = true; settings = { dbs = [ { path = "/var/lib/headscale/db.sqlite"; sync-interval = cfg.litestream.syncInterval; retention = cfg.litestream.retention; replicas = # Use custom replicas if provided if cfg.litestream.replicas != [] then cfg.litestream.replicas # Otherwise use local backup if path is provided else if cfg.litestream.backupPath != null then [{ path = cfg.litestream.backupPath; }] # Default empty (user must configure) else []; } ]; }; }; # Configure systemd service to use agenix secrets systemd.services.headscale.serviceConfig = mkMerge [ { SupplementaryGroups = [ "headscale-secrets" ]; } # Add environment file for litestream if specified (mkIf (cfg.litestream.enable && cfg.litestream.environmentFile != null) { EnvironmentFile = cfg.litestream.environmentFile; }) ]; # Configure litestream service with environment file if specified systemd.services.litestream = mkIf (cfg.litestream.enable && cfg.litestream.environmentFile != null) { serviceConfig = { EnvironmentFile = cfg.litestream.environmentFile; }; }; # Create a group for accessing secrets users.groups.headscale-secrets = {}; }; }