grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / misc / radicle.nix
blobcd7a2452223aca3f6b7d6ba4126b6cdc968c2408
1 { config, lib, pkgs, ... }:
2 let
3   cfg = config.services.radicle;
5   json = pkgs.formats.json { };
7   env = rec {
8     # rad fails if it cannot stat $HOME/.gitconfig
9     HOME = "/var/lib/radicle";
10     RAD_HOME = HOME;
11   };
13   # Convenient wrapper to run `rad` in the namespaces of `radicle-node.service`
14   rad-system = pkgs.writeShellScriptBin "rad-system" ''
15     set -o allexport
16     ${lib.toShellVars env}
17     # Note that --env is not used to preserve host's envvars like $TERM
18     exec ${lib.getExe' pkgs.util-linux "nsenter"} -a \
19       -t "$(${lib.getExe' config.systemd.package "systemctl"} show -P MainPID radicle-node.service)" \
20       -S "$(${lib.getExe' config.systemd.package "systemctl"} show -P UID radicle-node.service)" \
21       -G "$(${lib.getExe' config.systemd.package "systemctl"} show -P GID radicle-node.service)" \
22       ${lib.getExe' cfg.package "rad"} "$@"
23   '';
25   commonServiceConfig = serviceName: {
26     environment = env // {
27       RUST_LOG = lib.mkDefault "info";
28     };
29     path = [
30       pkgs.gitMinimal
31     ];
32     documentation = [
33       "https://docs.radicle.xyz/guides/seeder"
34     ];
35     after = [
36       "network.target"
37       "network-online.target"
38     ];
39     requires = [
40       "network-online.target"
41     ];
42     wantedBy = [ "multi-user.target" ];
43     serviceConfig = lib.mkMerge [
44       {
45         BindReadOnlyPaths = [
46           "${cfg.configFile}:${env.RAD_HOME}/config.json"
47           "${if lib.types.path.check cfg.publicKey then cfg.publicKey else pkgs.writeText "radicle.pub" cfg.publicKey}:${env.RAD_HOME}/keys/radicle.pub"
48         ];
49         KillMode = "process";
50         StateDirectory = [ "radicle" ];
51         User = config.users.users.radicle.name;
52         Group = config.users.groups.radicle.name;
53         WorkingDirectory = env.HOME;
54       }
55       # The following options are only for optimizing:
56       # systemd-analyze security ${serviceName}
57       {
58         BindReadOnlyPaths = [
59           "-/etc/resolv.conf"
60           "/etc/ssl/certs/ca-certificates.crt"
61           "/run/systemd"
62         ];
63         AmbientCapabilities = "";
64         CapabilityBoundingSet = "";
65         DeviceAllow = ""; # ProtectClock= adds DeviceAllow=char-rtc r
66         LockPersonality = true;
67         MemoryDenyWriteExecute = true;
68         NoNewPrivileges = true;
69         PrivateTmp = true;
70         ProcSubset = "pid";
71         ProtectClock = true;
72         ProtectHome = true;
73         ProtectHostname = true;
74         ProtectKernelLogs = true;
75         ProtectProc = "invisible";
76         ProtectSystem = "strict";
77         RemoveIPC = true;
78         RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
79         RestrictNamespaces = true;
80         RestrictRealtime = true;
81         RestrictSUIDSGID = true;
82         RuntimeDirectoryMode = "700";
83         SocketBindDeny = [ "any" ];
84         StateDirectoryMode = "0750";
85         SystemCallFilter = [
86           "@system-service"
87           "~@aio"
88           "~@chown"
89           "~@keyring"
90           "~@memlock"
91           "~@privileged"
92           "~@resources"
93           "~@setuid"
94           "~@timer"
95         ];
96         SystemCallArchitectures = "native";
97         # This is for BindPaths= and BindReadOnlyPaths=
98         # to allow traversal of directories they create inside RootDirectory=
99         UMask = "0066";
100       }
101     ];
102     confinement = {
103       enable = true;
104       mode = "full-apivfs";
105       packages = [
106         pkgs.gitMinimal
107         cfg.package
108         pkgs.iana-etc
109         (lib.getLib pkgs.nss)
110         pkgs.tzdata
111       ];
112     };
113   };
116   options = {
117     services.radicle = {
118       enable = lib.mkEnableOption "Radicle Seed Node";
119       package = lib.mkPackageOption pkgs "radicle-node" { };
120       privateKeyFile = lib.mkOption {
121         # Note that a key encrypted by systemd-creds is not a path but a str.
122         type = with lib.types; either path str;
123         description = ''
124           Absolute file path to an SSH private key,
125           usually generated by `rad auth`.
127           If it contains a colon (`:`) the string before the colon
128           is taken as the credential name
129           and the string after as a path encrypted with `systemd-creds`.
130         '';
131       };
132       publicKey = lib.mkOption {
133         type = with lib.types; either path str;
134         description = ''
135           An SSH public key (as an absolute file path or directly as a string),
136           usually generated by `rad auth`.
137         '';
138       };
139       node = {
140         listenAddress = lib.mkOption {
141           type = lib.types.str;
142           default = "[::]";
143           example = "127.0.0.1";
144           description = "The IP address on which `radicle-node` listens.";
145         };
146         listenPort = lib.mkOption {
147           type = lib.types.port;
148           default = 8776;
149           description = "The port on which `radicle-node` listens.";
150         };
151         openFirewall = lib.mkEnableOption "opening the firewall for `radicle-node`";
152         extraArgs = lib.mkOption {
153           type = with lib.types; listOf str;
154           default = [ ];
155           description = "Extra arguments for `radicle-node`";
156         };
157       };
158       configFile = lib.mkOption {
159         type = lib.types.package;
160         internal = true;
161         default = (json.generate "config.json" cfg.settings).overrideAttrs (previousAttrs: {
162           preferLocalBuild = true;
163           # None of the usual phases are run here because runCommandWith uses buildCommand,
164           # so just append to buildCommand what would usually be a checkPhase.
165           buildCommand = previousAttrs.buildCommand + lib.optionalString cfg.checkConfig ''
166             ln -s $out config.json
167             install -D -m 644 /dev/stdin keys/radicle.pub <<<"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBgFMhajUng+Rjj/sCFXI9PzG8BQjru2n7JgUVF1Kbv5 snakeoil"
168             export RAD_HOME=$PWD
169             ${lib.getExe' pkgs.buildPackages.radicle-node "rad"} config >/dev/null || {
170               cat -n config.json
171               echo "Invalid config.json according to rad."
172               echo "Please double-check your services.radicle.settings (producing the config.json above),"
173               echo "some settings may be missing or have the wrong type."
174               exit 1
175             } >&2
176           '';
177         });
178       };
179       checkConfig = lib.mkEnableOption "checking the {file}`config.json` file resulting from {option}`services.radicle.settings`" // { default = true; };
180       settings = lib.mkOption {
181         description = ''
182           See https://app.radicle.xyz/nodes/seed.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/tree/radicle/src/node/config.rs#L275
183         '';
184         default = { };
185         example = lib.literalExpression ''
186           {
187             web.pinned.repositories = [
188               "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5" # heartwood
189               "rad:z3trNYnLWS11cJWC6BbxDs5niGo82" # rips
190             ];
191           }
192         '';
193         type = lib.types.submodule {
194           freeformType = json.type;
195         };
196       };
197       httpd = {
198         enable = lib.mkEnableOption "Radicle HTTP gateway to radicle-node";
199         package = lib.mkPackageOption pkgs "radicle-httpd" { };
200         listenAddress = lib.mkOption {
201           type = lib.types.str;
202           default = "127.0.0.1";
203           description = "The IP address on which `radicle-httpd` listens.";
204         };
205         listenPort = lib.mkOption {
206           type = lib.types.port;
207           default = 8080;
208           description = "The port on which `radicle-httpd` listens.";
209         };
210         nginx = lib.mkOption {
211           # Type of a single virtual host, or null.
212           type = lib.types.nullOr (lib.types.submodule (
213             lib.recursiveUpdate (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) {
214               options.serverName = {
215                 default = "radicle-${config.networking.hostName}.${config.networking.domain}";
216                 defaultText = "radicle-\${config.networking.hostName}.\${config.networking.domain}";
217               };
218             }
219           ));
220           default = null;
221           example = lib.literalExpression ''
222             {
223               serverAliases = [
224                 "seed.''${config.networking.domain}"
225               ];
226               enableACME = false;
227               useACMEHost = config.networking.domain;
228             }
229           '';
230           description = ''
231             With this option, you can customize an nginx virtual host which already has sensible defaults for `radicle-httpd`.
232             Set to `{}` if you do not need any customization to the virtual host.
233             If enabled, then by default, the {option}`serverName` is
234             `radicle-''${config.networking.hostName}.''${config.networking.domain}`,
235             TLS is active, and certificates are acquired via ACME.
236             If this is set to null (the default), no nginx virtual host will be configured.
237           '';
238         };
239         extraArgs = lib.mkOption {
240           type = with lib.types; listOf str;
241           default = [ ];
242           description = "Extra arguments for `radicle-httpd`";
243         };
244       };
245     };
246   };
248   config = lib.mkIf cfg.enable (lib.mkMerge [
249     {
250       systemd.services.radicle-node = lib.mkMerge [
251         (commonServiceConfig "radicle-node")
252         {
253           description = "Radicle Node";
254           documentation = [ "man:radicle-node(1)" ];
255           serviceConfig = {
256             ExecStart = "${lib.getExe' cfg.package "radicle-node"} --force --listen ${cfg.node.listenAddress}:${toString cfg.node.listenPort} ${lib.escapeShellArgs cfg.node.extraArgs}";
257             Restart = lib.mkDefault "on-failure";
258             RestartSec = "30";
259             SocketBindAllow = [ "tcp:${toString cfg.node.listenPort}" ];
260             SystemCallFilter = lib.mkAfter [
261               # Needed by git upload-pack which calls alarm() and setitimer() when providing a rad clone
262               "@timer"
263             ];
264           };
265           confinement.packages = [
266             cfg.package
267           ];
268         }
269         # Give only access to the private key to radicle-node.
270         {
271           serviceConfig =
272             let keyCred = builtins.split ":" "${cfg.privateKeyFile}"; in
273             if lib.length keyCred > 1
274             then {
275               LoadCredentialEncrypted = [ cfg.privateKeyFile ];
276               # Note that neither %d nor ${CREDENTIALS_DIRECTORY} works in BindReadOnlyPaths=
277               BindReadOnlyPaths = [ "/run/credentials/radicle-node.service/${lib.head keyCred}:${env.RAD_HOME}/keys/radicle" ];
278             }
279             else {
280               LoadCredential = [ "radicle:${cfg.privateKeyFile}" ];
281               BindReadOnlyPaths = [ "/run/credentials/radicle-node.service/radicle:${env.RAD_HOME}/keys/radicle" ];
282             };
283         }
284       ];
286       environment.systemPackages = [
287         rad-system
288       ];
290       networking.firewall = lib.mkIf cfg.node.openFirewall {
291         allowedTCPPorts = [ cfg.node.listenPort ];
292       };
294       users = {
295         users.radicle = {
296           description = "Radicle";
297           group = "radicle";
298           home = env.HOME;
299           isSystemUser = true;
300         };
301         groups.radicle = {
302         };
303       };
304     }
306     (lib.mkIf cfg.httpd.enable (lib.mkMerge [
307       {
308         systemd.services.radicle-httpd = lib.mkMerge [
309           (commonServiceConfig "radicle-httpd")
310           {
311             description = "Radicle HTTP gateway to radicle-node";
312             documentation = [ "man:radicle-httpd(1)" ];
313             serviceConfig = {
314               ExecStart = "${lib.getExe' cfg.httpd.package "radicle-httpd"} --listen ${cfg.httpd.listenAddress}:${toString cfg.httpd.listenPort} ${lib.escapeShellArgs cfg.httpd.extraArgs}";
315               Restart = lib.mkDefault "on-failure";
316               RestartSec = "10";
317               SocketBindAllow = [ "tcp:${toString cfg.httpd.listenPort}" ];
318               SystemCallFilter = lib.mkAfter [
319                 # Needed by git upload-pack which calls alarm() and setitimer() when providing a git clone
320                 "@timer"
321               ];
322             };
323           confinement.packages = [
324             cfg.httpd.package
325           ];
326           }
327         ];
328       }
330       (lib.mkIf (cfg.httpd.nginx != null) {
331         services.nginx.virtualHosts.${cfg.httpd.nginx.serverName} = lib.mkMerge [
332           cfg.httpd.nginx
333           {
334             forceSSL = lib.mkDefault true;
335             enableACME = lib.mkDefault true;
336             locations."/" = {
337               proxyPass = "http://${cfg.httpd.listenAddress}:${toString cfg.httpd.listenPort}";
338               recommendedProxySettings = true;
339             };
340           }
341         ];
343         services.radicle.settings = {
344           node.alias = lib.mkDefault cfg.httpd.nginx.serverName;
345           node.externalAddresses = lib.mkDefault [
346             "${cfg.httpd.nginx.serverName}:${toString cfg.node.listenPort}"
347           ];
348         };
349       })
350     ]))
351   ]);
353   meta.maintainers = with lib.maintainers; [
354     julm
355     lorenzleutgeb
356   ];