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