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