nix-stuff/nixpkgs-patches/backport_unstable_headscale_changes.patch

1757 lines
72 KiB
Diff

From 4a5c88eac3aaf1fb3d5ab15ee84787b9883c2465 Mon Sep 17 00:00:00 2001
From: Kristoffer Dalby <kristoffer@tailscale.com>
Date: Fri, 6 Sep 2024 12:47:36 +0200
Subject: [PATCH 1/8] nixos/headscale: modernize
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
---
.../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 <kristoffer@tailscale.com>
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 <kristoffer@tailscale.com>
---
.../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?= <mail@dotlambda.de>
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?= <mail@dotlambda.de>
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?= <mail@dotlambda.de>
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?= <mail@dotlambda.de>
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 <jennifer.graul@wobcom.de>
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 <kristoffer@tailscale.com>
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 <kristoffer@tailscale.com>
---
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