7 cfg = config.services.headscale;
9 dataDir = "/var/lib/headscale";
10 runDir = "/run/headscale";
13 # Turn off update checks since the origin of our package
14 # is nixpkgs and not Github.
15 disable_check_updates = true;
17 unix_socket = "${runDir}/headscale.sock";
20 settingsFormat = pkgs.formats.yaml {};
21 configFile = settingsFormat.generate "headscale.yaml" cfg.settings;
22 cliConfigFile = settingsFormat.generate "headscale.yaml" cliConfig;
25 services.headscale = {
26 enable = lib.mkEnableOption "headscale, Open Source coordination server for Tailscale";
28 package = lib.mkPackageOption pkgs "headscale" {};
31 default = "headscale";
34 User account under which headscale runs.
37 If left as the default value this user will automatically be created
38 on system activation, otherwise you are responsible for
39 ensuring the user exists before the headscale service starts.
44 group = lib.mkOption {
45 default = "headscale";
48 Group under which headscale runs.
51 If left as the default value this group will automatically be created
52 on system activation, otherwise you are responsible for
53 ensuring the user exists before the headscale service starts.
58 address = lib.mkOption {
60 default = "127.0.0.1";
62 Listening address of headscale.
68 type = lib.types.port;
71 Listening port of headscale.
76 settings = lib.mkOption {
78 Overrides to {file}`config.yaml` as a Nix attribute set.
79 Check the [example config](https://github.com/juanfont/headscale/blob/main/config-example.yaml)
82 type = lib.types.submodule {
83 freeformType = settingsFormat.type;
86 (mkAliasOptionModule ["acl_policy_path"] ["policy" "path"])
87 (mkAliasOptionModule ["db_host"] ["database" "postgres" "host"])
88 (mkAliasOptionModule ["db_name"] ["database" "postgres" "name"])
89 (mkAliasOptionModule ["db_password_file"] ["database" "postgres" "password_file"])
90 (mkAliasOptionModule ["db_path"] ["database" "sqlite" "path"])
91 (mkAliasOptionModule ["db_port"] ["database" "postgres" "port"])
92 (mkAliasOptionModule ["db_type"] ["database" "type"])
93 (mkAliasOptionModule ["db_user"] ["database" "postgres" "user"])
94 (mkAliasOptionModule ["dns_config" "base_domain"] ["dns" "base_domain"])
95 (mkAliasOptionModule ["dns_config" "domains"] ["dns" "search_domains"])
96 (mkAliasOptionModule ["dns_config" "magic_dns"] ["dns" "magic_dns"])
97 (mkAliasOptionModule ["dns_config" "nameservers"] ["dns" "nameservers" "global"])
101 server_url = lib.mkOption {
102 type = lib.types.str;
103 default = "http://127.0.0.1:8080";
105 The url clients will connect to.
107 example = "https://myheadscale.example.com:443";
110 noise.private_key_path = lib.mkOption {
111 type = lib.types.path;
112 default = "${dataDir}/noise_private.key";
114 Path to noise private key file, generated automatically if it does not exist.
120 Each prefix consists of either an IPv4 or IPv6 address,
121 and the associated prefix length, delimited by a slash.
122 It must be within IP ranges supported by the Tailscale
123 client - i.e., subnets of 100.64.0.0/10 and fd7a:115c:a1e0::/48.
127 type = lib.types.str;
128 default = "100.64.0.0/10";
129 description = prefDesc;
133 type = lib.types.str;
134 default = "fd7a:115c:a1e0::/48";
135 description = prefDesc;
138 allocation = lib.mkOption {
139 type = lib.types.enum ["sequential" "random"];
141 default = "sequential";
143 Strategy used for allocation of IPs to nodes, available options:
144 - sequential (default): assigns the next free IP from the previous given IP.
145 - random: assigns the next free IP from a pseudo-random IP generator (crypto/rand).
151 urls = lib.mkOption {
152 type = lib.types.listOf lib.types.str;
153 default = ["https://controlplane.tailscale.com/derpmap/default"];
155 List of urls containing DERP maps.
156 See [How Tailscale works](https://tailscale.com/blog/how-tailscale-works/) for more information on DERP maps.
160 paths = lib.mkOption {
161 type = lib.types.listOf lib.types.path;
164 List of file paths containing DERP maps.
165 See [How Tailscale works](https://tailscale.com/blog/how-tailscale-works/) for more information on DERP maps.
169 auto_update_enable = lib.mkOption {
170 type = lib.types.bool;
173 Whether to automatically update DERP maps on a set frequency.
178 update_frequency = lib.mkOption {
179 type = lib.types.str;
182 Frequency to update DERP maps.
187 server.private_key_path = lib.mkOption {
188 type = lib.types.path;
189 default = "${dataDir}/derp_server_private.key";
191 Path to derp private key file, generated automatically if it does not exist.
196 ephemeral_node_inactivity_timeout = lib.mkOption {
197 type = lib.types.str;
200 Time before an inactive ephemeral node is deleted.
206 type = lib.mkOption {
207 type = lib.types.enum ["sqlite" "sqlite3" "postgres"];
208 example = "postgres";
211 Database engine to use.
212 Please note that using Postgres is highly discouraged as it is only supported for legacy reasons.
213 All new development, testing and optimisations are done with SQLite in mind.
218 path = lib.mkOption {
219 type = lib.types.nullOr lib.types.str;
220 default = "${dataDir}/db.sqlite";
221 description = "Path to the sqlite3 database file.";
224 write_ahead_log = lib.mkOption {
225 type = lib.types.bool;
228 Enable WAL mode for SQLite. This is recommended for production environments.
229 https://www.sqlite.org/wal.html
236 host = lib.mkOption {
237 type = lib.types.nullOr lib.types.str;
239 example = "127.0.0.1";
240 description = "Database host address.";
243 port = lib.mkOption {
244 type = lib.types.nullOr lib.types.port;
247 description = "Database host port.";
250 name = lib.mkOption {
251 type = lib.types.nullOr lib.types.str;
253 example = "headscale";
254 description = "Database name.";
257 user = lib.mkOption {
258 type = lib.types.nullOr lib.types.str;
260 example = "headscale";
261 description = "Database user.";
264 password_file = lib.mkOption {
265 type = lib.types.nullOr lib.types.path;
267 example = "/run/keys/headscale-dbpassword";
269 A file containing the password corresponding to
270 {option}`database.user`.
277 level = lib.mkOption {
278 type = lib.types.str;
286 format = lib.mkOption {
287 type = lib.types.str;
290 headscale log format.
297 magic_dns = lib.mkOption {
298 type = lib.types.bool;
301 Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/).
302 Only works if there is at least a nameserver defined.
307 base_domain = lib.mkOption {
308 type = lib.types.str;
311 Defines the base domain to create the hostnames for MagicDNS.
312 {option}`baseDomain` must be a FQDNs, without the trailing dot.
313 The FQDN of the hosts will be
314 `hostname.namespace.base_domain` (e.g.
315 `myhost.mynamespace.example.com`).
320 global = lib.mkOption {
321 type = lib.types.listOf lib.types.str;
324 List of nameservers to pass to Tailscale clients.
329 search_domains = lib.mkOption {
330 type = lib.types.listOf lib.types.str;
333 Search domains to inject to Tailscale clients.
335 example = ["mydomain.internal"];
340 issuer = lib.mkOption {
341 type = lib.types.str;
344 URL to OpenID issuer.
346 example = "https://openid.example.com";
349 client_id = lib.mkOption {
350 type = lib.types.str;
353 OpenID Connect client ID.
357 client_secret_path = lib.mkOption {
358 type = lib.types.nullOr lib.types.str;
361 Path to OpenID Connect client secret file. Expands environment variables in format ''${VAR}.
365 scope = lib.mkOption {
366 type = lib.types.listOf lib.types.str;
367 default = ["openid" "profile" "email"];
369 Scopes used in the OIDC flow.
373 extra_params = lib.mkOption {
374 type = lib.types.attrsOf lib.types.str;
377 Custom query parameters to send with the Authorize Endpoint request.
380 domain_hint = "example.com";
384 allowed_domains = lib.mkOption {
385 type = lib.types.listOf lib.types.str;
388 Allowed principal domains. if an authenticated user's domain
389 is not in this list authentication request will be rejected.
391 example = ["example.com"];
394 allowed_users = lib.mkOption {
395 type = lib.types.listOf lib.types.str;
398 Users allowed to authenticate even if not in allowedDomains.
400 example = ["alice@example.com"];
403 strip_email_domain = lib.mkOption {
404 type = lib.types.bool;
407 Whether the domain part of the email address should be removed when generating namespaces.
412 tls_letsencrypt_hostname = lib.mkOption {
413 type = lib.types.nullOr lib.types.str;
416 Domain name to request a TLS certificate for.
420 tls_letsencrypt_challenge_type = lib.mkOption {
421 type = lib.types.enum ["TLS-ALPN-01" "HTTP-01"];
424 Type of ACME challenge to use, currently supported types:
425 `HTTP-01` or `TLS-ALPN-01`.
429 tls_letsencrypt_listen = lib.mkOption {
430 type = lib.types.nullOr lib.types.str;
433 When HTTP-01 challenge is chosen, letsencrypt must set up a
434 verification endpoint, and it will be listening on:
439 tls_cert_path = lib.mkOption {
440 type = lib.types.nullOr lib.types.path;
443 Path to already created certificate.
447 tls_key_path = lib.mkOption {
448 type = lib.types.nullOr lib.types.path;
451 Path to key for already created certificate.
456 mode = lib.mkOption {
457 type = lib.types.enum ["file" "database"];
460 The mode can be "file" or "database" that defines
461 where the ACL policies are stored and read from.
465 path = lib.mkOption {
466 type = lib.types.nullOr lib.types.path;
469 If the mode is set to "file", the path to a
470 HuJSON file containing ACL policies.
480 imports = with lib; [
481 (mkRenamedOptionModule ["services" "headscale" "derp" "autoUpdate"] ["services" "headscale" "settings" "derp" "auto_update_enable"])
482 (mkRenamedOptionModule ["services" "headscale" "derp" "paths"] ["services" "headscale" "settings" "derp" "paths"])
483 (mkRenamedOptionModule ["services" "headscale" "derp" "updateFrequency"] ["services" "headscale" "settings" "derp" "update_frequency"])
484 (mkRenamedOptionModule ["services" "headscale" "derp" "urls"] ["services" "headscale" "settings" "derp" "urls"])
485 (mkRenamedOptionModule ["services" "headscale" "ephemeralNodeInactivityTimeout"] ["services" "headscale" "settings" "ephemeral_node_inactivity_timeout"])
486 (mkRenamedOptionModule ["services" "headscale" "logLevel"] ["services" "headscale" "settings" "log" "level"])
487 (mkRenamedOptionModule ["services" "headscale" "openIdConnect" "clientId"] ["services" "headscale" "settings" "oidc" "client_id"])
488 (mkRenamedOptionModule ["services" "headscale" "openIdConnect" "clientSecretFile"] ["services" "headscale" "settings" "oidc" "client_secret_path"])
489 (mkRenamedOptionModule ["services" "headscale" "openIdConnect" "issuer"] ["services" "headscale" "settings" "oidc" "issuer"])
490 (mkRenamedOptionModule ["services" "headscale" "serverUrl"] ["services" "headscale" "settings" "server_url"])
491 (mkRenamedOptionModule ["services" "headscale" "tls" "certFile"] ["services" "headscale" "settings" "tls_cert_path"])
492 (mkRenamedOptionModule ["services" "headscale" "tls" "keyFile"] ["services" "headscale" "settings" "tls_key_path"])
493 (mkRenamedOptionModule ["services" "headscale" "tls" "letsencrypt" "challengeType"] ["services" "headscale" "settings" "tls_letsencrypt_challenge_type"])
494 (mkRenamedOptionModule ["services" "headscale" "tls" "letsencrypt" "hostname"] ["services" "headscale" "settings" "tls_letsencrypt_hostname"])
495 (mkRenamedOptionModule ["services" "headscale" "tls" "letsencrypt" "httpListen"] ["services" "headscale" "settings" "tls_letsencrypt_listen"])
497 (mkRemovedOptionModule ["services" "headscale" "openIdConnect" "domainMap"] ''
498 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.
502 config = lib.mkIf cfg.enable {
503 services.headscale.settings = lib.mkMerge [
506 listen_addr = lib.mkDefault "${cfg.address}:${toString cfg.port}";
508 tls_letsencrypt_cache_dir = "${dataDir}/.cache";
513 # Headscale CLI needs a minimal config to be able to locate the unix socket
514 # to talk to the server instance.
515 etc."headscale/config.yaml".source = cliConfigFile;
517 systemPackages = [cfg.package];
520 users.groups.headscale = lib.mkIf (cfg.group == "headscale") {};
522 users.users.headscale = lib.mkIf (cfg.user == "headscale") {
523 description = "headscale user";
529 systemd.services.headscale = {
530 description = "headscale coordination server for Tailscale";
531 wants = ["network-online.target"];
532 after = ["network-online.target"];
533 wantedBy = ["multi-user.target"];
536 ${lib.optionalString (cfg.settings.database.postgres.password_file != null) ''
537 export HEADSCALE_DATABASE_POSTGRES_PASS="$(head -n1 ${lib.escapeShellArg cfg.settings.database.postgres.password_file})"
540 exec ${lib.getExe cfg.package} serve --config ${configFile}
544 capabilityBoundingSet = ["CAP_CHOWN"] ++ lib.optional (cfg.port < 1024) "CAP_NET_BIND_SERVICE";
552 RuntimeDirectory = "headscale";
553 # Allow headscale group access so users can be added and use the CLI.
554 RuntimeDirectoryMode = "0750";
556 StateDirectory = "headscale";
557 StateDirectoryMode = "0750";
559 ProtectSystem = "strict";
562 PrivateDevices = true;
563 ProtectKernelTunables = true;
564 ProtectControlGroups = true;
565 RestrictSUIDSGID = true;
566 PrivateMounts = true;
567 ProtectKernelModules = true;
568 ProtectKernelLogs = true;
569 ProtectHostname = true;
571 ProtectProc = "invisible";
573 RestrictNamespaces = true;
577 CapabilityBoundingSet = capabilityBoundingSet;
578 AmbientCapabilities = capabilityBoundingSet;
579 NoNewPrivileges = true;
580 LockPersonality = true;
581 RestrictRealtime = true;
582 SystemCallFilter = ["@system-service" "~@privileged" "@chown"];
583 SystemCallArchitectures = "native";
584 RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX";
589 meta.maintainers = with lib.maintainers; [kradalby misterio77];