{ config, lib, pkgs, ... }: let inherit (lib) mkIf; metricsEnable = false; mkAcl = src: dst: { action = "accept"; inherit src dst; }; mkSshAcl = src: dst: users: { action = "accept"; inherit src dst users; }; in mkIf config.services.headscale.enable { environment.systemPackages = [ config.services.headscale.package ]; services = { headscale = { address = "127.0.0.1"; port = 8521; settings = { server_url = "https://hs.ccnlc.eu"; tls_cert_path = null; tls_key_path = null; ip_prefixes = [ "100.64.0.0/10" "fd7a:115c:a1e0::/48" ]; ephemeral_node_inactivity_timeout = "30m"; node_update_check_interval = "10s"; metrics_listen_addr = mkIf metricsEnable "127.0.0.1:8086"; # logging log = { format = "text"; level = "info"; }; logtail.enabled = false; dns_config = { override_local_dns = true; magic_dns = true; nameservers = [ "100.64.0.4" ]; extra_records = let mkRecords = map (sub: { name = "${sub}.ccnlc.eu"; type = "A"; value = "100.64.0.4"; }); in [ { name = "ccnlc.eu"; type = "A"; value = "100.64.0.4"; } ] # Tailscale doesn't seem to support wildcard A/AAAA records # - https://github.com/juanfont/headscale/issues/2159#issuecomment-2393406444 ++ mkRecords [ "immich" "adguard" "nextcloud" "kitchenowl" "navidrome" "subsonic" "nextcloud" "paperless" "truenas" "fritz" ]; }; }; }; nginx.virtualHosts."hs.ccnlc.eu" = { forceSSL = true; enableACME = true; #quic = true; http3 = true; locations = { "/" = { proxyPass = "http://localhost:${toString config.services.headscale.port}"; proxyWebsockets = true; }; "/metrics" = mkIf metricsEnable { proxyPass = "http://${toString config.services.headscale.settings.metrics_listen_addr}/metrics"; }; }; extraConfig = '' add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; ''; }; }; systemd.services = { tailscaled.after = [ "headscale.service" ]; headscale = { environment = { # required to use ssh HEADSCALE_EXPERIMENTAL_FEATURE_SSH = "1"; }; }; }; services.headscale.settings.acl_policy_path = pkgs.writeTextFile { name = "headscale-acl.hujson"; text = builtins.toJSON { acls = [ (mkAcl [ "tag:client" ] [ "tag:client:*" ]) # client -> client (mkAcl [ "tag:client" ] [ "tag:server:*" ]) # client -> server { action = "accept"; src = [ "tag:client" "tag:server" ]; proto = "rsync"; # optional dst = [ "tag:backup" ]; } ]; ssh = [ (mkSshAcl [ "tag:client" ] [ "tag:server" "tag:client" ] [ "ny" ]) # client -> {client, server} ]; tags = [ "tag:client" "tag:server" "tag:backup" ]; tagOwners = let users = [ "ny" ]; tags = map (name: "tag:${name}") [ "server" "client" ]; in lib.genAttrs tags (_: users); autoApprovers = { exitNode = [ "*" ]; }; }; }; }