From 4a5c88eac3aaf1fb3d5ab15ee84787b9883c2465 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 6 Sep 2024 12:47:36 +0200 Subject: [PATCH 1/8] nixos/headscale: modernize Signed-off-by: Kristoffer Dalby --- .../modules/services/networking/headscale.nix | 472 ++++++++++-------- 1 file changed, 270 insertions(+), 202 deletions(-) diff --git a/nixos/modules/services/networking/headscale.nix b/nixos/modules/services/networking/headscale.nix index ea66faeabbf2..645d4fe9c069 100644 --- a/nixos/modules/services/networking/headscale.nix +++ b/nixos/modules/services/networking/headscale.nix @@ -1,8 +1,7 @@ -{ - config, - lib, - pkgs, - ... +{ config +, lib +, pkgs +, ... }: with lib; let cfg = config.services.headscale; @@ -10,9 +9,19 @@ with lib; let dataDir = "/var/lib/headscale"; runDir = "/run/headscale"; - settingsFormat = pkgs.formats.yaml {}; + cliConfig = { + # Turn off update checks since the origin of our package + # is nixpkgs and not Github. + disable_check_updates = true; + + unix_socket = "${runDir}/headscale.sock"; + }; + + settingsFormat = pkgs.formats.yaml { }; configFile = settingsFormat.generate "headscale.yaml" cfg.settings; -in { + cliConfigFile = settingsFormat.generate "headscale.yaml" cliConfig; +in +{ options = { services.headscale = { enable = mkEnableOption "headscale, Open Source coordination server for Tailscale"; @@ -84,14 +93,6 @@ in { example = "https://myheadscale.example.com:443"; }; - private_key_path = mkOption { - type = types.path; - default = "${dataDir}/private.key"; - description = '' - Path to private key file, generated automatically if it does not exist. - ''; - }; - noise.private_key_path = mkOption { type = types.path; default = "${dataDir}/noise_private.key"; @@ -100,10 +101,44 @@ in { ''; }; + prefixes = + let + prefDesc = '' + Each prefix consists of either an IPv4 or IPv6 address, + and the associated prefix length, delimited by a slash. + It must be within IP ranges supported by the Tailscale + client - i.e., subnets of 100.64.0.0/10 and fd7a:115c:a1e0::/48. + ''; + in + { + v4 = mkOption { + type = types.str; + default = "100.64.0.0/10"; + description = prefDesc; + }; + + v6 = mkOption { + type = types.str; + default = "fd7a:115c:a1e0::/48"; + description = prefDesc; + }; + + allocation = mkOption { + type = types.enum [ "sequential" "random" ]; + example = "random"; + default = "sequential"; + description = '' + Strategy used for allocation of IPs to nodes, available options: + - sequential (default): assigns the next free IP from the previous given IP. + - random: assigns the next free IP from a pseudo-random IP generator (crypto/rand). + ''; + }; + }; + derp = { urls = mkOption { type = types.listOf types.str; - default = ["https://controlplane.tailscale.com/derpmap/default"]; + default = [ "https://controlplane.tailscale.com/derpmap/default" ]; description = '' List of urls containing DERP maps. See [How Tailscale works](https://tailscale.com/blog/how-tailscale-works/) for more information on DERP maps. @@ -112,7 +147,7 @@ in { paths = mkOption { type = types.listOf types.path; - default = []; + default = [ ]; description = '' List of file paths containing DERP maps. See [How Tailscale works](https://tailscale.com/blog/how-tailscale-works/) for more information on DERP maps. @@ -136,6 +171,14 @@ in { ''; example = "5m"; }; + + server.private_key_path = lib.mkOption { + type = lib.types.path; + default = "${dataDir}/derp_server_private.key"; + description = '' + Path to derp private key file, generated automatically if it does not exist. + ''; + }; }; ephemeral_node_inactivity_timeout = mkOption { @@ -147,102 +190,98 @@ in { example = "5m"; }; - db_type = mkOption { - type = types.enum ["sqlite3" "postgres"]; - example = "postgres"; - default = "sqlite3"; - description = "Database engine to use."; - }; - - db_host = mkOption { - type = types.nullOr types.str; - default = null; - example = "127.0.0.1"; - description = "Database host address."; - }; - - db_port = mkOption { - type = types.nullOr types.port; - default = null; - example = 3306; - description = "Database host port."; - }; + database = { + type = mkOption { + type = types.enum [ "sqlite" "sqlite3" "postgres" ]; + example = "postgres"; + default = "sqlite"; + description = '' + Database engine to use. + Please note that using Postgres is highly discouraged as it is only supported for legacy reasons. + All new development, testing and optimisations are done with SQLite in mind. + ''; + }; - db_name = mkOption { - type = types.nullOr types.str; - default = null; - example = "headscale"; - description = "Database name."; - }; + sqlite = { + path = mkOption { + type = types.nullOr types.str; + default = "${dataDir}/db.sqlite"; + description = "Path to the sqlite3 database file."; + }; - db_user = mkOption { - type = types.nullOr types.str; - default = null; - example = "headscale"; - description = "Database user."; - }; + write_ahead_log = mkOption { + type = types.bool; + default = true; + description = '' + Enable WAL mode for SQLite. This is recommended for production environments. + https://www.sqlite.org/wal.html + ''; + example = true; + }; + }; - db_password_file = mkOption { - type = types.nullOr types.path; - default = null; - example = "/run/keys/headscale-dbpassword"; - description = '' - A file containing the password corresponding to - {option}`database.user`. - ''; - }; + postgres = { + host = mkOption { + type = types.nullOr types.str; + default = null; + example = "127.0.0.1"; + description = "Database host address."; + }; - db_path = mkOption { - type = types.nullOr types.str; - default = "${dataDir}/db.sqlite"; - description = "Path to the sqlite3 database file."; - }; + port = mkOption { + type = types.nullOr types.port; + default = null; + example = 3306; + description = "Database host port."; + }; - log.level = mkOption { - type = types.str; - default = "info"; - description = '' - headscale log level. - ''; - example = "debug"; - }; + name = mkOption { + type = types.nullOr types.str; + default = null; + example = "headscale"; + description = "Database name."; + }; - log.format = mkOption { - type = types.str; - default = "text"; - description = '' - headscale log format. - ''; - example = "json"; - }; + user = mkOption { + type = types.nullOr types.str; + default = null; + example = "headscale"; + description = "Database user."; + }; - dns_config = { - nameservers = mkOption { - type = types.listOf types.str; - default = ["1.1.1.1"]; - description = '' - List of nameservers to pass to Tailscale clients. - ''; + password_file = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/keys/headscale-dbpassword"; + description = '' + A file containing the password corresponding to + {option}`database.user`. + ''; + }; }; + }; - override_local_dns = mkOption { - type = types.bool; - default = false; + log = { + level = mkOption { + type = types.str; + default = "info"; description = '' - Whether to use [Override local DNS](https://tailscale.com/kb/1054/dns/). + headscale log level. ''; - example = true; + example = "debug"; }; - domains = mkOption { - type = types.listOf types.str; - default = []; + format = mkOption { + type = types.str; + default = "text"; description = '' - Search domains to inject to Tailscale clients. + headscale log format. ''; - example = ["mydomain.internal"]; + example = "json"; }; + }; + dns = { magic_dns = mkOption { type = types.bool; default = true; @@ -264,6 +303,25 @@ in { `myhost.mynamespace.example.com`). ''; }; + + nameservers = { + global = mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + List of nameservers to pass to Tailscale clients. + ''; + }; + }; + + search_domains = mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + Search domains to inject to Tailscale clients. + ''; + example = [ "mydomain.internal" ]; + }; }; oidc = { @@ -294,7 +352,7 @@ in { scope = mkOption { type = types.listOf types.str; - default = ["openid" "profile" "email"]; + default = [ "openid" "profile" "email" ]; description = '' Scopes used in the OIDC flow. ''; @@ -348,7 +406,7 @@ in { }; tls_letsencrypt_challenge_type = mkOption { - type = types.enum ["TLS-ALPN-01" "HTTP-01"]; + type = types.enum [ "TLS-ALPN-01" "HTTP-01" ]; default = "HTTP-01"; description = '' Type of ACME challenge to use, currently supported types: @@ -382,12 +440,24 @@ in { ''; }; - acl_policy_path = mkOption { - type = types.nullOr types.path; - default = null; - description = '' - Path to a file containing ACL policies. - ''; + policy = { + mode = mkOption { + type = types.enum [ "file" "database" ]; + default = "file"; + description = '' + The mode can be "file" or "database" that defines + where the ACL policies are stored and read from. + ''; + }; + + path = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + If the mode is set to "file", the path to a + HuJSON file containing ACL policies. + ''; + }; }; }; }; @@ -396,64 +466,63 @@ in { }; imports = [ - # TODO address + port = listen_addr - (mkRenamedOptionModule ["services" "headscale" "serverUrl"] ["services" "headscale" "settings" "server_url"]) - (mkRenamedOptionModule ["services" "headscale" "privateKeyFile"] ["services" "headscale" "settings" "private_key_path"]) - (mkRenamedOptionModule ["services" "headscale" "derp" "urls"] ["services" "headscale" "settings" "derp" "urls"]) - (mkRenamedOptionModule ["services" "headscale" "derp" "paths"] ["services" "headscale" "settings" "derp" "paths"]) - (mkRenamedOptionModule ["services" "headscale" "derp" "autoUpdate"] ["services" "headscale" "settings" "derp" "auto_update_enable"]) - (mkRenamedOptionModule ["services" "headscale" "derp" "updateFrequency"] ["services" "headscale" "settings" "derp" "update_frequency"]) - (mkRenamedOptionModule ["services" "headscale" "ephemeralNodeInactivityTimeout"] ["services" "headscale" "settings" "ephemeral_node_inactivity_timeout"]) - (mkRenamedOptionModule ["services" "headscale" "database" "type"] ["services" "headscale" "settings" "db_type"]) - (mkRenamedOptionModule ["services" "headscale" "database" "path"] ["services" "headscale" "settings" "db_path"]) - (mkRenamedOptionModule ["services" "headscale" "database" "host"] ["services" "headscale" "settings" "db_host"]) - (mkRenamedOptionModule ["services" "headscale" "database" "port"] ["services" "headscale" "settings" "db_port"]) - (mkRenamedOptionModule ["services" "headscale" "database" "name"] ["services" "headscale" "settings" "db_name"]) - (mkRenamedOptionModule ["services" "headscale" "database" "user"] ["services" "headscale" "settings" "db_user"]) - (mkRenamedOptionModule ["services" "headscale" "database" "passwordFile"] ["services" "headscale" "settings" "db_password_file"]) - (mkRenamedOptionModule ["services" "headscale" "logLevel"] ["services" "headscale" "settings" "log" "level"]) - (mkRenamedOptionModule ["services" "headscale" "dns" "nameservers"] ["services" "headscale" "settings" "dns_config" "nameservers"]) - (mkRenamedOptionModule ["services" "headscale" "dns" "domains"] ["services" "headscale" "settings" "dns_config" "domains"]) - (mkRenamedOptionModule ["services" "headscale" "dns" "magicDns"] ["services" "headscale" "settings" "dns_config" "magic_dns"]) - (mkRenamedOptionModule ["services" "headscale" "dns" "baseDomain"] ["services" "headscale" "settings" "dns_config" "base_domain"]) - (mkRenamedOptionModule ["services" "headscale" "openIdConnect" "issuer"] ["services" "headscale" "settings" "oidc" "issuer"]) - (mkRenamedOptionModule ["services" "headscale" "openIdConnect" "clientId"] ["services" "headscale" "settings" "oidc" "client_id"]) - (mkRenamedOptionModule ["services" "headscale" "openIdConnect" "clientSecretFile"] ["services" "headscale" "settings" "oidc" "client_secret_path"]) - (mkRenamedOptionModule ["services" "headscale" "tls" "letsencrypt" "hostname"] ["services" "headscale" "settings" "tls_letsencrypt_hostname"]) - (mkRenamedOptionModule ["services" "headscale" "tls" "letsencrypt" "challengeType"] ["services" "headscale" "settings" "tls_letsencrypt_challenge_type"]) - (mkRenamedOptionModule ["services" "headscale" "tls" "letsencrypt" "httpListen"] ["services" "headscale" "settings" "tls_letsencrypt_listen"]) - (mkRenamedOptionModule ["services" "headscale" "tls" "certFile"] ["services" "headscale" "settings" "tls_cert_path"]) - (mkRenamedOptionModule ["services" "headscale" "tls" "keyFile"] ["services" "headscale" "settings" "tls_key_path"]) - (mkRenamedOptionModule ["services" "headscale" "aclPolicyFile"] ["services" "headscale" "settings" "acl_policy_path"]) - - (mkRemovedOptionModule ["services" "headscale" "openIdConnect" "domainMap"] '' + (mkRenamedOptionModule [ "services" "headscale" "serverUrl" ] [ "services" "headscale" "settings" "server_url" ]) + (mkRenamedOptionModule [ "services" "headscale" "derp" "urls" ] [ "services" "headscale" "settings" "derp" "urls" ]) + (mkRenamedOptionModule [ "services" "headscale" "derp" "paths" ] [ "services" "headscale" "settings" "derp" "paths" ]) + (mkRenamedOptionModule [ "services" "headscale" "derp" "autoUpdate" ] [ "services" "headscale" "settings" "derp" "auto_update_enable" ]) + (mkRenamedOptionModule [ "services" "headscale" "derp" "updateFrequency" ] [ "services" "headscale" "settings" "derp" "update_frequency" ]) + (mkRenamedOptionModule [ "services" "headscale" "ephemeralNodeInactivityTimeout" ] [ "services" "headscale" "settings" "ephemeral_node_inactivity_timeout" ]) + + # (mkRenamedOptionModule ["services" "headscale" "settings" "db_type"] ["services" "headscale" "settings" "database" "type"]) + # (mkRenamedOptionModule ["services" "headscale" "settings" "db_path"] ["services" "headscale" "settings" "database" "sqlite" "path"]) + # (mkRenamedOptionModule ["services" "headscale" "settings" "db_host"] ["services" "headscale" "settings" "database" "postgres" "host"]) + # (mkRenamedOptionModule ["services" "headscale" "settings" "db_port"] ["services" "headscale" "settings" "database" "postgres" "port"]) + # (mkRenamedOptionModule ["services" "headscale" "settings" "db_name"] ["services" "headscale" "settings" "database" "postgres" "name"]) + # (mkRenamedOptionModule ["services" "headscale" "settings" "db_user"] ["services" "headscale" "settings" "database" "postgres" "user"]) + # (mkRenamedOptionModule ["services" "headscale" "settings" "db_password_file"] ["services" "headscale" "settings" "database" "postgres" "password_file"]) + + (mkRenamedOptionModule [ "services" "headscale" "logLevel" ] [ "services" "headscale" "settings" "log" "level" ]) + + # (mkRenamedOptionModule ["services" "headscale" "settings" "dns_config" "nameservers"] ["services" "headscale" "settings" "dns" "nameservers" "global"]) + # (mkRenamedOptionModule ["services" "headscale" "settings" "dns_config" "domains"] ["services" "headscale" "settings" "dns" "search_domains"]) + # (mkRenamedOptionModule ["services" "headscale" "settings" "dns_config" "magic_dns"] ["services" "headscale" "settings" "dns" "magic_dns"]) + # (mkRenamedOptionModule ["services" "headscale" "settings" "dns_config" "base_domain"] ["services" "headscale" "settings" "dns" "base_domain"]) + + (mkRenamedOptionModule [ "services" "headscale" "openIdConnect" "issuer" ] [ "services" "headscale" "settings" "oidc" "issuer" ]) + (mkRenamedOptionModule [ "services" "headscale" "openIdConnect" "clientId" ] [ "services" "headscale" "settings" "oidc" "client_id" ]) + (mkRenamedOptionModule [ "services" "headscale" "openIdConnect" "clientSecretFile" ] [ "services" "headscale" "settings" "oidc" "client_secret_path" ]) + (mkRenamedOptionModule [ "services" "headscale" "tls" "letsencrypt" "hostname" ] [ "services" "headscale" "settings" "tls_letsencrypt_hostname" ]) + (mkRenamedOptionModule [ "services" "headscale" "tls" "letsencrypt" "challengeType" ] [ "services" "headscale" "settings" "tls_letsencrypt_challenge_type" ]) + (mkRenamedOptionModule [ "services" "headscale" "tls" "letsencrypt" "httpListen" ] [ "services" "headscale" "settings" "tls_letsencrypt_listen" ]) + (mkRenamedOptionModule [ "services" "headscale" "tls" "certFile" ] [ "services" "headscale" "settings" "tls_cert_path" ]) + (mkRenamedOptionModule [ "services" "headscale" "tls" "keyFile" ] [ "services" "headscale" "settings" "tls_key_path" ]) + + # (mkRenamedOptionModule ["services" "headscale" "settings" "acl_policy_path"] ["services" "headscale" "settings" "policy" "path"]) + + (mkRemovedOptionModule [ "services" "headscale" "openIdConnect" "domainMap" ] '' Headscale no longer uses domain_map. If you're using an old version of headscale you can still set this option via services.headscale.settings.oidc.domain_map. '') ]; config = mkIf cfg.enable { - services.headscale.settings = { - listen_addr = mkDefault "${cfg.address}:${toString cfg.port}"; + services.headscale.settings = mkMerge [ + cliConfig + { + listen_addr = mkDefault "${cfg.address}:${toString cfg.port}"; - # Turn off update checks since the origin of our package - # is nixpkgs and not Github. - disable_check_updates = true; - - unix_socket = "${runDir}/headscale.sock"; - - tls_letsencrypt_cache_dir = "${dataDir}/.cache"; - }; + tls_letsencrypt_cache_dir = "${dataDir}/.cache"; + } + ]; environment = { - # Setup the headscale configuration in a known path in /etc to - # allow both the Server and the Client use it to find the socket - # for communication. - etc."headscale/config.yaml".source = configFile; + # Headscale CLI needs a minimal config to be able to locate the unix socket + # to talk to the server instance. + etc."headscale/config.yaml".source = cliConfigFile; systemPackages = [ cfg.package ]; }; - users.groups.headscale = mkIf (cfg.group == "headscale") {}; + users.groups.headscale = mkIf (cfg.group == "headscale") { }; users.users.headscale = mkIf (cfg.user == "headscale") { description = "headscale user"; @@ -465,65 +534,64 @@ in { systemd.services.headscale = { description = "headscale coordination server for Tailscale"; wants = [ "network-online.target" ]; - after = ["network-online.target"]; - wantedBy = ["multi-user.target"]; - restartTriggers = [configFile]; - - environment.GIN_MODE = "release"; + after = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; script = '' - ${optionalString (cfg.settings.db_password_file != null) '' - export HEADSCALE_DB_PASS="$(head -n1 ${escapeShellArg cfg.settings.db_password_file})" + ${optionalString (cfg.settings.database.postgres.password_file != null) '' + export HEADSCALE_DATABASE_POSTGRES_PASS="$(head -n1 ${escapeShellArg cfg.settings.database.postgres.password_file})" ''} - exec ${cfg.package}/bin/headscale serve + exec ${lib.getExe cfg.package} serve --config ${configFile} ''; - serviceConfig = let - capabilityBoundingSet = ["CAP_CHOWN"] ++ optional (cfg.port < 1024) "CAP_NET_BIND_SERVICE"; - in { - Restart = "always"; - Type = "simple"; - User = cfg.user; - Group = cfg.group; - - # Hardening options - RuntimeDirectory = "headscale"; - # Allow headscale group access so users can be added and use the CLI. - RuntimeDirectoryMode = "0750"; - - StateDirectory = "headscale"; - StateDirectoryMode = "0750"; - - ProtectSystem = "strict"; - ProtectHome = true; - PrivateTmp = true; - PrivateDevices = true; - ProtectKernelTunables = true; - ProtectControlGroups = true; - RestrictSUIDSGID = true; - PrivateMounts = true; - ProtectKernelModules = true; - ProtectKernelLogs = true; - ProtectHostname = true; - ProtectClock = true; - ProtectProc = "invisible"; - ProcSubset = "pid"; - RestrictNamespaces = true; - RemoveIPC = true; - UMask = "0077"; - - CapabilityBoundingSet = capabilityBoundingSet; - AmbientCapabilities = capabilityBoundingSet; - NoNewPrivileges = true; - LockPersonality = true; - RestrictRealtime = true; - SystemCallFilter = ["@system-service" "~@privileged" "@chown"]; - SystemCallArchitectures = "native"; - RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX"; - }; + serviceConfig = + let + capabilityBoundingSet = [ "CAP_CHOWN" ] ++ optional (cfg.port < 1024) "CAP_NET_BIND_SERVICE"; + in + { + Restart = "always"; + Type = "simple"; + User = cfg.user; + Group = cfg.group; + + # Hardening options + RuntimeDirectory = "headscale"; + # Allow headscale group access so users can be added and use the CLI. + RuntimeDirectoryMode = "0750"; + + StateDirectory = "headscale"; + StateDirectoryMode = "0750"; + + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + PrivateDevices = true; + ProtectKernelTunables = true; + ProtectControlGroups = true; + RestrictSUIDSGID = true; + PrivateMounts = true; + ProtectKernelModules = true; + ProtectKernelLogs = true; + ProtectHostname = true; + ProtectClock = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + RestrictNamespaces = true; + RemoveIPC = true; + UMask = "0077"; + + CapabilityBoundingSet = capabilityBoundingSet; + AmbientCapabilities = capabilityBoundingSet; + NoNewPrivileges = true; + LockPersonality = true; + RestrictRealtime = true; + SystemCallFilter = [ "@system-service" "~@privileged" "@chown" ]; + SystemCallArchitectures = "native"; + RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX"; + }; }; }; - meta.maintainers = with maintainers; [kradalby misterio77]; + meta.maintainers = with maintainers; [ kradalby misterio77 ]; } -- 2.46.1 From afff4fba9136ba0dbe8ff635cb3ead89d5240bf8 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 18 Sep 2024 10:24:19 +0100 Subject: [PATCH 2/8] nixos/headscale: update module to headscale 0.23.0 Signed-off-by: Kristoffer Dalby --- .../modules/services/networking/headscale.nix | 445 +++++++++--------- 1 file changed, 219 insertions(+), 226 deletions(-) diff --git a/nixos/modules/services/networking/headscale.nix b/nixos/modules/services/networking/headscale.nix index 645d4fe9c069..622a13fe7b61 100644 --- a/nixos/modules/services/networking/headscale.nix +++ b/nixos/modules/services/networking/headscale.nix @@ -1,9 +1,9 @@ -{ config -, lib -, pkgs -, ... -}: -with lib; let +{ + config, + lib, + pkgs, + ... +}: let cfg = config.services.headscale; dataDir = "/var/lib/headscale"; @@ -17,20 +17,19 @@ with lib; let unix_socket = "${runDir}/headscale.sock"; }; - settingsFormat = pkgs.formats.yaml { }; + settingsFormat = pkgs.formats.yaml {}; configFile = settingsFormat.generate "headscale.yaml" cfg.settings; cliConfigFile = settingsFormat.generate "headscale.yaml" cliConfig; -in -{ +in { options = { services.headscale = { - enable = mkEnableOption "headscale, Open Source coordination server for Tailscale"; + enable = lib.mkEnableOption "headscale, Open Source coordination server for Tailscale"; - package = mkPackageOption pkgs "headscale" { }; + package = lib.mkPackageOption pkgs "headscale" {}; - user = mkOption { + user = lib.mkOption { default = "headscale"; - type = types.str; + type = lib.types.str; description = '' User account under which headscale runs. @@ -42,9 +41,9 @@ in ''; }; - group = mkOption { + group = lib.mkOption { default = "headscale"; - type = types.str; + type = lib.types.str; description = '' Group under which headscale runs. @@ -56,8 +55,8 @@ in ''; }; - address = mkOption { - type = types.str; + address = lib.mkOption { + type = lib.types.str; default = "127.0.0.1"; description = '' Listening address of headscale. @@ -65,8 +64,8 @@ in example = "0.0.0.0"; }; - port = mkOption { - type = types.port; + port = lib.mkOption { + type = lib.types.port; default = 8080; description = '' Listening port of headscale. @@ -74,18 +73,33 @@ in example = 443; }; - settings = mkOption { + settings = lib.mkOption { description = '' Overrides to {file}`config.yaml` as a Nix attribute set. Check the [example config](https://github.com/juanfont/headscale/blob/main/config-example.yaml) for possible options. ''; - type = types.submodule { + type = lib.types.submodule { freeformType = settingsFormat.type; + imports = with lib; [ + (mkAliasOptionModule ["acl_policy_path"] ["policy" "path"]) + (mkAliasOptionModule ["db_host"] ["database" "postgres" "host"]) + (mkAliasOptionModule ["db_name"] ["database" "postgres" "name"]) + (mkAliasOptionModule ["db_password_file"] ["database" "postgres" "password_file"]) + (mkAliasOptionModule ["db_path"] ["database" "sqlite" "path"]) + (mkAliasOptionModule ["db_port"] ["database" "postgres" "port"]) + (mkAliasOptionModule ["db_type"] ["database" "type"]) + (mkAliasOptionModule ["db_user"] ["database" "postgres" "user"]) + (mkAliasOptionModule ["dns_config" "base_domain"] ["dns" "base_domain"]) + (mkAliasOptionModule ["dns_config" "domains"] ["dns" "search_domains"]) + (mkAliasOptionModule ["dns_config" "magic_dns"] ["dns" "magic_dns"]) + (mkAliasOptionModule ["dns_config" "nameservers"] ["dns" "nameservers" "global"]) + ]; + options = { - server_url = mkOption { - type = types.str; + server_url = lib.mkOption { + type = lib.types.str; default = "http://127.0.0.1:8080"; description = '' The url clients will connect to. @@ -93,69 +107,67 @@ in example = "https://myheadscale.example.com:443"; }; - noise.private_key_path = mkOption { - type = types.path; + noise.private_key_path = lib.mkOption { + type = lib.types.path; default = "${dataDir}/noise_private.key"; description = '' Path to noise private key file, generated automatically if it does not exist. ''; }; - prefixes = - let - prefDesc = '' - Each prefix consists of either an IPv4 or IPv6 address, - and the associated prefix length, delimited by a slash. - It must be within IP ranges supported by the Tailscale - client - i.e., subnets of 100.64.0.0/10 and fd7a:115c:a1e0::/48. - ''; - in - { - v4 = mkOption { - type = types.str; - default = "100.64.0.0/10"; - description = prefDesc; - }; + prefixes = let + prefDesc = '' + Each prefix consists of either an IPv4 or IPv6 address, + and the associated prefix length, delimited by a slash. + It must be within IP ranges supported by the Tailscale + client - i.e., subnets of 100.64.0.0/10 and fd7a:115c:a1e0::/48. + ''; + in { + v4 = lib.mkOption { + type = lib.types.str; + default = "100.64.0.0/10"; + description = prefDesc; + }; - v6 = mkOption { - type = types.str; - default = "fd7a:115c:a1e0::/48"; - description = prefDesc; - }; + v6 = lib.mkOption { + type = lib.types.str; + default = "fd7a:115c:a1e0::/48"; + description = prefDesc; + }; - allocation = mkOption { - type = types.enum [ "sequential" "random" ]; - example = "random"; - default = "sequential"; - description = '' - Strategy used for allocation of IPs to nodes, available options: - - sequential (default): assigns the next free IP from the previous given IP. - - random: assigns the next free IP from a pseudo-random IP generator (crypto/rand). - ''; - }; + allocation = lib.mkOption { + type = lib.types.enum ["sequential" "random"]; + example = "random"; + default = "sequential"; + description = '' + Strategy used for allocation of IPs to nodes, available options: + - sequential (default): assigns the next free IP from the previous given IP. + - random: assigns the next free IP from a pseudo-random IP generator (crypto/rand). + ''; }; + }; derp = { - urls = mkOption { - type = types.listOf types.str; - default = [ "https://controlplane.tailscale.com/derpmap/default" ]; + urls = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = ["https://controlplane.tailscale.com/derpmap/default"]; description = '' List of urls containing DERP maps. See [How Tailscale works](https://tailscale.com/blog/how-tailscale-works/) for more information on DERP maps. ''; }; - paths = mkOption { - type = types.listOf types.path; - default = [ ]; + paths = lib.mkOption { + type = lib.types.listOf lib.types.path; + default = []; description = '' List of file paths containing DERP maps. See [How Tailscale works](https://tailscale.com/blog/how-tailscale-works/) for more information on DERP maps. ''; }; - auto_update_enable = mkOption { - type = types.bool; + auto_update_enable = lib.mkOption { + type = lib.types.bool; default = true; description = '' Whether to automatically update DERP maps on a set frequency. @@ -163,8 +175,8 @@ in example = false; }; - update_frequency = mkOption { - type = types.str; + update_frequency = lib.mkOption { + type = lib.types.str; default = "24h"; description = '' Frequency to update DERP maps. @@ -181,8 +193,8 @@ in }; }; - ephemeral_node_inactivity_timeout = mkOption { - type = types.str; + ephemeral_node_inactivity_timeout = lib.mkOption { + type = lib.types.str; default = "30m"; description = '' Time before an inactive ephemeral node is deleted. @@ -191,8 +203,8 @@ in }; database = { - type = mkOption { - type = types.enum [ "sqlite" "sqlite3" "postgres" ]; + type = lib.mkOption { + type = lib.types.enum ["sqlite" "sqlite3" "postgres"]; example = "postgres"; default = "sqlite"; description = '' @@ -203,14 +215,14 @@ in }; sqlite = { - path = mkOption { - type = types.nullOr types.str; + path = lib.mkOption { + type = lib.types.nullOr lib.types.str; default = "${dataDir}/db.sqlite"; description = "Path to the sqlite3 database file."; }; - write_ahead_log = mkOption { - type = types.bool; + write_ahead_log = lib.mkOption { + type = lib.types.bool; default = true; description = '' Enable WAL mode for SQLite. This is recommended for production environments. @@ -221,36 +233,36 @@ in }; postgres = { - host = mkOption { - type = types.nullOr types.str; + host = lib.mkOption { + type = lib.types.nullOr lib.types.str; default = null; example = "127.0.0.1"; description = "Database host address."; }; - port = mkOption { - type = types.nullOr types.port; + port = lib.mkOption { + type = lib.types.nullOr lib.types.port; default = null; example = 3306; description = "Database host port."; }; - name = mkOption { - type = types.nullOr types.str; + name = lib.mkOption { + type = lib.types.nullOr lib.types.str; default = null; example = "headscale"; description = "Database name."; }; - user = mkOption { - type = types.nullOr types.str; + user = lib.mkOption { + type = lib.types.nullOr lib.types.str; default = null; example = "headscale"; description = "Database user."; }; - password_file = mkOption { - type = types.nullOr types.path; + password_file = lib.mkOption { + type = lib.types.nullOr lib.types.path; default = null; example = "/run/keys/headscale-dbpassword"; description = '' @@ -262,8 +274,8 @@ in }; log = { - level = mkOption { - type = types.str; + level = lib.mkOption { + type = lib.types.str; default = "info"; description = '' headscale log level. @@ -271,8 +283,8 @@ in example = "debug"; }; - format = mkOption { - type = types.str; + format = lib.mkOption { + type = lib.types.str; default = "text"; description = '' headscale log format. @@ -282,8 +294,8 @@ in }; dns = { - magic_dns = mkOption { - type = types.bool; + magic_dns = lib.mkOption { + type = lib.types.bool; default = true; description = '' Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/). @@ -292,8 +304,8 @@ in example = false; }; - base_domain = mkOption { - type = types.str; + base_domain = lib.mkOption { + type = lib.types.str; default = ""; description = '' Defines the base domain to create the hostnames for MagicDNS. @@ -305,28 +317,28 @@ in }; nameservers = { - global = mkOption { - type = types.listOf types.str; - default = [ ]; + global = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = []; description = '' List of nameservers to pass to Tailscale clients. ''; }; }; - search_domains = mkOption { - type = types.listOf types.str; - default = [ ]; + search_domains = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = []; description = '' Search domains to inject to Tailscale clients. ''; - example = [ "mydomain.internal" ]; + example = ["mydomain.internal"]; }; }; oidc = { - issuer = mkOption { - type = types.str; + issuer = lib.mkOption { + type = lib.types.str; default = ""; description = '' URL to OpenID issuer. @@ -334,33 +346,33 @@ in example = "https://openid.example.com"; }; - client_id = mkOption { - type = types.str; + client_id = lib.mkOption { + type = lib.types.str; default = ""; description = '' OpenID Connect client ID. ''; }; - client_secret_path = mkOption { - type = types.nullOr types.str; + client_secret_path = lib.mkOption { + type = lib.types.nullOr lib.types.str; default = null; description = '' Path to OpenID Connect client secret file. Expands environment variables in format ''${VAR}. ''; }; - scope = mkOption { - type = types.listOf types.str; - default = [ "openid" "profile" "email" ]; + scope = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = ["openid" "profile" "email"]; description = '' Scopes used in the OIDC flow. ''; }; - extra_params = mkOption { - type = types.attrsOf types.str; - default = { }; + extra_params = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = {}; description = '' Custom query parameters to send with the Authorize Endpoint request. ''; @@ -369,27 +381,27 @@ in }; }; - allowed_domains = mkOption { - type = types.listOf types.str; - default = [ ]; + allowed_domains = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = []; description = '' Allowed principal domains. if an authenticated user's domain is not in this list authentication request will be rejected. ''; - example = [ "example.com" ]; + example = ["example.com"]; }; - allowed_users = mkOption { - type = types.listOf types.str; - default = [ ]; + allowed_users = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = []; description = '' Users allowed to authenticate even if not in allowedDomains. ''; - example = [ "alice@example.com" ]; + example = ["alice@example.com"]; }; - strip_email_domain = mkOption { - type = types.bool; + strip_email_domain = lib.mkOption { + type = lib.types.bool; default = true; description = '' Whether the domain part of the email address should be removed when generating namespaces. @@ -397,16 +409,16 @@ in }; }; - tls_letsencrypt_hostname = mkOption { - type = types.nullOr types.str; + tls_letsencrypt_hostname = lib.mkOption { + type = lib.types.nullOr lib.types.str; default = ""; description = '' Domain name to request a TLS certificate for. ''; }; - tls_letsencrypt_challenge_type = mkOption { - type = types.enum [ "TLS-ALPN-01" "HTTP-01" ]; + tls_letsencrypt_challenge_type = lib.mkOption { + type = lib.types.enum ["TLS-ALPN-01" "HTTP-01"]; default = "HTTP-01"; description = '' Type of ACME challenge to use, currently supported types: @@ -414,8 +426,8 @@ in ''; }; - tls_letsencrypt_listen = mkOption { - type = types.nullOr types.str; + tls_letsencrypt_listen = lib.mkOption { + type = lib.types.nullOr lib.types.str; default = ":http"; description = '' When HTTP-01 challenge is chosen, letsencrypt must set up a @@ -424,16 +436,16 @@ in ''; }; - tls_cert_path = mkOption { - type = types.nullOr types.path; + tls_cert_path = lib.mkOption { + type = lib.types.nullOr lib.types.path; default = null; description = '' Path to already created certificate. ''; }; - tls_key_path = mkOption { - type = types.nullOr types.path; + tls_key_path = lib.mkOption { + type = lib.types.nullOr lib.types.path; default = null; description = '' Path to key for already created certificate. @@ -441,8 +453,8 @@ in }; policy = { - mode = mkOption { - type = types.enum [ "file" "database" ]; + mode = lib.mkOption { + type = lib.types.enum ["file" "database"]; default = "file"; description = '' The mode can be "file" or "database" that defines @@ -450,8 +462,8 @@ in ''; }; - path = mkOption { - type = types.nullOr types.path; + path = lib.mkOption { + type = lib.types.nullOr lib.types.path; default = null; description = '' If the mode is set to "file", the path to a @@ -465,50 +477,33 @@ in }; }; - imports = [ - (mkRenamedOptionModule [ "services" "headscale" "serverUrl" ] [ "services" "headscale" "settings" "server_url" ]) - (mkRenamedOptionModule [ "services" "headscale" "derp" "urls" ] [ "services" "headscale" "settings" "derp" "urls" ]) - (mkRenamedOptionModule [ "services" "headscale" "derp" "paths" ] [ "services" "headscale" "settings" "derp" "paths" ]) - (mkRenamedOptionModule [ "services" "headscale" "derp" "autoUpdate" ] [ "services" "headscale" "settings" "derp" "auto_update_enable" ]) - (mkRenamedOptionModule [ "services" "headscale" "derp" "updateFrequency" ] [ "services" "headscale" "settings" "derp" "update_frequency" ]) - (mkRenamedOptionModule [ "services" "headscale" "ephemeralNodeInactivityTimeout" ] [ "services" "headscale" "settings" "ephemeral_node_inactivity_timeout" ]) - - # (mkRenamedOptionModule ["services" "headscale" "settings" "db_type"] ["services" "headscale" "settings" "database" "type"]) - # (mkRenamedOptionModule ["services" "headscale" "settings" "db_path"] ["services" "headscale" "settings" "database" "sqlite" "path"]) - # (mkRenamedOptionModule ["services" "headscale" "settings" "db_host"] ["services" "headscale" "settings" "database" "postgres" "host"]) - # (mkRenamedOptionModule ["services" "headscale" "settings" "db_port"] ["services" "headscale" "settings" "database" "postgres" "port"]) - # (mkRenamedOptionModule ["services" "headscale" "settings" "db_name"] ["services" "headscale" "settings" "database" "postgres" "name"]) - # (mkRenamedOptionModule ["services" "headscale" "settings" "db_user"] ["services" "headscale" "settings" "database" "postgres" "user"]) - # (mkRenamedOptionModule ["services" "headscale" "settings" "db_password_file"] ["services" "headscale" "settings" "database" "postgres" "password_file"]) - - (mkRenamedOptionModule [ "services" "headscale" "logLevel" ] [ "services" "headscale" "settings" "log" "level" ]) - - # (mkRenamedOptionModule ["services" "headscale" "settings" "dns_config" "nameservers"] ["services" "headscale" "settings" "dns" "nameservers" "global"]) - # (mkRenamedOptionModule ["services" "headscale" "settings" "dns_config" "domains"] ["services" "headscale" "settings" "dns" "search_domains"]) - # (mkRenamedOptionModule ["services" "headscale" "settings" "dns_config" "magic_dns"] ["services" "headscale" "settings" "dns" "magic_dns"]) - # (mkRenamedOptionModule ["services" "headscale" "settings" "dns_config" "base_domain"] ["services" "headscale" "settings" "dns" "base_domain"]) - - (mkRenamedOptionModule [ "services" "headscale" "openIdConnect" "issuer" ] [ "services" "headscale" "settings" "oidc" "issuer" ]) - (mkRenamedOptionModule [ "services" "headscale" "openIdConnect" "clientId" ] [ "services" "headscale" "settings" "oidc" "client_id" ]) - (mkRenamedOptionModule [ "services" "headscale" "openIdConnect" "clientSecretFile" ] [ "services" "headscale" "settings" "oidc" "client_secret_path" ]) - (mkRenamedOptionModule [ "services" "headscale" "tls" "letsencrypt" "hostname" ] [ "services" "headscale" "settings" "tls_letsencrypt_hostname" ]) - (mkRenamedOptionModule [ "services" "headscale" "tls" "letsencrypt" "challengeType" ] [ "services" "headscale" "settings" "tls_letsencrypt_challenge_type" ]) - (mkRenamedOptionModule [ "services" "headscale" "tls" "letsencrypt" "httpListen" ] [ "services" "headscale" "settings" "tls_letsencrypt_listen" ]) - (mkRenamedOptionModule [ "services" "headscale" "tls" "certFile" ] [ "services" "headscale" "settings" "tls_cert_path" ]) - (mkRenamedOptionModule [ "services" "headscale" "tls" "keyFile" ] [ "services" "headscale" "settings" "tls_key_path" ]) - - # (mkRenamedOptionModule ["services" "headscale" "settings" "acl_policy_path"] ["services" "headscale" "settings" "policy" "path"]) - - (mkRemovedOptionModule [ "services" "headscale" "openIdConnect" "domainMap" ] '' + imports = with lib; [ + (mkRenamedOptionModule ["services" "headscale" "derp" "autoUpdate"] ["services" "headscale" "settings" "derp" "auto_update_enable"]) + (mkRenamedOptionModule ["services" "headscale" "derp" "paths"] ["services" "headscale" "settings" "derp" "paths"]) + (mkRenamedOptionModule ["services" "headscale" "derp" "updateFrequency"] ["services" "headscale" "settings" "derp" "update_frequency"]) + (mkRenamedOptionModule ["services" "headscale" "derp" "urls"] ["services" "headscale" "settings" "derp" "urls"]) + (mkRenamedOptionModule ["services" "headscale" "ephemeralNodeInactivityTimeout"] ["services" "headscale" "settings" "ephemeral_node_inactivity_timeout"]) + (mkRenamedOptionModule ["services" "headscale" "logLevel"] ["services" "headscale" "settings" "log" "level"]) + (mkRenamedOptionModule ["services" "headscale" "openIdConnect" "clientId"] ["services" "headscale" "settings" "oidc" "client_id"]) + (mkRenamedOptionModule ["services" "headscale" "openIdConnect" "clientSecretFile"] ["services" "headscale" "settings" "oidc" "client_secret_path"]) + (mkRenamedOptionModule ["services" "headscale" "openIdConnect" "issuer"] ["services" "headscale" "settings" "oidc" "issuer"]) + (mkRenamedOptionModule ["services" "headscale" "serverUrl"] ["services" "headscale" "settings" "server_url"]) + (mkRenamedOptionModule ["services" "headscale" "tls" "certFile"] ["services" "headscale" "settings" "tls_cert_path"]) + (mkRenamedOptionModule ["services" "headscale" "tls" "keyFile"] ["services" "headscale" "settings" "tls_key_path"]) + (mkRenamedOptionModule ["services" "headscale" "tls" "letsencrypt" "challengeType"] ["services" "headscale" "settings" "tls_letsencrypt_challenge_type"]) + (mkRenamedOptionModule ["services" "headscale" "tls" "letsencrypt" "hostname"] ["services" "headscale" "settings" "tls_letsencrypt_hostname"]) + (mkRenamedOptionModule ["services" "headscale" "tls" "letsencrypt" "httpListen"] ["services" "headscale" "settings" "tls_letsencrypt_listen"]) + + (mkRemovedOptionModule ["services" "headscale" "openIdConnect" "domainMap"] '' Headscale no longer uses domain_map. If you're using an old version of headscale you can still set this option via services.headscale.settings.oidc.domain_map. '') ]; - config = mkIf cfg.enable { - services.headscale.settings = mkMerge [ + config = lib.mkIf cfg.enable { + services.headscale.settings = lib.mkMerge [ cliConfig { - listen_addr = mkDefault "${cfg.address}:${toString cfg.port}"; + listen_addr = lib.mkDefault "${cfg.address}:${toString cfg.port}"; tls_letsencrypt_cache_dir = "${dataDir}/.cache"; } @@ -519,12 +514,12 @@ in # to talk to the server instance. etc."headscale/config.yaml".source = cliConfigFile; - systemPackages = [ cfg.package ]; + systemPackages = [cfg.package]; }; - users.groups.headscale = mkIf (cfg.group == "headscale") { }; + users.groups.headscale = lib.mkIf (cfg.group == "headscale") {}; - users.users.headscale = mkIf (cfg.user == "headscale") { + users.users.headscale = lib.mkIf (cfg.user == "headscale") { description = "headscale user"; home = dataDir; group = cfg.group; @@ -533,65 +528,63 @@ in systemd.services.headscale = { description = "headscale coordination server for Tailscale"; - wants = [ "network-online.target" ]; - after = [ "network-online.target" ]; - wantedBy = [ "multi-user.target" ]; + wants = ["network-online.target"]; + after = ["network-online.target"]; + wantedBy = ["multi-user.target"]; script = '' - ${optionalString (cfg.settings.database.postgres.password_file != null) '' - export HEADSCALE_DATABASE_POSTGRES_PASS="$(head -n1 ${escapeShellArg cfg.settings.database.postgres.password_file})" + ${lib.optionalString (cfg.settings.database.postgres.password_file != null) '' + export HEADSCALE_DATABASE_POSTGRES_PASS="$(head -n1 ${lib.escapeShellArg cfg.settings.database.postgres.password_file})" ''} exec ${lib.getExe cfg.package} serve --config ${configFile} ''; - serviceConfig = - let - capabilityBoundingSet = [ "CAP_CHOWN" ] ++ optional (cfg.port < 1024) "CAP_NET_BIND_SERVICE"; - in - { - Restart = "always"; - Type = "simple"; - User = cfg.user; - Group = cfg.group; - - # Hardening options - RuntimeDirectory = "headscale"; - # Allow headscale group access so users can be added and use the CLI. - RuntimeDirectoryMode = "0750"; - - StateDirectory = "headscale"; - StateDirectoryMode = "0750"; - - ProtectSystem = "strict"; - ProtectHome = true; - PrivateTmp = true; - PrivateDevices = true; - ProtectKernelTunables = true; - ProtectControlGroups = true; - RestrictSUIDSGID = true; - PrivateMounts = true; - ProtectKernelModules = true; - ProtectKernelLogs = true; - ProtectHostname = true; - ProtectClock = true; - ProtectProc = "invisible"; - ProcSubset = "pid"; - RestrictNamespaces = true; - RemoveIPC = true; - UMask = "0077"; - - CapabilityBoundingSet = capabilityBoundingSet; - AmbientCapabilities = capabilityBoundingSet; - NoNewPrivileges = true; - LockPersonality = true; - RestrictRealtime = true; - SystemCallFilter = [ "@system-service" "~@privileged" "@chown" ]; - SystemCallArchitectures = "native"; - RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX"; - }; + serviceConfig = let + capabilityBoundingSet = ["CAP_CHOWN"] ++ lib.optional (cfg.port < 1024) "CAP_NET_BIND_SERVICE"; + in { + Restart = "always"; + Type = "simple"; + User = cfg.user; + Group = cfg.group; + + # Hardening options + RuntimeDirectory = "headscale"; + # Allow headscale group access so users can be added and use the CLI. + RuntimeDirectoryMode = "0750"; + + StateDirectory = "headscale"; + StateDirectoryMode = "0750"; + + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + PrivateDevices = true; + ProtectKernelTunables = true; + ProtectControlGroups = true; + RestrictSUIDSGID = true; + PrivateMounts = true; + ProtectKernelModules = true; + ProtectKernelLogs = true; + ProtectHostname = true; + ProtectClock = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + RestrictNamespaces = true; + RemoveIPC = true; + UMask = "0077"; + + CapabilityBoundingSet = capabilityBoundingSet; + AmbientCapabilities = capabilityBoundingSet; + NoNewPrivileges = true; + LockPersonality = true; + RestrictRealtime = true; + SystemCallFilter = ["@system-service" "~@privileged" "@chown"]; + SystemCallArchitectures = "native"; + RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX"; + }; }; }; - meta.maintainers = with maintainers; [ kradalby misterio77 ]; + meta.maintainers = with lib.maintainers; [kradalby misterio77]; } -- 2.46.1 From 7b2c10bda8a50ea531a444526a3e89e2851bd681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=BCtz?= Date: Fri, 11 Oct 2024 13:23:36 -0700 Subject: [PATCH 3/8] nixos/headscale: assert that server_url does not contain base_domain --- nixos/modules/services/networking/headscale.nix | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/nixos/modules/services/networking/headscale.nix b/nixos/modules/services/networking/headscale.nix index 622a13fe7b61..c2e616d30e87 100644 --- a/nixos/modules/services/networking/headscale.nix +++ b/nixos/modules/services/networking/headscale.nix @@ -500,6 +500,15 @@ in { ]; config = lib.mkIf cfg.enable { + assertions = [ + { + # This is stricter than it needs to be but is exactly what upstream does: + # https://github.com/kradalby/headscale/blob/adc084f20f843d7963c999764fa83939668d2d2c/hscontrol/types/config.go#L799 + assertion = with cfg.settings; dns.use_username_in_magic_dns or false || dns.base_domain == "" || !lib.hasInfix dns.base_domain server_url; + message = "server_url cannot contain the base_domain, this will cause the headscale server and embedded DERP to become unreachable from the Tailscale node."; + } + ]; + services.headscale.settings = lib.mkMerge [ cliConfig { -- 2.46.1 From 0c1215620750c8ca403827076f3a3d02ce419889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=BCtz?= Date: Fri, 11 Oct 2024 13:58:20 -0700 Subject: [PATCH 4/8] nixos/headscale: don't set deprecated options in config We cannot use `mkRenamedOptionModule` or `mkRemovedOptionModule` inside a freeform option. Thus we have to manually assert these deprecated options aren't used rather than aliasing them to their replacement. --- .../modules/services/networking/headscale.nix | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/nixos/modules/services/networking/headscale.nix b/nixos/modules/services/networking/headscale.nix index c2e616d30e87..fd2fd8dbede9 100644 --- a/nixos/modules/services/networking/headscale.nix +++ b/nixos/modules/services/networking/headscale.nix @@ -20,6 +20,11 @@ settingsFormat = pkgs.formats.yaml {}; configFile = settingsFormat.generate "headscale.yaml" cfg.settings; cliConfigFile = settingsFormat.generate "headscale.yaml" cliConfig; + + assertRemovedOption = option: message: { + assertion = !lib.hasAttrByPath option cfg; + message = "The option `services.headscale.${lib.options.showOption option}` was removed. " + message; + }; in { options = { services.headscale = { @@ -82,21 +87,6 @@ in { type = lib.types.submodule { freeformType = settingsFormat.type; - imports = with lib; [ - (mkAliasOptionModule ["acl_policy_path"] ["policy" "path"]) - (mkAliasOptionModule ["db_host"] ["database" "postgres" "host"]) - (mkAliasOptionModule ["db_name"] ["database" "postgres" "name"]) - (mkAliasOptionModule ["db_password_file"] ["database" "postgres" "password_file"]) - (mkAliasOptionModule ["db_path"] ["database" "sqlite" "path"]) - (mkAliasOptionModule ["db_port"] ["database" "postgres" "port"]) - (mkAliasOptionModule ["db_type"] ["database" "type"]) - (mkAliasOptionModule ["db_user"] ["database" "postgres" "user"]) - (mkAliasOptionModule ["dns_config" "base_domain"] ["dns" "base_domain"]) - (mkAliasOptionModule ["dns_config" "domains"] ["dns" "search_domains"]) - (mkAliasOptionModule ["dns_config" "magic_dns"] ["dns" "magic_dns"]) - (mkAliasOptionModule ["dns_config" "nameservers"] ["dns" "nameservers" "global"]) - ]; - options = { server_url = lib.mkOption { type = lib.types.str; @@ -507,6 +497,17 @@ in { assertion = with cfg.settings; dns.use_username_in_magic_dns or false || dns.base_domain == "" || !lib.hasInfix dns.base_domain server_url; message = "server_url cannot contain the base_domain, this will cause the headscale server and embedded DERP to become unreachable from the Tailscale node."; } + (assertRemovedOption ["settings" "acl_policy_path"] "Use `policy.path` instead.") + (assertRemovedOption ["settings" "db_host"] "Use `database.postgres.host` instead.") + (assertRemovedOption ["settings" "db_name"] "Use `database.postgres.name` instead.") + (assertRemovedOption ["settings" "db_password_file"] "Use `database.postgres.password_file` instead.") + (assertRemovedOption ["settings" "db_path"] "Use `database.sqlite.path` instead.") + (assertRemovedOption ["settings" "db_port"] "Use `database.postgres.port` instead.") + (assertRemovedOption ["settings" "db_type"] "Use `database.type` instead.") + (assertRemovedOption ["settings" "db_user"] "Use `database.postgres.user` instead.") + (assertRemovedOption ["settings" "dns_config"] "Use `dns` instead.") + (assertRemovedOption ["settings" "dns_config" "domains"] "Use `dns.search_domains` instead.") + (assertRemovedOption ["settings" "dns_config" "nameservers"] "Use `dns.nameservers.global` instead.") ]; services.headscale.settings = lib.mkMerge [ -- 2.46.1 From 31de80dcfee97b024298128836c8ab49ca7ae798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=BCtz?= Date: Fri, 11 Oct 2024 20:17:15 -0700 Subject: [PATCH 5/8] nixos/headscale: update option descriptions --- nixos/modules/services/networking/headscale.nix | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/nixos/modules/services/networking/headscale.nix b/nixos/modules/services/networking/headscale.nix index fd2fd8dbede9..aac6d331a027 100644 --- a/nixos/modules/services/networking/headscale.nix +++ b/nixos/modules/services/networking/headscale.nix @@ -289,7 +289,6 @@ in { default = true; description = '' Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/). - Only works if there is at least a nameserver defined. ''; example = false; }; @@ -299,11 +298,13 @@ in { default = ""; description = '' Defines the base domain to create the hostnames for MagicDNS. - {option}`baseDomain` must be a FQDNs, without the trailing dot. - The FQDN of the hosts will be - `hostname.namespace.base_domain` (e.g. - `myhost.mynamespace.example.com`). + This domain must be different from the {option}`server_url` + domain. + {option}`base_domain` must be a FQDN, without the trailing dot. + The FQDN of the hosts will be `hostname.base_domain` (e.g. + `myhost.tailnet.example.com`). ''; + example = "tailnet.example.com"; }; nameservers = { -- 2.46.1 From 083dec59811d7bfcaed1a34fc33d33eabc40fcc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=BCtz?= Date: Sat, 12 Oct 2024 18:28:17 -0700 Subject: [PATCH 6/8] nixos/headscale: assert that dns.base_domain is set when using MagicDNS --- nixos/modules/services/networking/headscale.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nixos/modules/services/networking/headscale.nix b/nixos/modules/services/networking/headscale.nix index aac6d331a027..9261ec03c532 100644 --- a/nixos/modules/services/networking/headscale.nix +++ b/nixos/modules/services/networking/headscale.nix @@ -498,6 +498,10 @@ in { assertion = with cfg.settings; dns.use_username_in_magic_dns or false || dns.base_domain == "" || !lib.hasInfix dns.base_domain server_url; message = "server_url cannot contain the base_domain, this will cause the headscale server and embedded DERP to become unreachable from the Tailscale node."; } + { + assertion = with cfg.settings; dns.magic_dns -> dns.base_domain != ""; + message = "dns.base_domain must be set when using MagicDNS"; + } (assertRemovedOption ["settings" "acl_policy_path"] "Use `policy.path` instead.") (assertRemovedOption ["settings" "db_host"] "Use `database.postgres.host` instead.") (assertRemovedOption ["settings" "db_name"] "Use `database.postgres.name` instead.") -- 2.46.1 From 720b90273528c02a0fb865062fe44a45eb04f82e Mon Sep 17 00:00:00 2001 From: Jennifer Graul Date: Wed, 13 Sep 2023 13:32:47 +0200 Subject: [PATCH 7/8] headscale: fix reacting to SIGTERM The current version of headscale does not react to SIGTERMs and so it can only be terminated by a SIGKILL at the moment. This commit provides a patch to fix this. --- pkgs/servers/headscale/default.nix | 4 ++++ pkgs/servers/headscale/sigterm-fix.patch | 12 ++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 pkgs/servers/headscale/sigterm-fix.patch diff --git a/pkgs/servers/headscale/default.nix b/pkgs/servers/headscale/default.nix index fca56f5fc848..eb9ad79b1888 100644 --- a/pkgs/servers/headscale/default.nix +++ b/pkgs/servers/headscale/default.nix @@ -21,6 +21,10 @@ buildGoModule rec { patches = [ # backport of https://github.com/juanfont/headscale/pull/1697 ./trim-oidc-secret-path.patch + + # fix for headscale not reacting to SIGTERM + # see https://github.com/juanfont/headscale/pull/1480 and https://github.com/juanfont/headscale/issues/1461 + ./sigterm-fix.patch ]; ldflags = ["-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}"]; diff --git a/pkgs/servers/headscale/sigterm-fix.patch b/pkgs/servers/headscale/sigterm-fix.patch new file mode 100644 index 000000000000..22a97d98caac --- /dev/null +++ b/pkgs/servers/headscale/sigterm-fix.patch @@ -0,0 +1,12 @@ +diff --git a/hscontrol/app.go b/hscontrol/app.go +index b8dceba..4bcf019 100644 +--- a/hscontrol/app.go ++++ b/hscontrol/app.go +@@ -821,6 +821,7 @@ func (h *Headscale) Serve() error { + + // And we're done: + cancel() ++ return + } + } + } -- 2.46.1 From 1a1fa839c7c8fafdea120b8b9af28d8839f8cafb Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 6 Sep 2024 13:32:52 +0200 Subject: [PATCH 8/8] headscale: 0.22.3 -> 0.23.0 Signed-off-by: Kristoffer Dalby --- pkgs/servers/headscale/default.nix | 20 +++++-------------- pkgs/servers/headscale/sigterm-fix.patch | 12 ----------- .../headscale/trim-oidc-secret-path.patch | 13 ------------ pkgs/top-level/all-packages.nix | 4 +++- 4 files changed, 8 insertions(+), 41 deletions(-) delete mode 100644 pkgs/servers/headscale/sigterm-fix.patch delete mode 100644 pkgs/servers/headscale/trim-oidc-secret-path.patch diff --git a/pkgs/servers/headscale/default.nix b/pkgs/servers/headscale/default.nix index eb9ad79b1888..9f127946217e 100644 --- a/pkgs/servers/headscale/default.nix +++ b/pkgs/servers/headscale/default.nix @@ -7,33 +7,22 @@ }: buildGoModule rec { pname = "headscale"; - version = "0.22.3"; + version = "0.23.0"; src = fetchFromGitHub { owner = "juanfont"; repo = "headscale"; rev = "v${version}"; - hash = "sha256-nqmTqe3F3Oh8rnJH0clwACD/0RpqmfOMXNubr3C8rEc="; + hash = "sha256-5tlnVNpn+hJayxHjTpbOO3kRInOYOFz0pe9pwjXZlBE="; }; - vendorHash = "sha256-IOkbbFtE6+tNKnglE/8ZuNxhPSnloqM2sLgTvagMmnc="; - - patches = [ - # backport of https://github.com/juanfont/headscale/pull/1697 - ./trim-oidc-secret-path.patch - - # fix for headscale not reacting to SIGTERM - # see https://github.com/juanfont/headscale/pull/1480 and https://github.com/juanfont/headscale/issues/1461 - ./sigterm-fix.patch - ]; + vendorHash = "sha256-+8dOxPG/Q+wuHgRwwWqdphHOuop0W9dVyClyQuh7aRc="; ldflags = ["-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}"]; nativeBuildInputs = [installShellFiles]; checkFlags = ["-short"]; - tags = ["ts2019"]; - postInstall = '' installShellCompletion --cmd headscale \ --bash <($out/bin/headscale completion bash) \ @@ -41,7 +30,7 @@ buildGoModule rec { --zsh <($out/bin/headscale completion zsh) ''; - passthru.tests = { inherit (nixosTests) headscale; }; + passthru.tests = {inherit (nixosTests) headscale;}; meta = with lib; { homepage = "https://github.com/juanfont/headscale"; @@ -63,6 +52,7 @@ buildGoModule rec { Headscale implements this coordination server. ''; license = licenses.bsd3; + mainProgram = "headscale"; maintainers = with maintainers; [nkje jk kradalby misterio77 ghuntley]; }; } diff --git a/pkgs/servers/headscale/sigterm-fix.patch b/pkgs/servers/headscale/sigterm-fix.patch deleted file mode 100644 index 22a97d98caac..000000000000 --- a/pkgs/servers/headscale/sigterm-fix.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/hscontrol/app.go b/hscontrol/app.go -index b8dceba..4bcf019 100644 ---- a/hscontrol/app.go -+++ b/hscontrol/app.go -@@ -821,6 +821,7 @@ func (h *Headscale) Serve() error { - - // And we're done: - cancel() -+ return - } - } - } diff --git a/pkgs/servers/headscale/trim-oidc-secret-path.patch b/pkgs/servers/headscale/trim-oidc-secret-path.patch deleted file mode 100644 index 4275988aa7db..000000000000 --- a/pkgs/servers/headscale/trim-oidc-secret-path.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/hscontrol/config.go b/hscontrol/config.go -index 0e83a1c..71fbfb0 100644 ---- a/hscontrol/config.go -+++ b/hscontrol/config.go -@@ -573,7 +573,7 @@ func GetHeadscaleConfig() (*Config, error) { - if err != nil { - return nil, err - } -- oidcClientSecret = string(secretBytes) -+ oidcClientSecret = strings.TrimSpace(string(secretBytes)) - } - - return &Config{ diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index 39aac77560e1..c1811da9b6a4 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -8912,7 +8912,9 @@ with pkgs; heimdall-gui = heimdall.override { enableGUI = true; }; - headscale = callPackage ../servers/headscale { }; + headscale = callPackage ../servers/headscale { + buildGoModule = buildGo123Module; + }; health = callPackage ../applications/misc/health { }; -- 2.46.1