1{
2 config,
3 lib,
4 pkgs,
5 utils,
6 ...
7}:
8
9let
10 inherit (lib)
11 any
12 concatMap
13 getExe'
14 literalExpression
15 mkEnableOption
16 mkIf
17 mkOption
18 mkPackageOption
19 optional
20 recursiveUpdate
21 ;
22
23 inherit (lib.types)
24 bool
25 enum
26 listOf
27 port
28 str
29 ;
30
31 inherit (utils) escapeSystemdExecArgs genJqSecretsReplacementSnippet;
32
33 stateDir = "/var/lib/netbird-mgmt";
34
35 settingsFormat = pkgs.formats.json { };
36
37 defaultSettings = {
38 Stuns = [
39 {
40 Proto = "udp";
41 URI = "stun:${cfg.turnDomain}:3478";
42 Username = "";
43 Password = null;
44 }
45 ];
46
47 TURNConfig = {
48 Turns = [
49 {
50 Proto = "udp";
51 URI = "turn:${cfg.turnDomain}:${builtins.toString cfg.turnPort}";
52 Username = "netbird";
53 Password = "netbird";
54 }
55 ];
56
57 CredentialsTTL = "12h";
58 Secret = "not-secure-secret";
59 TimeBasedCredentials = false;
60 };
61
62 Signal = {
63 Proto = "https";
64 URI = "${cfg.domain}:443";
65 Username = "";
66 Password = null;
67 };
68
69 ReverseProxy = {
70 TrustedHTTPProxies = [ ];
71 TrustedHTTPProxiesCount = 0;
72 TrustedPeers = [ "0.0.0.0/0" ];
73 };
74
75 Datadir = "${stateDir}/data";
76 DataStoreEncryptionKey = "very-insecure-key";
77 StoreConfig = {
78 Engine = "sqlite";
79 };
80
81 HttpConfig = {
82 Address = "127.0.0.1:${builtins.toString cfg.port}";
83 IdpSignKeyRefreshEnabled = true;
84 OIDCConfigEndpoint = cfg.oidcConfigEndpoint;
85 };
86
87 IdpManagerConfig = {
88 ManagerType = "none";
89 ClientConfig = {
90 Issuer = "";
91 TokenEndpoint = "";
92 ClientID = "netbird";
93 ClientSecret = "";
94 GrantType = "client_credentials";
95 };
96
97 ExtraConfig = { };
98 Auth0ClientCredentials = null;
99 AzureClientCredentials = null;
100 KeycloakClientCredentials = null;
101 ZitadelClientCredentials = null;
102 };
103
104 DeviceAuthorizationFlow = {
105 Provider = "none";
106 ProviderConfig = {
107 Audience = "netbird";
108 Domain = null;
109 ClientID = "netbird";
110 TokenEndpoint = null;
111 DeviceAuthEndpoint = "";
112 Scope = "openid profile email";
113 UseIDToken = false;
114 };
115 };
116
117 PKCEAuthorizationFlow = {
118 ProviderConfig = {
119 Audience = "netbird";
120 ClientID = "netbird";
121 ClientSecret = "";
122 AuthorizationEndpoint = "";
123 TokenEndpoint = "";
124 Scope = "openid profile email";
125 RedirectURLs = [ "http://localhost:53000" ];
126 UseIDToken = false;
127 };
128 };
129 };
130
131 managementConfig = recursiveUpdate defaultSettings cfg.settings;
132
133 managementFile = settingsFormat.generate "config.json" managementConfig;
134
135 cfg = config.services.netbird.server.management;
136in
137
138{
139 options.services.netbird.server.management = {
140 enable = mkEnableOption "Netbird Management Service";
141
142 package = mkPackageOption pkgs "netbird" { };
143
144 domain = mkOption {
145 type = str;
146 description = "The domain under which the management API runs.";
147 };
148
149 turnDomain = mkOption {
150 type = str;
151 description = "The domain of the TURN server to use.";
152 };
153
154 turnPort = mkOption {
155 type = port;
156 default = 3478;
157 description = ''
158 The port of the TURN server to use.
159 '';
160 };
161
162 dnsDomain = mkOption {
163 type = str;
164 default = "netbird.selfhosted";
165 description = "Domain used for peer resolution.";
166 };
167
168 singleAccountModeDomain = mkOption {
169 type = str;
170 default = "netbird.selfhosted";
171 description = ''
172 Enables single account mode.
173 This means that all the users will be under the same account grouped by the specified domain.
174 If the installation has more than one account, the property is ineffective.
175 '';
176 };
177
178 disableAnonymousMetrics = mkOption {
179 type = bool;
180 default = true;
181 description = "Disables push of anonymous usage metrics to NetBird.";
182 };
183
184 disableSingleAccountMode = mkOption {
185 type = bool;
186 default = false;
187 description = ''
188 If set to true, disables single account mode.
189 The `singleAccountModeDomain` property will be ignored and every new user will have a separate NetBird account.
190 '';
191 };
192
193 port = mkOption {
194 type = port;
195 default = 8011;
196 description = "Internal port of the management server.";
197 };
198
199 metricsPort = mkOption {
200 type = port;
201 default = 9090;
202 description = "Internal port of the metrics server.";
203 };
204
205 extraOptions = mkOption {
206 type = listOf str;
207 default = [ ];
208 description = ''
209 Additional options given to netbird-mgmt as commandline arguments.
210 '';
211 };
212
213 oidcConfigEndpoint = mkOption {
214 type = str;
215 description = "The oidc discovery endpoint.";
216 example = "https://example.eu.auth0.com/.well-known/openid-configuration";
217 };
218
219 settings = mkOption {
220 inherit (settingsFormat) type;
221
222 defaultText = literalExpression ''
223 defaultSettings = {
224 Stuns = [
225 {
226 Proto = "udp";
227 URI = "stun:''${cfg.turnDomain}:3478";
228 Username = "";
229 Password = null;
230 }
231 ];
232
233 TURNConfig = {
234 Turns = [
235 {
236 Proto = "udp";
237 URI = "turn:''${cfg.turnDomain}:3478";
238 Username = "netbird";
239 Password = "netbird";
240 }
241 ];
242
243 CredentialsTTL = "12h";
244 Secret = "not-secure-secret";
245 TimeBasedCredentials = false;
246 };
247
248 Signal = {
249 Proto = "https";
250 URI = "''${cfg.domain}:443";
251 Username = "";
252 Password = null;
253 };
254
255 ReverseProxy = {
256 TrustedHTTPProxies = [ ];
257 TrustedHTTPProxiesCount = 0;
258 TrustedPeers = [ "0.0.0.0/0" ];
259 };
260
261 Datadir = "''${stateDir}/data";
262 DataStoreEncryptionKey = "genEVP6j/Yp2EeVujm0zgqXrRos29dQkpvX0hHdEUlQ=";
263 StoreConfig = { Engine = "sqlite"; };
264
265 HttpConfig = {
266 Address = "127.0.0.1:''${builtins.toString cfg.port}";
267 IdpSignKeyRefreshEnabled = true;
268 OIDCConfigEndpoint = cfg.oidcConfigEndpoint;
269 };
270
271 IdpManagerConfig = {
272 ManagerType = "none";
273 ClientConfig = {
274 Issuer = "";
275 TokenEndpoint = "";
276 ClientID = "netbird";
277 ClientSecret = "";
278 GrantType = "client_credentials";
279 };
280
281 ExtraConfig = { };
282 Auth0ClientCredentials = null;
283 AzureClientCredentials = null;
284 KeycloakClientCredentials = null;
285 ZitadelClientCredentials = null;
286 };
287
288 DeviceAuthorizationFlow = {
289 Provider = "none";
290 ProviderConfig = {
291 Audience = "netbird";
292 Domain = null;
293 ClientID = "netbird";
294 TokenEndpoint = null;
295 DeviceAuthEndpoint = "";
296 Scope = "openid profile email offline_access api";
297 UseIDToken = false;
298 };
299 };
300
301 PKCEAuthorizationFlow = {
302 ProviderConfig = {
303 Audience = "netbird";
304 ClientID = "netbird";
305 ClientSecret = "";
306 AuthorizationEndpoint = "";
307 TokenEndpoint = "";
308 Scope = "openid profile email offline_access api";
309 RedirectURLs = "http://localhost:53000";
310 UseIDToken = false;
311 };
312 };
313 };
314 '';
315
316 default = { };
317
318 description = ''
319 Configuration of the netbird management server.
320 Options containing secret data should be set to an attribute set containing the attribute _secret
321 - a string pointing to a file containing the value the option should be set to.
322 See the example to get a better picture of this: in the resulting management.json file,
323 the `DataStoreEncryptionKey` key will be set to the contents of the /run/agenix/netbird_mgmt-data_store_encryption_key file.
324 '';
325
326 example = {
327 DataStoreEncryptionKey = {
328 _secret = "/run/agenix/netbird_mgmt-data_store_encryption_key";
329 };
330 };
331 };
332
333 logLevel = mkOption {
334 type = enum [
335 "ERROR"
336 "WARN"
337 "INFO"
338 "DEBUG"
339 ];
340 default = "INFO";
341 description = "Log level of the netbird services.";
342 };
343
344 enableNginx = mkEnableOption "Nginx reverse-proxy for the netbird management service";
345 };
346
347 config = mkIf cfg.enable {
348 warnings =
349 concatMap
350 (
351 { check, name }:
352 optional check "${name} is world-readable in the Nix Store, you should provide it as a _secret."
353 )
354 [
355 {
356 check = builtins.isString managementConfig.TURNConfig.Secret;
357 name = "The TURNConfig.secret";
358 }
359 {
360 check = builtins.isString managementConfig.DataStoreEncryptionKey;
361 name = "The DataStoreEncryptionKey";
362 }
363 {
364 check = any (T: (T ? Password) && builtins.isString T.Password) managementConfig.TURNConfig.Turns;
365 name = "A Turn configuration's password";
366 }
367 ];
368
369 assertions = [
370 {
371 assertion = cfg.port != cfg.metricsPort;
372 message = "The primary listen port cannot be the same as the listen port for the metrics endpoint";
373 }
374 ];
375
376 systemd.services.netbird-management = {
377 description = "The management server for Netbird, a wireguard VPN";
378 documentation = [ "https://netbird.io/docs/" ];
379
380 after = [ "network.target" ];
381 wantedBy = [ "multi-user.target" ];
382 restartTriggers = [ managementFile ];
383
384 preStart = genJqSecretsReplacementSnippet managementConfig "${stateDir}/management.json";
385
386 serviceConfig = {
387 ExecStart = escapeSystemdExecArgs (
388 [
389 (getExe' cfg.package "netbird-mgmt")
390 "management"
391 # Config file
392 "--config"
393 "${stateDir}/management.json"
394 # Data directory
395 "--datadir"
396 "${stateDir}/data"
397 # DNS domain
398 "--dns-domain"
399 cfg.dnsDomain
400 # Port to listen on
401 "--port"
402 cfg.port
403 # Port the internal prometheus server listens on
404 "--metrics-port"
405 cfg.metricsPort
406 # Log to stdout
407 "--log-file"
408 "console"
409 # Log level
410 "--log-level"
411 cfg.logLevel
412 #
413 "--idp-sign-key-refresh-enabled"
414 # Domain for internal resolution
415 "--single-account-mode-domain"
416 cfg.singleAccountModeDomain
417 ]
418 ++ (optional cfg.disableAnonymousMetrics "--disable-anonymous-metrics")
419 ++ (optional cfg.disableSingleAccountMode "--disable-single-account-mode")
420 ++ cfg.extraOptions
421 );
422 Restart = "always";
423 RuntimeDirectory = "netbird-mgmt";
424 StateDirectory = [
425 "netbird-mgmt"
426 "netbird-mgmt/data"
427 ];
428 StateDirectoryMode = "0750";
429 RuntimeDirectoryMode = "0750";
430 WorkingDirectory = stateDir;
431
432 # hardening
433 LockPersonality = true;
434 MemoryDenyWriteExecute = true;
435 NoNewPrivileges = true;
436 PrivateMounts = true;
437 PrivateTmp = true;
438 ProtectClock = true;
439 ProtectControlGroups = true;
440 ProtectHome = true;
441 ProtectHostname = true;
442 ProtectKernelLogs = true;
443 ProtectKernelModules = true;
444 ProtectKernelTunables = true;
445 ProtectSystem = true;
446 RemoveIPC = true;
447 RestrictNamespaces = true;
448 RestrictRealtime = true;
449 RestrictSUIDSGID = true;
450 };
451
452 stopIfChanged = false;
453 };
454
455 services.nginx = mkIf cfg.enableNginx {
456 enable = true;
457
458 virtualHosts.${cfg.domain} = {
459 locations = {
460 "/api".proxyPass = "http://localhost:${builtins.toString cfg.port}";
461
462 "/management.ManagementService/".extraConfig = ''
463 # This is necessary so that grpc connections do not get closed early
464 # see https://stackoverflow.com/a/67805465
465 client_body_timeout 1d;
466
467 grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
468
469 grpc_pass grpc://localhost:${builtins.toString cfg.port};
470 grpc_read_timeout 1d;
471 grpc_send_timeout 1d;
472 grpc_socket_keepalive on;
473 '';
474 };
475 };
476 };
477 };
478}