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 }