From fdad5d88bbf5942f89add50faf4e2ac7285e76ce Mon Sep 17 00:00:00 2001 From: Toast Date: Tue, 5 Nov 2024 19:22:35 +0100 Subject: [PATCH] Flake: backport headscale changes to stable nixpkgs --- flake.nix | 4 +- .../backport_unstable_headscale_changes.patch | 1757 +++++++++++++++++ 2 files changed, 1760 insertions(+), 1 deletion(-) create mode 100644 nixpkgs-patches/backport_unstable_headscale_changes.patch diff --git a/flake.nix b/flake.nix index 2832d46..18f8ed7 100644 --- a/flake.nix +++ b/flake.nix @@ -111,7 +111,9 @@ nixpkgs-patched = nixpkgs-raw.legacyPackages.x86_64-linux.applyPatches { name = "patched-nixpkgs"; src = nixpkgs-raw; - patches = []; + patches = [ + ./nixpkgs-patches/backport_unstable_headscale_changes.patch + ]; }; # https://discourse.nixos.org/t/proper-way-of-applying-patch-to-system-managed-via-flake/21073/26 nixpkgs-unstable = (import "${nixpkgs-unstable-patched}/flake.nix").outputs {self = inputs.self;}; diff --git a/nixpkgs-patches/backport_unstable_headscale_changes.patch b/nixpkgs-patches/backport_unstable_headscale_changes.patch new file mode 100644 index 0000000..c173ef9 --- /dev/null +++ b/nixpkgs-patches/backport_unstable_headscale_changes.patch @@ -0,0 +1,1757 @@ +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 +