nixos/headscale: update module to headscale 0.23.0

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
Kristoffer Dalby 2024-09-18 10:24:19 +01:00 committed by Sandro Jäckel
parent aec399ee4b
commit abb3b0089b
No known key found for this signature in database
GPG Key ID: 3AF5A43A3EECC2E5

View File

@ -1,9 +1,9 @@
{ config {
, lib config,
, pkgs lib,
, ... pkgs,
}: ...
with lib; let }: let
cfg = config.services.headscale; cfg = config.services.headscale;
dataDir = "/var/lib/headscale"; dataDir = "/var/lib/headscale";
@ -17,20 +17,19 @@ with lib; let
unix_socket = "${runDir}/headscale.sock"; unix_socket = "${runDir}/headscale.sock";
}; };
settingsFormat = pkgs.formats.yaml { }; settingsFormat = pkgs.formats.yaml {};
configFile = settingsFormat.generate "headscale.yaml" cfg.settings; configFile = settingsFormat.generate "headscale.yaml" cfg.settings;
cliConfigFile = settingsFormat.generate "headscale.yaml" cliConfig; cliConfigFile = settingsFormat.generate "headscale.yaml" cliConfig;
in in {
{
options = { options = {
services.headscale = { 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"; default = "headscale";
type = types.str; type = lib.types.str;
description = '' description = ''
User account under which headscale runs. User account under which headscale runs.
@ -42,9 +41,9 @@ in
''; '';
}; };
group = mkOption { group = lib.mkOption {
default = "headscale"; default = "headscale";
type = types.str; type = lib.types.str;
description = '' description = ''
Group under which headscale runs. Group under which headscale runs.
@ -56,8 +55,8 @@ in
''; '';
}; };
address = mkOption { address = lib.mkOption {
type = types.str; type = lib.types.str;
default = "127.0.0.1"; default = "127.0.0.1";
description = '' description = ''
Listening address of headscale. Listening address of headscale.
@ -65,8 +64,8 @@ in
example = "0.0.0.0"; example = "0.0.0.0";
}; };
port = mkOption { port = lib.mkOption {
type = types.port; type = lib.types.port;
default = 8080; default = 8080;
description = '' description = ''
Listening port of headscale. Listening port of headscale.
@ -74,18 +73,33 @@ in
example = 443; example = 443;
}; };
settings = mkOption { settings = lib.mkOption {
description = '' description = ''
Overrides to {file}`config.yaml` as a Nix attribute set. Overrides to {file}`config.yaml` as a Nix attribute set.
Check the [example config](https://github.com/juanfont/headscale/blob/main/config-example.yaml) Check the [example config](https://github.com/juanfont/headscale/blob/main/config-example.yaml)
for possible options. for possible options.
''; '';
type = types.submodule { type = lib.types.submodule {
freeformType = settingsFormat.type; 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 = { options = {
server_url = mkOption { server_url = lib.mkOption {
type = types.str; type = lib.types.str;
default = "http://127.0.0.1:8080"; default = "http://127.0.0.1:8080";
description = '' description = ''
The url clients will connect to. The url clients will connect to.
@ -93,69 +107,67 @@ in
example = "https://myheadscale.example.com:443"; example = "https://myheadscale.example.com:443";
}; };
noise.private_key_path = mkOption { noise.private_key_path = lib.mkOption {
type = types.path; type = lib.types.path;
default = "${dataDir}/noise_private.key"; default = "${dataDir}/noise_private.key";
description = '' description = ''
Path to noise private key file, generated automatically if it does not exist. Path to noise private key file, generated automatically if it does not exist.
''; '';
}; };
prefixes = prefixes = let
let prefDesc = ''
prefDesc = '' Each prefix consists of either an IPv4 or IPv6 address,
Each prefix consists of either an IPv4 or IPv6 address, and the associated prefix length, delimited by a slash.
and the associated prefix length, delimited by a slash. It must be within IP ranges supported by the Tailscale
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.
client - i.e., subnets of 100.64.0.0/10 and fd7a:115c:a1e0::/48. '';
''; in {
in v4 = lib.mkOption {
{ type = lib.types.str;
v4 = mkOption { default = "100.64.0.0/10";
type = types.str; description = prefDesc;
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).
'';
};
}; };
v6 = lib.mkOption {
type = lib.types.str;
default = "fd7a:115c:a1e0::/48";
description = prefDesc;
};
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 = { derp = {
urls = mkOption { urls = lib.mkOption {
type = types.listOf types.str; type = lib.types.listOf lib.types.str;
default = [ "https://controlplane.tailscale.com/derpmap/default" ]; default = ["https://controlplane.tailscale.com/derpmap/default"];
description = '' description = ''
List of urls containing DERP maps. List of urls containing DERP maps.
See [How Tailscale works](https://tailscale.com/blog/how-tailscale-works/) for more information on DERP maps. See [How Tailscale works](https://tailscale.com/blog/how-tailscale-works/) for more information on DERP maps.
''; '';
}; };
paths = mkOption { paths = lib.mkOption {
type = types.listOf types.path; type = lib.types.listOf lib.types.path;
default = [ ]; default = [];
description = '' description = ''
List of file paths containing DERP maps. List of file paths containing DERP maps.
See [How Tailscale works](https://tailscale.com/blog/how-tailscale-works/) for more information on DERP maps. See [How Tailscale works](https://tailscale.com/blog/how-tailscale-works/) for more information on DERP maps.
''; '';
}; };
auto_update_enable = mkOption { auto_update_enable = lib.mkOption {
type = types.bool; type = lib.types.bool;
default = true; default = true;
description = '' description = ''
Whether to automatically update DERP maps on a set frequency. Whether to automatically update DERP maps on a set frequency.
@ -163,8 +175,8 @@ in
example = false; example = false;
}; };
update_frequency = mkOption { update_frequency = lib.mkOption {
type = types.str; type = lib.types.str;
default = "24h"; default = "24h";
description = '' description = ''
Frequency to update DERP maps. Frequency to update DERP maps.
@ -181,8 +193,8 @@ in
}; };
}; };
ephemeral_node_inactivity_timeout = mkOption { ephemeral_node_inactivity_timeout = lib.mkOption {
type = types.str; type = lib.types.str;
default = "30m"; default = "30m";
description = '' description = ''
Time before an inactive ephemeral node is deleted. Time before an inactive ephemeral node is deleted.
@ -191,8 +203,8 @@ in
}; };
database = { database = {
type = mkOption { type = lib.mkOption {
type = types.enum [ "sqlite" "sqlite3" "postgres" ]; type = lib.types.enum ["sqlite" "sqlite3" "postgres"];
example = "postgres"; example = "postgres";
default = "sqlite"; default = "sqlite";
description = '' description = ''
@ -203,14 +215,14 @@ in
}; };
sqlite = { sqlite = {
path = mkOption { path = lib.mkOption {
type = types.nullOr types.str; type = lib.types.nullOr lib.types.str;
default = "${dataDir}/db.sqlite"; default = "${dataDir}/db.sqlite";
description = "Path to the sqlite3 database file."; description = "Path to the sqlite3 database file.";
}; };
write_ahead_log = mkOption { write_ahead_log = lib.mkOption {
type = types.bool; type = lib.types.bool;
default = true; default = true;
description = '' description = ''
Enable WAL mode for SQLite. This is recommended for production environments. Enable WAL mode for SQLite. This is recommended for production environments.
@ -221,36 +233,36 @@ in
}; };
postgres = { postgres = {
host = mkOption { host = lib.mkOption {
type = types.nullOr types.str; type = lib.types.nullOr lib.types.str;
default = null; default = null;
example = "127.0.0.1"; example = "127.0.0.1";
description = "Database host address."; description = "Database host address.";
}; };
port = mkOption { port = lib.mkOption {
type = types.nullOr types.port; type = lib.types.nullOr lib.types.port;
default = null; default = null;
example = 3306; example = 3306;
description = "Database host port."; description = "Database host port.";
}; };
name = mkOption { name = lib.mkOption {
type = types.nullOr types.str; type = lib.types.nullOr lib.types.str;
default = null; default = null;
example = "headscale"; example = "headscale";
description = "Database name."; description = "Database name.";
}; };
user = mkOption { user = lib.mkOption {
type = types.nullOr types.str; type = lib.types.nullOr lib.types.str;
default = null; default = null;
example = "headscale"; example = "headscale";
description = "Database user."; description = "Database user.";
}; };
password_file = mkOption { password_file = lib.mkOption {
type = types.nullOr types.path; type = lib.types.nullOr lib.types.path;
default = null; default = null;
example = "/run/keys/headscale-dbpassword"; example = "/run/keys/headscale-dbpassword";
description = '' description = ''
@ -262,8 +274,8 @@ in
}; };
log = { log = {
level = mkOption { level = lib.mkOption {
type = types.str; type = lib.types.str;
default = "info"; default = "info";
description = '' description = ''
headscale log level. headscale log level.
@ -271,8 +283,8 @@ in
example = "debug"; example = "debug";
}; };
format = mkOption { format = lib.mkOption {
type = types.str; type = lib.types.str;
default = "text"; default = "text";
description = '' description = ''
headscale log format. headscale log format.
@ -282,8 +294,8 @@ in
}; };
dns = { dns = {
magic_dns = mkOption { magic_dns = lib.mkOption {
type = types.bool; type = lib.types.bool;
default = true; default = true;
description = '' description = ''
Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/). Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/).
@ -292,8 +304,8 @@ in
example = false; example = false;
}; };
base_domain = mkOption { base_domain = lib.mkOption {
type = types.str; type = lib.types.str;
default = ""; default = "";
description = '' description = ''
Defines the base domain to create the hostnames for MagicDNS. Defines the base domain to create the hostnames for MagicDNS.
@ -305,28 +317,28 @@ in
}; };
nameservers = { nameservers = {
global = mkOption { global = lib.mkOption {
type = types.listOf types.str; type = lib.types.listOf lib.types.str;
default = [ ]; default = [];
description = '' description = ''
List of nameservers to pass to Tailscale clients. List of nameservers to pass to Tailscale clients.
''; '';
}; };
}; };
search_domains = mkOption { search_domains = lib.mkOption {
type = types.listOf types.str; type = lib.types.listOf lib.types.str;
default = [ ]; default = [];
description = '' description = ''
Search domains to inject to Tailscale clients. Search domains to inject to Tailscale clients.
''; '';
example = [ "mydomain.internal" ]; example = ["mydomain.internal"];
}; };
}; };
oidc = { oidc = {
issuer = mkOption { issuer = lib.mkOption {
type = types.str; type = lib.types.str;
default = ""; default = "";
description = '' description = ''
URL to OpenID issuer. URL to OpenID issuer.
@ -334,33 +346,33 @@ in
example = "https://openid.example.com"; example = "https://openid.example.com";
}; };
client_id = mkOption { client_id = lib.mkOption {
type = types.str; type = lib.types.str;
default = ""; default = "";
description = '' description = ''
OpenID Connect client ID. OpenID Connect client ID.
''; '';
}; };
client_secret_path = mkOption { client_secret_path = lib.mkOption {
type = types.nullOr types.str; type = lib.types.nullOr lib.types.str;
default = null; default = null;
description = '' description = ''
Path to OpenID Connect client secret file. Expands environment variables in format ''${VAR}. Path to OpenID Connect client secret file. Expands environment variables in format ''${VAR}.
''; '';
}; };
scope = mkOption { scope = lib.mkOption {
type = types.listOf types.str; type = lib.types.listOf lib.types.str;
default = [ "openid" "profile" "email" ]; default = ["openid" "profile" "email"];
description = '' description = ''
Scopes used in the OIDC flow. Scopes used in the OIDC flow.
''; '';
}; };
extra_params = mkOption { extra_params = lib.mkOption {
type = types.attrsOf types.str; type = lib.types.attrsOf lib.types.str;
default = { }; default = {};
description = '' description = ''
Custom query parameters to send with the Authorize Endpoint request. Custom query parameters to send with the Authorize Endpoint request.
''; '';
@ -369,27 +381,27 @@ in
}; };
}; };
allowed_domains = mkOption { allowed_domains = lib.mkOption {
type = types.listOf types.str; type = lib.types.listOf lib.types.str;
default = [ ]; default = [];
description = '' description = ''
Allowed principal domains. if an authenticated user's domain Allowed principal domains. if an authenticated user's domain
is not in this list authentication request will be rejected. is not in this list authentication request will be rejected.
''; '';
example = [ "example.com" ]; example = ["example.com"];
}; };
allowed_users = mkOption { allowed_users = lib.mkOption {
type = types.listOf types.str; type = lib.types.listOf lib.types.str;
default = [ ]; default = [];
description = '' description = ''
Users allowed to authenticate even if not in allowedDomains. Users allowed to authenticate even if not in allowedDomains.
''; '';
example = [ "alice@example.com" ]; example = ["alice@example.com"];
}; };
strip_email_domain = mkOption { strip_email_domain = lib.mkOption {
type = types.bool; type = lib.types.bool;
default = true; default = true;
description = '' description = ''
Whether the domain part of the email address should be removed when generating namespaces. Whether the domain part of the email address should be removed when generating namespaces.
@ -397,16 +409,16 @@ in
}; };
}; };
tls_letsencrypt_hostname = mkOption { tls_letsencrypt_hostname = lib.mkOption {
type = types.nullOr types.str; type = lib.types.nullOr lib.types.str;
default = ""; default = "";
description = '' description = ''
Domain name to request a TLS certificate for. Domain name to request a TLS certificate for.
''; '';
}; };
tls_letsencrypt_challenge_type = mkOption { tls_letsencrypt_challenge_type = lib.mkOption {
type = types.enum [ "TLS-ALPN-01" "HTTP-01" ]; type = lib.types.enum ["TLS-ALPN-01" "HTTP-01"];
default = "HTTP-01"; default = "HTTP-01";
description = '' description = ''
Type of ACME challenge to use, currently supported types: Type of ACME challenge to use, currently supported types:
@ -414,8 +426,8 @@ in
''; '';
}; };
tls_letsencrypt_listen = mkOption { tls_letsencrypt_listen = lib.mkOption {
type = types.nullOr types.str; type = lib.types.nullOr lib.types.str;
default = ":http"; default = ":http";
description = '' description = ''
When HTTP-01 challenge is chosen, letsencrypt must set up a When HTTP-01 challenge is chosen, letsencrypt must set up a
@ -424,16 +436,16 @@ in
''; '';
}; };
tls_cert_path = mkOption { tls_cert_path = lib.mkOption {
type = types.nullOr types.path; type = lib.types.nullOr lib.types.path;
default = null; default = null;
description = '' description = ''
Path to already created certificate. Path to already created certificate.
''; '';
}; };
tls_key_path = mkOption { tls_key_path = lib.mkOption {
type = types.nullOr types.path; type = lib.types.nullOr lib.types.path;
default = null; default = null;
description = '' description = ''
Path to key for already created certificate. Path to key for already created certificate.
@ -441,8 +453,8 @@ in
}; };
policy = { policy = {
mode = mkOption { mode = lib.mkOption {
type = types.enum [ "file" "database" ]; type = lib.types.enum ["file" "database"];
default = "file"; default = "file";
description = '' description = ''
The mode can be "file" or "database" that defines The mode can be "file" or "database" that defines
@ -450,8 +462,8 @@ in
''; '';
}; };
path = mkOption { path = lib.mkOption {
type = types.nullOr types.path; type = lib.types.nullOr lib.types.path;
default = null; default = null;
description = '' description = ''
If the mode is set to "file", the path to a If the mode is set to "file", the path to a
@ -465,50 +477,33 @@ in
}; };
}; };
imports = [ imports = with lib; [
(mkRenamedOptionModule [ "services" "headscale" "serverUrl" ] [ "services" "headscale" "settings" "server_url" ]) (mkRenamedOptionModule ["services" "headscale" "derp" "autoUpdate"] ["services" "headscale" "settings" "derp" "auto_update_enable"])
(mkRenamedOptionModule [ "services" "headscale" "derp" "urls" ] [ "services" "headscale" "settings" "derp" "urls" ]) (mkRenamedOptionModule ["services" "headscale" "derp" "paths"] ["services" "headscale" "settings" "derp" "paths"])
(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" "autoUpdate" ] [ "services" "headscale" "settings" "derp" "auto_update_enable" ]) (mkRenamedOptionModule ["services" "headscale" "derp" "urls"] ["services" "headscale" "settings" "derp" "urls"])
(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" "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"])
# (mkRenamedOptionModule ["services" "headscale" "settings" "db_type"] ["services" "headscale" "settings" "database" "type"]) (mkRemovedOptionModule ["services" "headscale" "openIdConnect" "domainMap"] ''
# (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. 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 { config = lib.mkIf cfg.enable {
services.headscale.settings = mkMerge [ services.headscale.settings = lib.mkMerge [
cliConfig cliConfig
{ {
listen_addr = mkDefault "${cfg.address}:${toString cfg.port}"; listen_addr = lib.mkDefault "${cfg.address}:${toString cfg.port}";
tls_letsencrypt_cache_dir = "${dataDir}/.cache"; tls_letsencrypt_cache_dir = "${dataDir}/.cache";
} }
@ -519,12 +514,12 @@ in
# to talk to the server instance. # to talk to the server instance.
etc."headscale/config.yaml".source = cliConfigFile; 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"; description = "headscale user";
home = dataDir; home = dataDir;
group = cfg.group; group = cfg.group;
@ -533,65 +528,63 @@ in
systemd.services.headscale = { systemd.services.headscale = {
description = "headscale coordination server for Tailscale"; description = "headscale coordination server for Tailscale";
wants = [ "network-online.target" ]; wants = ["network-online.target"];
after = [ "network-online.target" ]; after = ["network-online.target"];
wantedBy = [ "multi-user.target" ]; wantedBy = ["multi-user.target"];
script = '' script = ''
${optionalString (cfg.settings.database.postgres.password_file != null) '' ${lib.optionalString (cfg.settings.database.postgres.password_file != null) ''
export HEADSCALE_DATABASE_POSTGRES_PASS="$(head -n1 ${escapeShellArg cfg.settings.database.postgres.password_file})" export HEADSCALE_DATABASE_POSTGRES_PASS="$(head -n1 ${lib.escapeShellArg cfg.settings.database.postgres.password_file})"
''} ''}
exec ${lib.getExe cfg.package} serve --config ${configFile} exec ${lib.getExe cfg.package} serve --config ${configFile}
''; '';
serviceConfig = serviceConfig = let
let capabilityBoundingSet = ["CAP_CHOWN"] ++ lib.optional (cfg.port < 1024) "CAP_NET_BIND_SERVICE";
capabilityBoundingSet = [ "CAP_CHOWN" ] ++ optional (cfg.port < 1024) "CAP_NET_BIND_SERVICE"; in {
in Restart = "always";
{ Type = "simple";
Restart = "always"; User = cfg.user;
Type = "simple"; Group = cfg.group;
User = cfg.user;
Group = cfg.group;
# Hardening options # Hardening options
RuntimeDirectory = "headscale"; RuntimeDirectory = "headscale";
# Allow headscale group access so users can be added and use the CLI. # Allow headscale group access so users can be added and use the CLI.
RuntimeDirectoryMode = "0750"; RuntimeDirectoryMode = "0750";
StateDirectory = "headscale"; StateDirectory = "headscale";
StateDirectoryMode = "0750"; StateDirectoryMode = "0750";
ProtectSystem = "strict"; ProtectSystem = "strict";
ProtectHome = true; ProtectHome = true;
PrivateTmp = true; PrivateTmp = true;
PrivateDevices = true; PrivateDevices = true;
ProtectKernelTunables = true; ProtectKernelTunables = true;
ProtectControlGroups = true; ProtectControlGroups = true;
RestrictSUIDSGID = true; RestrictSUIDSGID = true;
PrivateMounts = true; PrivateMounts = true;
ProtectKernelModules = true; ProtectKernelModules = true;
ProtectKernelLogs = true; ProtectKernelLogs = true;
ProtectHostname = true; ProtectHostname = true;
ProtectClock = true; ProtectClock = true;
ProtectProc = "invisible"; ProtectProc = "invisible";
ProcSubset = "pid"; ProcSubset = "pid";
RestrictNamespaces = true; RestrictNamespaces = true;
RemoveIPC = true; RemoveIPC = true;
UMask = "0077"; UMask = "0077";
CapabilityBoundingSet = capabilityBoundingSet; CapabilityBoundingSet = capabilityBoundingSet;
AmbientCapabilities = capabilityBoundingSet; AmbientCapabilities = capabilityBoundingSet;
NoNewPrivileges = true; NoNewPrivileges = true;
LockPersonality = true; LockPersonality = true;
RestrictRealtime = true; RestrictRealtime = true;
SystemCallFilter = [ "@system-service" "~@privileged" "@chown" ]; SystemCallFilter = ["@system-service" "~@privileged" "@chown"];
SystemCallArchitectures = "native"; SystemCallArchitectures = "native";
RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX"; RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX";
}; };
}; };
}; };
meta.maintainers = with maintainers; [ kradalby misterio77 ]; meta.maintainers = with lib.maintainers; [kradalby misterio77];
} }