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 }