vuls: init at 0.27.0
[NixPkgs.git] / nixos / modules / services / networking / headscale.nix
blob622a13fe7b61b668f1df3c8043eb3de971e3638f
2   config,
3   lib,
4   pkgs,
5   ...
6 }: let
7   cfg = config.services.headscale;
9   dataDir = "/var/lib/headscale";
10   runDir = "/run/headscale";
12   cliConfig = {
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";
18   };
20   settingsFormat = pkgs.formats.yaml {};
21   configFile = settingsFormat.generate "headscale.yaml" cfg.settings;
22   cliConfigFile = settingsFormat.generate "headscale.yaml" cliConfig;
23 in {
24   options = {
25     services.headscale = {
26       enable = lib.mkEnableOption "headscale, Open Source coordination server for Tailscale";
28       package = lib.mkPackageOption pkgs "headscale" {};
30       user = lib.mkOption {
31         default = "headscale";
32         type = lib.types.str;
33         description = ''
34           User account under which headscale runs.
36           ::: {.note}
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.
40           :::
41         '';
42       };
44       group = lib.mkOption {
45         default = "headscale";
46         type = lib.types.str;
47         description = ''
48           Group under which headscale runs.
50           ::: {.note}
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.
54           :::
55         '';
56       };
58       address = lib.mkOption {
59         type = lib.types.str;
60         default = "127.0.0.1";
61         description = ''
62           Listening address of headscale.
63         '';
64         example = "0.0.0.0";
65       };
67       port = lib.mkOption {
68         type = lib.types.port;
69         default = 8080;
70         description = ''
71           Listening port of headscale.
72         '';
73         example = 443;
74       };
76       settings = lib.mkOption {
77         description = ''
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)
80           for possible options.
81         '';
82         type = lib.types.submodule {
83           freeformType = settingsFormat.type;
85           imports = with lib; [
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"])
98           ];
100           options = {
101             server_url = lib.mkOption {
102               type = lib.types.str;
103               default = "http://127.0.0.1:8080";
104               description = ''
105                 The url clients will connect to.
106               '';
107               example = "https://myheadscale.example.com:443";
108             };
110             noise.private_key_path = lib.mkOption {
111               type = lib.types.path;
112               default = "${dataDir}/noise_private.key";
113               description = ''
114                 Path to noise private key file, generated automatically if it does not exist.
115               '';
116             };
118             prefixes = let
119               prefDesc = ''
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.
124               '';
125             in {
126               v4 = lib.mkOption {
127                 type = lib.types.str;
128                 default = "100.64.0.0/10";
129                 description = prefDesc;
130               };
132               v6 = lib.mkOption {
133                 type = lib.types.str;
134                 default = "fd7a:115c:a1e0::/48";
135                 description = prefDesc;
136               };
138               allocation = lib.mkOption {
139                 type = lib.types.enum ["sequential" "random"];
140                 example = "random";
141                 default = "sequential";
142                 description = ''
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).
146                 '';
147               };
148             };
150             derp = {
151               urls = lib.mkOption {
152                 type = lib.types.listOf lib.types.str;
153                 default = ["https://controlplane.tailscale.com/derpmap/default"];
154                 description = ''
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.
157                 '';
158               };
160               paths = lib.mkOption {
161                 type = lib.types.listOf lib.types.path;
162                 default = [];
163                 description = ''
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.
166                 '';
167               };
169               auto_update_enable = lib.mkOption {
170                 type = lib.types.bool;
171                 default = true;
172                 description = ''
173                   Whether to automatically update DERP maps on a set frequency.
174                 '';
175                 example = false;
176               };
178               update_frequency = lib.mkOption {
179                 type = lib.types.str;
180                 default = "24h";
181                 description = ''
182                   Frequency to update DERP maps.
183                 '';
184                 example = "5m";
185               };
187               server.private_key_path = lib.mkOption {
188                 type = lib.types.path;
189                 default = "${dataDir}/derp_server_private.key";
190                 description = ''
191                   Path to derp private key file, generated automatically if it does not exist.
192                 '';
193               };
194             };
196             ephemeral_node_inactivity_timeout = lib.mkOption {
197               type = lib.types.str;
198               default = "30m";
199               description = ''
200                 Time before an inactive ephemeral node is deleted.
201               '';
202               example = "5m";
203             };
205             database = {
206               type = lib.mkOption {
207                 type = lib.types.enum ["sqlite" "sqlite3" "postgres"];
208                 example = "postgres";
209                 default = "sqlite";
210                 description = ''
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.
214                 '';
215               };
217               sqlite = {
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.";
222                 };
224                 write_ahead_log = lib.mkOption {
225                   type = lib.types.bool;
226                   default = true;
227                   description = ''
228                     Enable WAL mode for SQLite. This is recommended for production environments.
229                     https://www.sqlite.org/wal.html
230                   '';
231                   example = true;
232                 };
233               };
235               postgres = {
236                 host = lib.mkOption {
237                   type = lib.types.nullOr lib.types.str;
238                   default = null;
239                   example = "127.0.0.1";
240                   description = "Database host address.";
241                 };
243                 port = lib.mkOption {
244                   type = lib.types.nullOr lib.types.port;
245                   default = null;
246                   example = 3306;
247                   description = "Database host port.";
248                 };
250                 name = lib.mkOption {
251                   type = lib.types.nullOr lib.types.str;
252                   default = null;
253                   example = "headscale";
254                   description = "Database name.";
255                 };
257                 user = lib.mkOption {
258                   type = lib.types.nullOr lib.types.str;
259                   default = null;
260                   example = "headscale";
261                   description = "Database user.";
262                 };
264                 password_file = lib.mkOption {
265                   type = lib.types.nullOr lib.types.path;
266                   default = null;
267                   example = "/run/keys/headscale-dbpassword";
268                   description = ''
269                     A file containing the password corresponding to
270                     {option}`database.user`.
271                   '';
272                 };
273               };
274             };
276             log = {
277               level = lib.mkOption {
278                 type = lib.types.str;
279                 default = "info";
280                 description = ''
281                   headscale log level.
282                 '';
283                 example = "debug";
284               };
286               format = lib.mkOption {
287                 type = lib.types.str;
288                 default = "text";
289                 description = ''
290                   headscale log format.
291                 '';
292                 example = "json";
293               };
294             };
296             dns = {
297               magic_dns = lib.mkOption {
298                 type = lib.types.bool;
299                 default = true;
300                 description = ''
301                   Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/).
302                   Only works if there is at least a nameserver defined.
303                 '';
304                 example = false;
305               };
307               base_domain = lib.mkOption {
308                 type = lib.types.str;
309                 default = "";
310                 description = ''
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`).
316                 '';
317               };
319               nameservers = {
320                 global = lib.mkOption {
321                   type = lib.types.listOf lib.types.str;
322                   default = [];
323                   description = ''
324                     List of nameservers to pass to Tailscale clients.
325                   '';
326                 };
327               };
329               search_domains = lib.mkOption {
330                 type = lib.types.listOf lib.types.str;
331                 default = [];
332                 description = ''
333                   Search domains to inject to Tailscale clients.
334                 '';
335                 example = ["mydomain.internal"];
336               };
337             };
339             oidc = {
340               issuer = lib.mkOption {
341                 type = lib.types.str;
342                 default = "";
343                 description = ''
344                   URL to OpenID issuer.
345                 '';
346                 example = "https://openid.example.com";
347               };
349               client_id = lib.mkOption {
350                 type = lib.types.str;
351                 default = "";
352                 description = ''
353                   OpenID Connect client ID.
354                 '';
355               };
357               client_secret_path = lib.mkOption {
358                 type = lib.types.nullOr lib.types.str;
359                 default = null;
360                 description = ''
361                   Path to OpenID Connect client secret file. Expands environment variables in format ''${VAR}.
362                 '';
363               };
365               scope = lib.mkOption {
366                 type = lib.types.listOf lib.types.str;
367                 default = ["openid" "profile" "email"];
368                 description = ''
369                   Scopes used in the OIDC flow.
370                 '';
371               };
373               extra_params = lib.mkOption {
374                 type = lib.types.attrsOf lib.types.str;
375                 default = {};
376                 description = ''
377                   Custom query parameters to send with the Authorize Endpoint request.
378                 '';
379                 example = {
380                   domain_hint = "example.com";
381                 };
382               };
384               allowed_domains = lib.mkOption {
385                 type = lib.types.listOf lib.types.str;
386                 default = [];
387                 description = ''
388                   Allowed principal domains. if an authenticated user's domain
389                   is not in this list authentication request will be rejected.
390                 '';
391                 example = ["example.com"];
392               };
394               allowed_users = lib.mkOption {
395                 type = lib.types.listOf lib.types.str;
396                 default = [];
397                 description = ''
398                   Users allowed to authenticate even if not in allowedDomains.
399                 '';
400                 example = ["alice@example.com"];
401               };
403               strip_email_domain = lib.mkOption {
404                 type = lib.types.bool;
405                 default = true;
406                 description = ''
407                   Whether the domain part of the email address should be removed when generating namespaces.
408                 '';
409               };
410             };
412             tls_letsencrypt_hostname = lib.mkOption {
413               type = lib.types.nullOr lib.types.str;
414               default = "";
415               description = ''
416                 Domain name to request a TLS certificate for.
417               '';
418             };
420             tls_letsencrypt_challenge_type = lib.mkOption {
421               type = lib.types.enum ["TLS-ALPN-01" "HTTP-01"];
422               default = "HTTP-01";
423               description = ''
424                 Type of ACME challenge to use, currently supported types:
425                 `HTTP-01` or `TLS-ALPN-01`.
426               '';
427             };
429             tls_letsencrypt_listen = lib.mkOption {
430               type = lib.types.nullOr lib.types.str;
431               default = ":http";
432               description = ''
433                 When HTTP-01 challenge is chosen, letsencrypt must set up a
434                 verification endpoint, and it will be listening on:
435                 `:http = port 80`.
436               '';
437             };
439             tls_cert_path = lib.mkOption {
440               type = lib.types.nullOr lib.types.path;
441               default = null;
442               description = ''
443                 Path to already created certificate.
444               '';
445             };
447             tls_key_path = lib.mkOption {
448               type = lib.types.nullOr lib.types.path;
449               default = null;
450               description = ''
451                 Path to key for already created certificate.
452               '';
453             };
455             policy = {
456               mode = lib.mkOption {
457                 type = lib.types.enum ["file" "database"];
458                 default = "file";
459                 description = ''
460                   The mode can be "file" or "database" that defines
461                   where the ACL policies are stored and read from.
462                 '';
463               };
465               path = lib.mkOption {
466                 type = lib.types.nullOr lib.types.path;
467                 default = null;
468                 description = ''
469                   If the mode is set to "file", the path to a
470                   HuJSON file containing ACL policies.
471                 '';
472               };
473             };
474           };
475         };
476       };
477     };
478   };
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.
499     '')
500   ];
502   config = lib.mkIf cfg.enable {
503     services.headscale.settings = lib.mkMerge [
504       cliConfig
505       {
506         listen_addr = lib.mkDefault "${cfg.address}:${toString cfg.port}";
508         tls_letsencrypt_cache_dir = "${dataDir}/.cache";
509       }
510     ];
512     environment = {
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];
518     };
520     users.groups.headscale = lib.mkIf (cfg.group == "headscale") {};
522     users.users.headscale = lib.mkIf (cfg.user == "headscale") {
523       description = "headscale user";
524       home = dataDir;
525       group = cfg.group;
526       isSystemUser = true;
527     };
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"];
535       script = ''
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})"
538         ''}
540         exec ${lib.getExe cfg.package} serve --config ${configFile}
541       '';
543       serviceConfig = let
544         capabilityBoundingSet = ["CAP_CHOWN"] ++ lib.optional (cfg.port < 1024) "CAP_NET_BIND_SERVICE";
545       in {
546         Restart = "always";
547         Type = "simple";
548         User = cfg.user;
549         Group = cfg.group;
551         # Hardening options
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";
560         ProtectHome = true;
561         PrivateTmp = true;
562         PrivateDevices = true;
563         ProtectKernelTunables = true;
564         ProtectControlGroups = true;
565         RestrictSUIDSGID = true;
566         PrivateMounts = true;
567         ProtectKernelModules = true;
568         ProtectKernelLogs = true;
569         ProtectHostname = true;
570         ProtectClock = true;
571         ProtectProc = "invisible";
572         ProcSubset = "pid";
573         RestrictNamespaces = true;
574         RemoveIPC = true;
575         UMask = "0077";
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";
585       };
586     };
587   };
589   meta.maintainers = with lib.maintainers; [kradalby misterio77];