terraform-providers.sumologic: 2.31.5 -> 3.0.0 (#365043)
[NixPkgs.git] / nixos / modules / services / networking / headscale.nix
blob495dc6650e60b53835aa84490f80fad652e8fc50
2   config,
3   lib,
4   pkgs,
5   ...
6 }:
7 let
8   cfg = config.services.headscale;
10   dataDir = "/var/lib/headscale";
11   runDir = "/run/headscale";
13   cliConfig = {
14     # Turn off update checks since the origin of our package
15     # is nixpkgs and not Github.
16     disable_check_updates = true;
18     unix_socket = "${runDir}/headscale.sock";
19   };
21   settingsFormat = pkgs.formats.yaml { };
22   configFile = settingsFormat.generate "headscale.yaml" cfg.settings;
23   cliConfigFile = settingsFormat.generate "headscale.yaml" cliConfig;
25   assertRemovedOption = option: message: {
26     assertion = !lib.hasAttrByPath option cfg;
27     message =
28       "The option `services.headscale.${lib.options.showOption option}` was removed. " + message;
29   };
32   options = {
33     services.headscale = {
34       enable = lib.mkEnableOption "headscale, Open Source coordination server for Tailscale";
36       package = lib.mkPackageOption pkgs "headscale" { };
38       user = lib.mkOption {
39         default = "headscale";
40         type = lib.types.str;
41         description = ''
42           User account under which headscale runs.
44           ::: {.note}
45           If left as the default value this user will automatically be created
46           on system activation, otherwise you are responsible for
47           ensuring the user exists before the headscale service starts.
48           :::
49         '';
50       };
52       group = lib.mkOption {
53         default = "headscale";
54         type = lib.types.str;
55         description = ''
56           Group under which headscale runs.
58           ::: {.note}
59           If left as the default value this group will automatically be created
60           on system activation, otherwise you are responsible for
61           ensuring the user exists before the headscale service starts.
62           :::
63         '';
64       };
66       address = lib.mkOption {
67         type = lib.types.str;
68         default = "127.0.0.1";
69         description = ''
70           Listening address of headscale.
71         '';
72         example = "0.0.0.0";
73       };
75       port = lib.mkOption {
76         type = lib.types.port;
77         default = 8080;
78         description = ''
79           Listening port of headscale.
80         '';
81         example = 443;
82       };
84       settings = lib.mkOption {
85         description = ''
86           Overrides to {file}`config.yaml` as a Nix attribute set.
87           Check the [example config](https://github.com/juanfont/headscale/blob/main/config-example.yaml)
88           for possible options.
89         '';
90         type = lib.types.submodule {
91           freeformType = settingsFormat.type;
93           options = {
94             server_url = lib.mkOption {
95               type = lib.types.str;
96               default = "http://127.0.0.1:8080";
97               description = ''
98                 The url clients will connect to.
99               '';
100               example = "https://myheadscale.example.com:443";
101             };
103             noise.private_key_path = lib.mkOption {
104               type = lib.types.path;
105               default = "${dataDir}/noise_private.key";
106               description = ''
107                 Path to noise private key file, generated automatically if it does not exist.
108               '';
109             };
111             prefixes =
112               let
113                 prefDesc = ''
114                   Each prefix consists of either an IPv4 or IPv6 address,
115                   and the associated prefix length, delimited by a slash.
116                   It must be within IP ranges supported by the Tailscale
117                   client - i.e., subnets of 100.64.0.0/10 and fd7a:115c:a1e0::/48.
118                 '';
119               in
120               {
121                 v4 = lib.mkOption {
122                   type = lib.types.str;
123                   default = "100.64.0.0/10";
124                   description = prefDesc;
125                 };
127                 v6 = lib.mkOption {
128                   type = lib.types.str;
129                   default = "fd7a:115c:a1e0::/48";
130                   description = prefDesc;
131                 };
133                 allocation = lib.mkOption {
134                   type = lib.types.enum [
135                     "sequential"
136                     "random"
137                   ];
138                   example = "random";
139                   default = "sequential";
140                   description = ''
141                     Strategy used for allocation of IPs to nodes, available options:
142                     - sequential (default): assigns the next free IP from the previous given IP.
143                     - random: assigns the next free IP from a pseudo-random IP generator (crypto/rand).
144                   '';
145                 };
146               };
148             derp = {
149               urls = lib.mkOption {
150                 type = lib.types.listOf lib.types.str;
151                 default = [ "https://controlplane.tailscale.com/derpmap/default" ];
152                 description = ''
153                   List of urls containing DERP maps.
154                   See [How Tailscale works](https://tailscale.com/blog/how-tailscale-works/) for more information on DERP maps.
155                 '';
156               };
158               paths = lib.mkOption {
159                 type = lib.types.listOf lib.types.path;
160                 default = [ ];
161                 description = ''
162                   List of file paths containing DERP maps.
163                   See [How Tailscale works](https://tailscale.com/blog/how-tailscale-works/) for more information on DERP maps.
164                 '';
165               };
167               auto_update_enable = lib.mkOption {
168                 type = lib.types.bool;
169                 default = true;
170                 description = ''
171                   Whether to automatically update DERP maps on a set frequency.
172                 '';
173                 example = false;
174               };
176               update_frequency = lib.mkOption {
177                 type = lib.types.str;
178                 default = "24h";
179                 description = ''
180                   Frequency to update DERP maps.
181                 '';
182                 example = "5m";
183               };
185               server.private_key_path = lib.mkOption {
186                 type = lib.types.path;
187                 default = "${dataDir}/derp_server_private.key";
188                 description = ''
189                   Path to derp private key file, generated automatically if it does not exist.
190                 '';
191               };
192             };
194             ephemeral_node_inactivity_timeout = lib.mkOption {
195               type = lib.types.str;
196               default = "30m";
197               description = ''
198                 Time before an inactive ephemeral node is deleted.
199               '';
200               example = "5m";
201             };
203             database = {
204               type = lib.mkOption {
205                 type = lib.types.enum [
206                   "sqlite"
207                   "sqlite3"
208                   "postgres"
209                 ];
210                 example = "postgres";
211                 default = "sqlite";
212                 description = ''
213                   Database engine to use.
214                   Please note that using Postgres is highly discouraged as it is only supported for legacy reasons.
215                   All new development, testing and optimisations are done with SQLite in mind.
216                 '';
217               };
219               sqlite = {
220                 path = lib.mkOption {
221                   type = lib.types.nullOr lib.types.str;
222                   default = "${dataDir}/db.sqlite";
223                   description = "Path to the sqlite3 database file.";
224                 };
226                 write_ahead_log = lib.mkOption {
227                   type = lib.types.bool;
228                   default = true;
229                   description = ''
230                     Enable WAL mode for SQLite. This is recommended for production environments.
231                     https://www.sqlite.org/wal.html
232                   '';
233                   example = true;
234                 };
235               };
237               postgres = {
238                 host = lib.mkOption {
239                   type = lib.types.nullOr lib.types.str;
240                   default = null;
241                   example = "127.0.0.1";
242                   description = "Database host address.";
243                 };
245                 port = lib.mkOption {
246                   type = lib.types.nullOr lib.types.port;
247                   default = null;
248                   example = 3306;
249                   description = "Database host port.";
250                 };
252                 name = lib.mkOption {
253                   type = lib.types.nullOr lib.types.str;
254                   default = null;
255                   example = "headscale";
256                   description = "Database name.";
257                 };
259                 user = lib.mkOption {
260                   type = lib.types.nullOr lib.types.str;
261                   default = null;
262                   example = "headscale";
263                   description = "Database user.";
264                 };
266                 password_file = lib.mkOption {
267                   type = lib.types.nullOr lib.types.path;
268                   default = null;
269                   example = "/run/keys/headscale-dbpassword";
270                   description = ''
271                     A file containing the password corresponding to
272                     {option}`database.user`.
273                   '';
274                 };
275               };
276             };
278             log = {
279               level = lib.mkOption {
280                 type = lib.types.str;
281                 default = "info";
282                 description = ''
283                   headscale log level.
284                 '';
285                 example = "debug";
286               };
288               format = lib.mkOption {
289                 type = lib.types.str;
290                 default = "text";
291                 description = ''
292                   headscale log format.
293                 '';
294                 example = "json";
295               };
296             };
298             dns = {
299               magic_dns = lib.mkOption {
300                 type = lib.types.bool;
301                 default = true;
302                 description = ''
303                   Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/).
304                 '';
305                 example = false;
306               };
308               base_domain = lib.mkOption {
309                 type = lib.types.str;
310                 default = "";
311                 description = ''
312                   Defines the base domain to create the hostnames for MagicDNS.
313                   This domain must be different from the {option}`server_url`
314                   domain.
315                   {option}`base_domain` must be a FQDN, without the trailing dot.
316                   The FQDN of the hosts will be `hostname.base_domain` (e.g.
317                   `myhost.tailnet.example.com`).
318                 '';
319                 example = "tailnet.example.com";
320               };
322               nameservers = {
323                 global = lib.mkOption {
324                   type = lib.types.listOf lib.types.str;
325                   default = [ ];
326                   description = ''
327                     List of nameservers to pass to Tailscale clients.
328                   '';
329                 };
330               };
332               search_domains = lib.mkOption {
333                 type = lib.types.listOf lib.types.str;
334                 default = [ ];
335                 description = ''
336                   Search domains to inject to Tailscale clients.
337                 '';
338                 example = [ "mydomain.internal" ];
339               };
340             };
342             oidc = {
343               issuer = lib.mkOption {
344                 type = lib.types.str;
345                 default = "";
346                 description = ''
347                   URL to OpenID issuer.
348                 '';
349                 example = "https://openid.example.com";
350               };
352               client_id = lib.mkOption {
353                 type = lib.types.str;
354                 default = "";
355                 description = ''
356                   OpenID Connect client ID.
357                 '';
358               };
360               client_secret_path = lib.mkOption {
361                 type = lib.types.nullOr lib.types.str;
362                 default = null;
363                 description = ''
364                   Path to OpenID Connect client secret file. Expands environment variables in format ''${VAR}.
365                 '';
366               };
368               scope = lib.mkOption {
369                 type = lib.types.listOf lib.types.str;
370                 default = [
371                   "openid"
372                   "profile"
373                   "email"
374                 ];
375                 description = ''
376                   Scopes used in the OIDC flow.
377                 '';
378               };
380               extra_params = lib.mkOption {
381                 type = lib.types.attrsOf lib.types.str;
382                 default = { };
383                 description = ''
384                   Custom query parameters to send with the Authorize Endpoint request.
385                 '';
386                 example = {
387                   domain_hint = "example.com";
388                 };
389               };
391               allowed_domains = lib.mkOption {
392                 type = lib.types.listOf lib.types.str;
393                 default = [ ];
394                 description = ''
395                   Allowed principal domains. if an authenticated user's domain
396                   is not in this list authentication request will be rejected.
397                 '';
398                 example = [ "example.com" ];
399               };
401               allowed_users = lib.mkOption {
402                 type = lib.types.listOf lib.types.str;
403                 default = [ ];
404                 description = ''
405                   Users allowed to authenticate even if not in allowedDomains.
406                 '';
407                 example = [ "alice@example.com" ];
408               };
410               strip_email_domain = lib.mkOption {
411                 type = lib.types.bool;
412                 default = true;
413                 description = ''
414                   Whether the domain part of the email address should be removed when generating namespaces.
415                 '';
416               };
417             };
419             tls_letsencrypt_hostname = lib.mkOption {
420               type = lib.types.nullOr lib.types.str;
421               default = "";
422               description = ''
423                 Domain name to request a TLS certificate for.
424               '';
425             };
427             tls_letsencrypt_challenge_type = lib.mkOption {
428               type = lib.types.enum [
429                 "TLS-ALPN-01"
430                 "HTTP-01"
431               ];
432               default = "HTTP-01";
433               description = ''
434                 Type of ACME challenge to use, currently supported types:
435                 `HTTP-01` or `TLS-ALPN-01`.
436               '';
437             };
439             tls_letsencrypt_listen = lib.mkOption {
440               type = lib.types.nullOr lib.types.str;
441               default = ":http";
442               description = ''
443                 When HTTP-01 challenge is chosen, letsencrypt must set up a
444                 verification endpoint, and it will be listening on:
445                 `:http = port 80`.
446               '';
447             };
449             tls_cert_path = lib.mkOption {
450               type = lib.types.nullOr lib.types.path;
451               default = null;
452               description = ''
453                 Path to already created certificate.
454               '';
455             };
457             tls_key_path = lib.mkOption {
458               type = lib.types.nullOr lib.types.path;
459               default = null;
460               description = ''
461                 Path to key for already created certificate.
462               '';
463             };
465             policy = {
466               mode = lib.mkOption {
467                 type = lib.types.enum [
468                   "file"
469                   "database"
470                 ];
471                 default = "file";
472                 description = ''
473                   The mode can be "file" or "database" that defines
474                   where the ACL policies are stored and read from.
475                 '';
476               };
478               path = lib.mkOption {
479                 type = lib.types.nullOr lib.types.path;
480                 default = null;
481                 description = ''
482                   If the mode is set to "file", the path to a
483                   HuJSON file containing ACL policies.
484                 '';
485               };
486             };
487           };
488         };
489       };
490     };
491   };
493   imports = with lib; [
494     (mkRenamedOptionModule
495       [ "services" "headscale" "derp" "autoUpdate" ]
496       [ "services" "headscale" "settings" "derp" "auto_update_enable" ]
497     )
498     (mkRenamedOptionModule
499       [ "services" "headscale" "derp" "paths" ]
500       [ "services" "headscale" "settings" "derp" "paths" ]
501     )
502     (mkRenamedOptionModule
503       [ "services" "headscale" "derp" "updateFrequency" ]
504       [ "services" "headscale" "settings" "derp" "update_frequency" ]
505     )
506     (mkRenamedOptionModule
507       [ "services" "headscale" "derp" "urls" ]
508       [ "services" "headscale" "settings" "derp" "urls" ]
509     )
510     (mkRenamedOptionModule
511       [ "services" "headscale" "ephemeralNodeInactivityTimeout" ]
512       [ "services" "headscale" "settings" "ephemeral_node_inactivity_timeout" ]
513     )
514     (mkRenamedOptionModule
515       [ "services" "headscale" "logLevel" ]
516       [ "services" "headscale" "settings" "log" "level" ]
517     )
518     (mkRenamedOptionModule
519       [ "services" "headscale" "openIdConnect" "clientId" ]
520       [ "services" "headscale" "settings" "oidc" "client_id" ]
521     )
522     (mkRenamedOptionModule
523       [ "services" "headscale" "openIdConnect" "clientSecretFile" ]
524       [ "services" "headscale" "settings" "oidc" "client_secret_path" ]
525     )
526     (mkRenamedOptionModule
527       [ "services" "headscale" "openIdConnect" "issuer" ]
528       [ "services" "headscale" "settings" "oidc" "issuer" ]
529     )
530     (mkRenamedOptionModule
531       [ "services" "headscale" "serverUrl" ]
532       [ "services" "headscale" "settings" "server_url" ]
533     )
534     (mkRenamedOptionModule
535       [ "services" "headscale" "tls" "certFile" ]
536       [ "services" "headscale" "settings" "tls_cert_path" ]
537     )
538     (mkRenamedOptionModule
539       [ "services" "headscale" "tls" "keyFile" ]
540       [ "services" "headscale" "settings" "tls_key_path" ]
541     )
542     (mkRenamedOptionModule
543       [ "services" "headscale" "tls" "letsencrypt" "challengeType" ]
544       [ "services" "headscale" "settings" "tls_letsencrypt_challenge_type" ]
545     )
546     (mkRenamedOptionModule
547       [ "services" "headscale" "tls" "letsencrypt" "hostname" ]
548       [ "services" "headscale" "settings" "tls_letsencrypt_hostname" ]
549     )
550     (mkRenamedOptionModule
551       [ "services" "headscale" "tls" "letsencrypt" "httpListen" ]
552       [ "services" "headscale" "settings" "tls_letsencrypt_listen" ]
553     )
555     (mkRemovedOptionModule [ "services" "headscale" "openIdConnect" "domainMap" ] ''
556       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.
557     '')
558   ];
560   config = lib.mkIf cfg.enable {
561     assertions = [
562       {
563         # This is stricter than it needs to be but is exactly what upstream does:
564         # https://github.com/kradalby/headscale/blob/adc084f20f843d7963c999764fa83939668d2d2c/hscontrol/types/config.go#L799
565         assertion =
566           with cfg.settings;
567           dns.use_username_in_magic_dns or false
568           || dns.base_domain == ""
569           || !lib.hasInfix dns.base_domain server_url;
570         message = "server_url cannot contain the base_domain, this will cause the headscale server and embedded DERP to become unreachable from the Tailscale node.";
571       }
572       {
573         assertion = with cfg.settings; dns.magic_dns -> dns.base_domain != "";
574         message = "dns.base_domain must be set when using MagicDNS";
575       }
576       (assertRemovedOption [ "settings" "acl_policy_path" ] "Use `policy.path` instead.")
577       (assertRemovedOption [ "settings" "db_host" ] "Use `database.postgres.host` instead.")
578       (assertRemovedOption [ "settings" "db_name" ] "Use `database.postgres.name` instead.")
579       (assertRemovedOption [
580         "settings"
581         "db_password_file"
582       ] "Use `database.postgres.password_file` instead.")
583       (assertRemovedOption [ "settings" "db_path" ] "Use `database.sqlite.path` instead.")
584       (assertRemovedOption [ "settings" "db_port" ] "Use `database.postgres.port` instead.")
585       (assertRemovedOption [ "settings" "db_type" ] "Use `database.type` instead.")
586       (assertRemovedOption [ "settings" "db_user" ] "Use `database.postgres.user` instead.")
587       (assertRemovedOption [ "settings" "dns_config" ] "Use `dns` instead.")
588       (assertRemovedOption [ "settings" "dns_config" "domains" ] "Use `dns.search_domains` instead.")
589       (assertRemovedOption [
590         "settings"
591         "dns_config"
592         "nameservers"
593       ] "Use `dns.nameservers.global` instead.")
594     ];
596     services.headscale.settings = lib.mkMerge [
597       cliConfig
598       {
599         listen_addr = lib.mkDefault "${cfg.address}:${toString cfg.port}";
601         tls_letsencrypt_cache_dir = "${dataDir}/.cache";
602       }
603     ];
605     environment = {
606       # Headscale CLI needs a minimal config to be able to locate the unix socket
607       # to talk to the server instance.
608       etc."headscale/config.yaml".source = cliConfigFile;
610       systemPackages = [ cfg.package ];
611     };
613     users.groups.headscale = lib.mkIf (cfg.group == "headscale") { };
615     users.users.headscale = lib.mkIf (cfg.user == "headscale") {
616       description = "headscale user";
617       home = dataDir;
618       group = cfg.group;
619       isSystemUser = true;
620     };
622     systemd.services.headscale = {
623       description = "headscale coordination server for Tailscale";
624       wants = [ "network-online.target" ];
625       after = [ "network-online.target" ];
626       wantedBy = [ "multi-user.target" ];
628       script = ''
629         ${lib.optionalString (cfg.settings.database.postgres.password_file != null) ''
630           export HEADSCALE_DATABASE_POSTGRES_PASS="$(head -n1 ${lib.escapeShellArg cfg.settings.database.postgres.password_file})"
631         ''}
633         exec ${lib.getExe cfg.package} serve --config ${configFile}
634       '';
636       serviceConfig =
637         let
638           capabilityBoundingSet = [ "CAP_CHOWN" ] ++ lib.optional (cfg.port < 1024) "CAP_NET_BIND_SERVICE";
639         in
640         {
641           Restart = "always";
642           Type = "simple";
643           User = cfg.user;
644           Group = cfg.group;
646           # Hardening options
647           RuntimeDirectory = "headscale";
648           # Allow headscale group access so users can be added and use the CLI.
649           RuntimeDirectoryMode = "0750";
651           StateDirectory = "headscale";
652           StateDirectoryMode = "0750";
654           ProtectSystem = "strict";
655           ProtectHome = true;
656           PrivateTmp = true;
657           PrivateDevices = true;
658           ProtectKernelTunables = true;
659           ProtectControlGroups = true;
660           RestrictSUIDSGID = true;
661           PrivateMounts = true;
662           ProtectKernelModules = true;
663           ProtectKernelLogs = true;
664           ProtectHostname = true;
665           ProtectClock = true;
666           ProtectProc = "invisible";
667           ProcSubset = "pid";
668           RestrictNamespaces = true;
669           RemoveIPC = true;
670           UMask = "0077";
672           CapabilityBoundingSet = capabilityBoundingSet;
673           AmbientCapabilities = capabilityBoundingSet;
674           NoNewPrivileges = true;
675           LockPersonality = true;
676           RestrictRealtime = true;
677           SystemCallFilter = [
678             "@system-service"
679             "~@privileged"
680             "@chown"
681           ];
682           SystemCallArchitectures = "native";
683           RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX";
684         };
685     };
686   };
688   meta.maintainers = with lib.maintainers; [
689     kradalby
690     misterio77
691   ];