python3Packages.orjson: Disable failing tests on 32 bit
[NixPkgs.git] / nixos / modules / services / networking / headscale.nix
blob0334c5a00bab53c53364c67030eec7e9ccff69ba
1 { config, lib, pkgs, ... }:
2 with lib;
3 let
4   cfg = config.services.headscale;
6   dataDir = "/var/lib/headscale";
7   runDir = "/run/headscale";
9   settingsFormat = pkgs.formats.yaml { };
10   configFile = settingsFormat.generate "headscale.yaml" cfg.settings;
13   options = {
14     services.headscale = {
15       enable = mkEnableOption (lib.mdDoc "headscale, Open Source coordination server for Tailscale");
17       package = mkOption {
18         type = types.package;
19         default = pkgs.headscale;
20         defaultText = literalExpression "pkgs.headscale";
21         description = lib.mdDoc ''
22           Which headscale package to use for the running server.
23         '';
24       };
26       user = mkOption {
27         default = "headscale";
28         type = types.str;
29         description = lib.mdDoc ''
30           User account under which headscale runs.
32           ::: {.note}
33           If left as the default value this user will automatically be created
34           on system activation, otherwise you are responsible for
35           ensuring the user exists before the headscale service starts.
36           :::
37         '';
38       };
40       group = mkOption {
41         default = "headscale";
42         type = types.str;
43         description = lib.mdDoc ''
44           Group under which headscale runs.
46           ::: {.note}
47           If left as the default value this group will automatically be created
48           on system activation, otherwise you are responsible for
49           ensuring the user exists before the headscale service starts.
50           :::
51         '';
52       };
54       serverUrl = mkOption {
55         type = types.str;
56         default = "http://127.0.0.1:8080";
57         description = lib.mdDoc ''
58           The url clients will connect to.
59         '';
60         example = "https://myheadscale.example.com:443";
61       };
63       address = mkOption {
64         type = types.str;
65         default = "127.0.0.1";
66         description = lib.mdDoc ''
67           Listening address of headscale.
68         '';
69         example = "0.0.0.0";
70       };
72       port = mkOption {
73         type = types.port;
74         default = 8080;
75         description = lib.mdDoc ''
76           Listening port of headscale.
77         '';
78         example = 443;
79       };
81       privateKeyFile = mkOption {
82         type = types.path;
83         default = "${dataDir}/private.key";
84         description = lib.mdDoc ''
85           Path to private key file, generated automatically if it does not exist.
86         '';
87       };
89       derp = {
90         urls = mkOption {
91           type = types.listOf types.str;
92           default = [ "https://controlplane.tailscale.com/derpmap/default" ];
93           description = lib.mdDoc ''
94             List of urls containing DERP maps.
95             See [How Tailscale works](https://tailscale.com/blog/how-tailscale-works/) for more information on DERP maps.
96           '';
97         };
99         paths = mkOption {
100           type = types.listOf types.path;
101           default = [ ];
102           description = lib.mdDoc ''
103             List of file paths containing DERP maps.
104             See [How Tailscale works](https://tailscale.com/blog/how-tailscale-works/) for more information on DERP maps.
105           '';
106         };
109         autoUpdate = mkOption {
110           type = types.bool;
111           default = true;
112           description = lib.mdDoc ''
113             Whether to automatically update DERP maps on a set frequency.
114           '';
115           example = false;
116         };
118         updateFrequency = mkOption {
119           type = types.str;
120           default = "24h";
121           description = lib.mdDoc ''
122             Frequency to update DERP maps.
123           '';
124           example = "5m";
125         };
127       };
129       ephemeralNodeInactivityTimeout = mkOption {
130         type = types.str;
131         default = "30m";
132         description = lib.mdDoc ''
133           Time before an inactive ephemeral node is deleted.
134         '';
135         example = "5m";
136       };
138       database = {
139         type = mkOption {
140           type = types.enum [ "sqlite3" "postgres" ];
141           example = "postgres";
142           default = "sqlite3";
143           description = lib.mdDoc "Database engine to use.";
144         };
146         host = mkOption {
147           type = types.nullOr types.str;
148           default = null;
149           example = "127.0.0.1";
150           description = lib.mdDoc "Database host address.";
151         };
153         port = mkOption {
154           type = types.nullOr types.port;
155           default = null;
156           example = 3306;
157           description = lib.mdDoc "Database host port.";
158         };
160         name = mkOption {
161           type = types.nullOr types.str;
162           default = null;
163           example = "headscale";
164           description = lib.mdDoc "Database name.";
165         };
167         user = mkOption {
168           type = types.nullOr types.str;
169           default = null;
170           example = "headscale";
171           description = lib.mdDoc "Database user.";
172         };
174         passwordFile = mkOption {
175           type = types.nullOr types.path;
176           default = null;
177           example = "/run/keys/headscale-dbpassword";
178           description = lib.mdDoc ''
179             A file containing the password corresponding to
180             {option}`database.user`.
181           '';
182         };
184         path = mkOption {
185           type = types.nullOr types.str;
186           default = "${dataDir}/db.sqlite";
187           description = lib.mdDoc "Path to the sqlite3 database file.";
188         };
189       };
191       logLevel = mkOption {
192         type = types.str;
193         default = "info";
194         description = lib.mdDoc ''
195           headscale log level.
196         '';
197         example = "debug";
198       };
200       dns = {
201         nameservers = mkOption {
202           type = types.listOf types.str;
203           default = [ "1.1.1.1" ];
204           description = lib.mdDoc ''
205             List of nameservers to pass to Tailscale clients.
206           '';
207         };
209         domains = mkOption {
210           type = types.listOf types.str;
211           default = [ ];
212           description = lib.mdDoc ''
213             Search domains to inject to Tailscale clients.
214           '';
215           example = [ "mydomain.internal" ];
216         };
218         magicDns = mkOption {
219           type = types.bool;
220           default = true;
221           description = lib.mdDoc ''
222             Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/).
223             Only works if there is at least a nameserver defined.
224           '';
225           example = false;
226         };
228         baseDomain = mkOption {
229           type = types.str;
230           default = "";
231           description = lib.mdDoc ''
232             Defines the base domain to create the hostnames for MagicDNS.
233             {option}`baseDomain` must be a FQDNs, without the trailing dot.
234             The FQDN of the hosts will be
235             `hostname.namespace.base_domain` (e.g.
236             `myhost.mynamespace.example.com`).
237           '';
238         };
239       };
241       openIdConnect = {
242         issuer = mkOption {
243           type = types.str;
244           default = "";
245           description = lib.mdDoc ''
246             URL to OpenID issuer.
247           '';
248           example = "https://openid.example.com";
249         };
251         clientId = mkOption {
252           type = types.str;
253           default = "";
254           description = lib.mdDoc ''
255             OpenID Connect client ID.
256           '';
257         };
259         clientSecretFile = mkOption {
260           type = types.nullOr types.path;
261           default = null;
262           description = lib.mdDoc ''
263             Path to OpenID Connect client secret file.
264           '';
265         };
267         domainMap = mkOption {
268           type = types.attrsOf types.str;
269           default = { };
270           description = lib.mdDoc ''
271             Domain map is used to map incomming users (by their email) to
272             a namespace. The key can be a string, or regex.
273           '';
274           example = {
275             ".*" = "default-namespace";
276           };
277         };
279       };
281       tls = {
282         letsencrypt = {
283           hostname = mkOption {
284             type = types.nullOr types.str;
285             default = "";
286             description = lib.mdDoc ''
287               Domain name to request a TLS certificate for.
288             '';
289           };
290           challengeType = mkOption {
291             type = types.enum [ "TLS-ALPN-01" "HTTP-01" ];
292             default = "HTTP-01";
293             description = lib.mdDoc ''
294               Type of ACME challenge to use, currently supported types:
295               `HTTP-01` or `TLS-ALPN-01`.
296             '';
297           };
298           httpListen = mkOption {
299             type = types.nullOr types.str;
300             default = ":http";
301             description = lib.mdDoc ''
302               When HTTP-01 challenge is chosen, letsencrypt must set up a
303               verification endpoint, and it will be listening on:
304               `:http = port 80`.
305             '';
306           };
307         };
309         certFile = mkOption {
310           type = types.nullOr types.path;
311           default = null;
312           description = lib.mdDoc ''
313             Path to already created certificate.
314           '';
315         };
316         keyFile = mkOption {
317           type = types.nullOr types.path;
318           default = null;
319           description = lib.mdDoc ''
320             Path to key for already created certificate.
321           '';
322         };
323       };
325       aclPolicyFile = mkOption {
326         type = types.nullOr types.path;
327         default = null;
328         description = lib.mdDoc ''
329           Path to a file containg ACL policies.
330         '';
331       };
333       settings = mkOption {
334         type = settingsFormat.type;
335         default = { };
336         description = lib.mdDoc ''
337           Overrides to {file}`config.yaml` as a Nix attribute set.
338           This option is ideal for overriding settings not exposed as Nix options.
339           Check the [example config](https://github.com/juanfont/headscale/blob/main/config-example.yaml)
340           for possible options.
341         '';
342       };
345     };
347   };
348   config = mkIf cfg.enable {
350     services.headscale.settings = {
351       server_url = mkDefault cfg.serverUrl;
352       listen_addr = mkDefault "${cfg.address}:${toString cfg.port}";
354       private_key_path = mkDefault cfg.privateKeyFile;
356       derp = {
357         urls = mkDefault cfg.derp.urls;
358         paths = mkDefault cfg.derp.paths;
359         auto_update_enable = mkDefault cfg.derp.autoUpdate;
360         update_frequency = mkDefault cfg.derp.updateFrequency;
361       };
363       # Turn off update checks since the origin of our package
364       # is nixpkgs and not Github.
365       disable_check_updates = true;
367       ephemeral_node_inactivity_timeout = mkDefault cfg.ephemeralNodeInactivityTimeout;
369       db_type = mkDefault cfg.database.type;
370       db_path = mkDefault cfg.database.path;
372       log_level = mkDefault cfg.logLevel;
374       dns_config = {
375         nameservers = mkDefault cfg.dns.nameservers;
376         domains = mkDefault cfg.dns.domains;
377         magic_dns = mkDefault cfg.dns.magicDns;
378         base_domain = mkDefault cfg.dns.baseDomain;
379       };
381       unix_socket = "${runDir}/headscale.sock";
383       # OpenID Connect
384       oidc = {
385         issuer = mkDefault cfg.openIdConnect.issuer;
386         client_id = mkDefault cfg.openIdConnect.clientId;
387         domain_map = mkDefault cfg.openIdConnect.domainMap;
388       };
390       tls_letsencrypt_cache_dir = "${dataDir}/.cache";
392     } // optionalAttrs (cfg.database.host != null) {
393       db_host = mkDefault cfg.database.host;
394     } // optionalAttrs (cfg.database.port != null) {
395       db_port = mkDefault cfg.database.port;
396     } // optionalAttrs (cfg.database.name != null) {
397       db_name = mkDefault cfg.database.name;
398     } // optionalAttrs (cfg.database.user != null) {
399       db_user = mkDefault cfg.database.user;
400     } // optionalAttrs (cfg.tls.letsencrypt.hostname != null) {
401       tls_letsencrypt_hostname = mkDefault cfg.tls.letsencrypt.hostname;
402     } // optionalAttrs (cfg.tls.letsencrypt.challengeType != null) {
403       tls_letsencrypt_challenge_type = mkDefault cfg.tls.letsencrypt.challengeType;
404     } // optionalAttrs (cfg.tls.letsencrypt.httpListen != null) {
405       tls_letsencrypt_listen = mkDefault cfg.tls.letsencrypt.httpListen;
406     } // optionalAttrs (cfg.tls.certFile != null) {
407       tls_cert_path = mkDefault cfg.tls.certFile;
408     } // optionalAttrs (cfg.tls.keyFile != null) {
409       tls_key_path = mkDefault cfg.tls.keyFile;
410     } // optionalAttrs (cfg.aclPolicyFile != null) {
411       acl_policy_path = mkDefault cfg.aclPolicyFile;
412     };
414     # Setup the headscale configuration in a known path in /etc to
415     # allow both the Server and the Client use it to find the socket
416     # for communication.
417     environment.etc."headscale/config.yaml".source = configFile;
419     users.groups.headscale = mkIf (cfg.group == "headscale") { };
421     users.users.headscale = mkIf (cfg.user == "headscale") {
422       description = "headscale user";
423       home = dataDir;
424       group = cfg.group;
425       isSystemUser = true;
426     };
428     systemd.services.headscale = {
429       description = "headscale coordination server for Tailscale";
430       after = [ "network-online.target" ];
431       wantedBy = [ "multi-user.target" ];
432       restartTriggers = [ configFile ];
434       environment.GIN_MODE = "release";
436       script = ''
437         ${optionalString (cfg.database.passwordFile != null) ''
438           export HEADSCALE_DB_PASS="$(head -n1 ${escapeShellArg cfg.database.passwordFile})"
439         ''}
441         ${optionalString (cfg.openIdConnect.clientSecretFile != null) ''
442           export HEADSCALE_OIDC_CLIENT_SECRET="$(head -n1 ${escapeShellArg cfg.openIdConnect.clientSecretFile})"
443         ''}
444         exec ${cfg.package}/bin/headscale serve
445       '';
447       serviceConfig =
448         let
449           capabilityBoundingSet = [ "CAP_CHOWN" ] ++ optional (cfg.port < 1024) "CAP_NET_BIND_SERVICE";
450         in
451         {
452           Restart = "always";
453           Type = "simple";
454           User = cfg.user;
455           Group = cfg.group;
457           # Hardening options
458           RuntimeDirectory = "headscale";
459           # Allow headscale group access so users can be added and use the CLI.
460           RuntimeDirectoryMode = "0750";
462           StateDirectory = "headscale";
463           StateDirectoryMode = "0750";
465           ProtectSystem = "strict";
466           ProtectHome = true;
467           PrivateTmp = true;
468           PrivateDevices = true;
469           ProtectKernelTunables = true;
470           ProtectControlGroups = true;
471           RestrictSUIDSGID = true;
472           PrivateMounts = true;
473           ProtectKernelModules = true;
474           ProtectKernelLogs = true;
475           ProtectHostname = true;
476           ProtectClock = true;
477           ProtectProc = "invisible";
478           ProcSubset = "pid";
479           RestrictNamespaces = true;
480           RemoveIPC = true;
481           UMask = "0077";
483           CapabilityBoundingSet = capabilityBoundingSet;
484           AmbientCapabilities = capabilityBoundingSet;
485           NoNewPrivileges = true;
486           LockPersonality = true;
487           RestrictRealtime = true;
488           SystemCallFilter = [ "@system-service" "~@privileged" "@chown" ];
489           SystemCallArchitectures = "native";
490           RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX";
491         };
492     };
493   };
495   meta.maintainers = with maintainers; [ kradalby ];