/ nix / modules / fedimintd.nix
fedimintd.nix
  1  {
  2    config,
  3    lib,
  4    pkgs,
  5    ...
  6  }:
  7  let
  8    inherit (lib)
  9      filterAttrs
 10      flatten
 11      mapAttrs'
 12      mapAttrsToList
 13      mkDefault
 14      mkEnableOption
 15      mkIf
 16      mkOption
 17      mkPackageOption
 18      nameValuePair
 19      types
 20      ;
 21  
 22    eachFedimintd = filterAttrs (fedimintdName: cfg: cfg.enable) config.services.fedimintd;
 23    eachFedimintdNginx = filterAttrs (fedimintdName: cfg: cfg.nginx.enable) eachFedimintd;
 24  
 25    fedimintdOpts =
 26      {
 27        config,
 28        lib,
 29        name,
 30        ...
 31      }:
 32      {
 33        options = {
 34          enable = mkEnableOption "fedimintd";
 35  
 36          package = mkPackageOption pkgs "fedimintd" { };
 37  
 38          user = mkOption {
 39            type = types.str;
 40            default = "fedimintd-${name}";
 41            description = "The user as which to run fedimintd.";
 42          };
 43  
 44          group = mkOption {
 45            type = types.str;
 46            default = config.user;
 47            description = "The group as which to run fedimintd.";
 48          };
 49  
 50          extraEnvironment = mkOption {
 51            type = types.attrsOf types.str;
 52            description = lib.mdDoc "Extra Environment variables to pass to the fedimintd.";
 53            default = {
 54              RUST_BACKTRACE = "1";
 55            };
 56            example = {
 57              RUST_LOG = "info,fm=debug";
 58              RUST_BACKTRACE = "1";
 59            };
 60          };
 61  
 62          p2p = {
 63            openFirewall = mkOption {
 64              type = types.bool;
 65              default = true;
 66              description = lib.mdDoc "Opens port in firewall for fedimintd's p2p port";
 67            };
 68            port = mkOption {
 69              type = types.port;
 70              default = 8173;
 71              description = lib.mdDoc "Port to bind on for p2p connections from peers";
 72            };
 73            bind = mkOption {
 74              type = types.str;
 75              default = "0.0.0.0";
 76              description = lib.mdDoc "Address to bind on for p2p connections from peers";
 77            };
 78            fqdn = mkOption {
 79              type = types.nullOr types.str;
 80              default = null;
 81              example = "p2p.myfedimint.com";
 82              description = lib.mdDoc "Domain to host p2p over";
 83            };
 84            address = mkOption {
 85              type = types.nullOr types.str;
 86              default = "fedimint://${config.p2p.fqdn}";
 87              example = "fedimint://p2p.myfedimint.com";
 88              description = lib.mdDoc ''
 89                Public address for p2p connections from peers
 90  
 91                Typically you want to set `fqdn` instead.
 92              '';
 93            };
 94          };
 95          api = {
 96            openFirewall = mkOption {
 97              type = types.bool;
 98              default = false;
 99              description = lib.mdDoc "Opens port in firewall for fedimintd's api port";
100            };
101            port = mkOption {
102              type = types.port;
103              default = 8174;
104              description = lib.mdDoc "Port to bind on for API connections relied by the reverse proxy/tls terminator.";
105            };
106            bind = mkOption {
107              type = types.str;
108              default = "127.0.0.1";
109              description = lib.mdDoc "Address to bind on for API connections relied by the reverse proxy/tls terminator. Usually starting with `fedimint://`";
110            };
111            fqdn = mkOption {
112              type = types.nullOr types.str;
113              default = null;
114              example = "api.myfedimint.com";
115              description = lib.mdDoc "Domain to host API on";
116            };
117            address = mkOption {
118              type = types.nullOr types.str;
119              default = "wss://${config.api.fqdn}";
120              description = lib.mdDoc ''
121                Public URL of the API address of the reverse proxy/tls terminator. Usually starting with `wss://`.
122  
123                Typically you want to override `fqdn` instead.
124              '';
125            };
126          };
127          bitcoin = {
128            network = mkOption {
129              type = types.str;
130              default = "signet";
131              example = "bitcoin";
132              description = lib.mdDoc "Bitcoin network to participate in.";
133            };
134            rpc = {
135              address = mkOption {
136                type = types.str;
137                default = "http://127.0.0.1:38332";
138                example = "signet";
139                description = lib.mdDoc "Bitcoin node (bitcoind/electrum/esplora) address to connect to";
140              };
141  
142              kind = mkOption {
143                type = types.str;
144                default = "bitcoind";
145                example = "electrum";
146                description = lib.mdDoc "Kind of a bitcoin node.";
147              };
148  
149              secretFile = mkOption {
150                type = types.nullOr types.str;
151                default = null;
152                description = lib.mdDoc ''
153                  If set the URL specified in `bitcoin.rpc.address` will get the content of this file added
154                  as an URL password, so `http://user@example.com` will turn into `http://user:SOMESECRET@example.com`.
155  
156                  Example:
157  
158                  `/etc/nix-bitcoin-secrets/bitcoin-rpcpassword-public` (for nix-bitcoin default)
159                '';
160              };
161            };
162          };
163  
164          consensus.finalityDelay = mkOption {
165            type = types.number;
166            default = 10;
167            description = lib.mdDoc "Consensus peg-in finality delay.";
168          };
169  
170          dataDir = mkOption {
171            type = types.str;
172            default = "/var/lib/fedimintd-${name}/";
173            readOnly = true;
174            description = lib.mdDoc ''
175              Path to the data dir fedimintd will use to store its data.
176              Note that due to using the DynamicUser feature of systemd, this value should not be changed
177              and is set to be read only.
178            '';
179          };
180  
181          nginx = {
182            enable = mkEnableOption "fedimint";
183            config = mkOption {
184              # TODO: change to something like https://github.com/NixOS/nixpkgs/pull/314440/files#diff-47ed1acddaad94538b9ee7995ffa8d7cc1376f9667350acee9cec912cec6a3bfR201
185              type = types.attrs;
186              default = { };
187              description = lib.mdDoc "Overrides to the nginx vhost section for api";
188            };
189          };
190        };
191      };
192  in
193  {
194    options = {
195      services.fedimintd = mkOption {
196        type = types.attrsOf (types.submodule fedimintdOpts);
197        default = { };
198        description = lib.mdDoc "Specification of one or more fedimintd instances.";
199      };
200    };
201  
202    config = mkIf (eachFedimintd != { }) {
203  
204      assertions = flatten (
205        mapAttrsToList (fedimintdName: cfg: [
206          {
207            assertion = cfg.p2p.address != null;
208            message = ''
209              `services.fedimintd.${fedimintdName}.p2p.address` must be set to address reachable by other peers.
210  
211              Example: `fedimint://p2p.mymint.org`.
212            '';
213          }
214          {
215            assertion = cfg.api.address != null;
216            message = ''
217              `services.fedimintd.${fedimintdName}.api.address` must be set to address reachable by the clients, with TLS terminated by external service (typically nginx), and relayed to the fedimintd bind address.
218  
219              Example: `wss://api.mymint.org`.
220            '';
221          }
222        ]) eachFedimintd
223      );
224  
225      networking.firewall.allowedTCPPorts = flatten (
226        mapAttrsToList (
227          fedimintdName: cfg:
228          (if cfg.api.openFirewall then [ cfg.api.port ] else [ ])
229          ++ (if cfg.p2p.openFirewall then [ cfg.p2p.port ] else [ ])
230        ) eachFedimintd
231      );
232  
233      systemd.services = mapAttrs' (
234        fedimintdName: cfg:
235        (nameValuePair "fedimintd-${fedimintdName}" (
236          let
237            startScript = pkgs.writeShellScript "fedimintd-start" (
238              (
239                if cfg.bitcoin.rpc.secretFile != null then
240                  ''
241                    secret=$(${pkgs.coreutils}/bin/head -n 1 "${cfg.bitcoin.rpc.secretFile}")
242                    prefix="''${FM_DEFAULT_BITCOIN_RPC_URL%*@*}"  # Everything before the last '@'
243                    suffix="''${FM_DEFAULT_BITCOIN_RPC_URL##*@}"  # Everything after the last '@'
244                    FM_DEFAULT_BITCOIN_RPC_URL="''${prefix}:''${secret}@''${suffix}"
245                  ''
246                else
247                  ""
248              )
249              + ''
250                exec ${cfg.package}/bin/fedimintd
251              ''
252            );
253          in
254          {
255            description = "Fedimint Server";
256            documentation = [ "https://github.com/fedimint/fedimint/" ];
257            wantedBy = [ "multi-user.target" ];
258            environment = lib.mkMerge ([
259              {
260                FM_BIND_P2P = "${cfg.p2p.bind}:${builtins.toString cfg.p2p.port}";
261                FM_BIND_API = "${cfg.api.bind}:${builtins.toString cfg.api.port}";
262                FM_P2P_URL = cfg.p2p.address;
263                FM_API_URL = cfg.api.address;
264                FM_DATA_DIR = cfg.dataDir;
265                FM_BITCOIN_NETWORK = cfg.bitcoin.network;
266                FM_DEFAULT_BITCOIN_RPC_URL = cfg.bitcoin.rpc.address;
267                FM_DEFAULT_BITCOIN_RPC_KIND = cfg.bitcoin.rpc.kind;
268  
269                # Deprecated envvars, for backward compatibility
270                FM_BITCOIN_RPC_URL = cfg.bitcoin.rpc.address;
271                FM_BITCOIN_RPC_KIND = cfg.bitcoin.rpc.kind;
272              }
273              cfg.extraEnvironment
274            ]);
275            serviceConfig = {
276              User = cfg.user;
277              Group = cfg.group;
278  
279              Restart = "always";
280              RestartSec = 10;
281              StartLimitBurst = 5;
282              UMask = "077";
283              LimitNOFILE = "100000";
284  
285              LockPersonality = true;
286              ProtectClock = true;
287              ProtectControlGroups = true;
288              ProtectHostname = true;
289              ProtectKernelLogs = true;
290              ProtectKernelModules = true;
291              ProtectKernelTunables = true;
292              PrivateMounts = true;
293              RestrictAddressFamilies = [
294                "AF_INET"
295                "AF_INET6"
296              ];
297              RestrictNamespaces = true;
298              RestrictRealtime = true;
299              SystemCallArchitectures = "native";
300              SystemCallFilter = [
301                "@system-service"
302                "~@privileged"
303              ];
304              StateDirectory = "fedimintd-${fedimintdName}";
305              StateDirectoryMode = "0700";
306              ExecStart = startScript;
307  
308              # Hardening measures
309              PrivateTmp = "true";
310              ProtectSystem = "full";
311              NoNewPrivileges = "true";
312              PrivateDevices = "true";
313              MemoryDenyWriteExecute = "true";
314            };
315          }
316        ))
317      ) eachFedimintd;
318  
319      users.users = mapAttrs' (
320        fedimintdName: cfg:
321        (nameValuePair "fedimintd-${fedimintdName}" {
322          name = cfg.user;
323          group = cfg.group;
324          description = "Fedimint daemon user";
325          home = cfg.dataDir;
326          isSystemUser = true;
327        })
328      ) eachFedimintd;
329  
330      users.groups = mapAttrs' (fedimintdName: cfg: (nameValuePair "${cfg.group}" { })) eachFedimintd;
331  
332      services.nginx.virtualHosts = mapAttrs' (
333        fedimintdName: cfg:
334        (nameValuePair cfg.api.fqdn (
335          lib.mkMerge [
336  
337            cfg.nginx.config
338  
339            {
340              enableACME = mkDefault true;
341              forceSSL = mkDefault true;
342              locations."/ws/" = {
343                proxyPass = "http://127.0.0.1:${builtins.toString cfg.api.port}/";
344                proxyWebsockets = true;
345                extraConfig = "proxy_pass_header Authorization;";
346              };
347            }
348  
349          ]
350        ))
351      ) eachFedimintdNginx;
352    };
353  
354  }