diff --git a/maintainers/maintainer-list.nix b/maintainers/maintainer-list.nix index 6bcd9116a8f20..6dbe2b7391c86 100644 --- a/maintainers/maintainer-list.nix +++ b/maintainers/maintainer-list.nix @@ -25089,6 +25089,12 @@ github = "shunueda"; githubId = 62182668; }; + shuuri-labs = { + name = "Ashley Mensah"; + email = "ashley@netbird.io"; + github = "shuuri-labs"; + githubId = 61762328; + }; shved = { name = "Yury Shvedov"; email = "mestofel13@gmail.com"; diff --git a/nixos/doc/manual/redirects.json b/nixos/doc/manual/redirects.json index 2cad40493dc51..c8fb80a19d781 100644 --- a/nixos/doc/manual/redirects.json +++ b/nixos/doc/manual/redirects.json @@ -1202,6 +1202,41 @@ "module-services-netbird-customization": [ "index.html#module-services-netbird-customization" ], + "module-services-netbird-features": [ + "index.html#module-services-netbird-features", + "index.html#module-services-netbird-dns", + "index.html#module-services-netbird-routing", + "index.html#module-services-netbird-security", + "index.html#module-services-netbird-rosenpass", + "index.html#module-services-netbird-ssh", + "index.html#module-services-netbird-connection", + "index.html#module-services-netbird-selfhosted", + "index.html#module-services-netbird-advanced" + ], + "module-services-netbird-server-quickstart-coturn": [ + "index.html#module-services-netbird-server-quickstart-coturn" + ], + "module-services-netbird-server-quickstart-relay": [ + "index.html#module-services-netbird-server-quickstart-relay" + ], + "module-services-netbird-server-relay-vs-coturn": [ + "index.html#module-services-netbird-server-relay-vs-coturn" + ], + "module-services-netbird-server-embedded-idp": [ + "index.html#module-services-netbird-server-embedded-idp" + ], + "module-services-netbird-server-database": [ + "index.html#module-services-netbird-server-database" + ], + "module-services-netbird-server-database-postgres": [ + "index.html#module-services-netbird-server-database-postgres" + ], + "module-services-netbird-server-relay-config": [ + "index.html#module-services-netbird-server-relay-config" + ], + "module-services-netbird-server-complete-example": [ + "index.html#module-services-netbird-server-complete-example" + ], "module-services-mosquitto": [ "index.html#module-services-mosquitto" ], diff --git a/nixos/modules/services/networking/netbird.md b/nixos/modules/services/networking/netbird.md index a1aab94473a31..be4c007ff4754 100644 --- a/nixos/modules/services/networking/netbird.md +++ b/nixos/modules/services/networking/netbird.md @@ -89,3 +89,36 @@ See the option description for more information. [environment](#opt-services.netbird.clients._name_.environment) allows you to pass additional configurations through environment variables, but special care needs to be taken for overriding config location and daemon address due [hardened](#opt-services.netbird.clients._name_.hardened) option. + +## Feature Configuration {#module-services-netbird-features} + +NetBird features (DNS, routing, SSH, Rosenpass, etc.) are configured via `NB_*` environment variables +using the [extraEnvironment](#opt-services.netbird.clients._name_.extraEnvironment) option. +Settings that affect the [config.json](#opt-services.netbird.clients._name_.config) (e.g. MTU, management URL) +can be set via the `config` option. + +```nix +{ + services.netbird.clients.work = { + port = 51820; + + # Feature flags via environment variables + extraEnvironment = { + NB_DISABLE_DNS = "true"; + NB_ALLOW_SERVER_SSH = "true"; + NB_ENABLE_ROSENPASS = "true"; + NB_HOSTNAME = "my-peer"; + }; + + # Config.json overrides + config = { + ManagementURL = "https://management.example.com:443"; + Mtu = 1280; + }; + }; +} +``` + +The NetBird client reads its full set of `NB_*` flags via `setFlagsFromEnvVars()`. +Consult the [upstream source](https://github.com/netbirdio/netbird/blob/main/client/internal/connect.go) +for the complete list of supported variables. diff --git a/nixos/modules/services/networking/netbird.nix b/nixos/modules/services/networking/netbird.nix index 368e3d9013f18..7c75b021418e6 100644 --- a/nixos/modules/services/networking/netbird.nix +++ b/nixos/modules/services/networking/netbird.nix @@ -180,6 +180,27 @@ in ''; }; + extraEnvironment = mkOption { + type = attrsOf str; + default = { }; + example = literalExpression '' + { + NB_DISABLE_DNS = "true"; + NB_ALLOW_SERVER_SSH = "true"; + NB_ENABLE_ROSENPASS = "true"; + } + ''; + description = '' + Additional environment variables to pass to the NetBird service. + + NetBird features are configured via `NB_*` environment variables + (e.g. `NB_DISABLE_DNS`, `NB_ALLOW_SERVER_SSH`, `NB_ENABLE_ROSENPASS`). + + These are merged with the computed environment variables, with + values from this option taking precedence on conflicts. + ''; + }; + interface = mkOption { type = str; default = "nb-${client.name}"; @@ -208,6 +229,7 @@ in } // optionalAttrs (client.dns-resolver.address != null) { NB_DNS_RESOLVER_ADDRESS = "''${client.dns-resolver.address}:''${toString client.dns-resolver.port}"; } + // client.extraEnvironment ''; description = '' Environment for the netbird service, used to pass configuration options. @@ -275,10 +297,7 @@ in - `CAP_NET_RAW`, `CAP_NET_ADMIN` and `CAP_BPF` still give unlimited network manipulation possibilites, - older kernels don't have `CAP_BPF` and use `CAP_SYS_ADMIN` instead, - Known security features that are not (yet) integrated into the module: - - 2024-02-14: `rosenpass` is an experimental feature configurable solely - through `--enable-rosenpass` flag on the `netbird up` command, - see [the docs](https://docs.netbird.io/how-to/enable-post-quantum-cryptography) + For post-quantum cryptography, set `NB_ENABLE_ROSENPASS = "true"` in `extraEnvironment`. ''; }; @@ -447,7 +466,8 @@ in } // optionalAttrs (client.dns-resolver.address != null) { NB_DNS_RESOLVER_ADDRESS = "${client.dns-resolver.address}:${toString client.dns-resolver.port}"; - }; + } + // client.extraEnvironment; config.config = { DisableAutoConnect = !client.autoStart; diff --git a/nixos/modules/services/networking/netbird/management.nix b/nixos/modules/services/networking/netbird/management.nix index 8d65e2e20aa62..a49922335ef9a 100644 --- a/nixos/modules/services/networking/netbird/management.nix +++ b/nixos/modules/services/networking/netbird/management.nix @@ -17,6 +17,8 @@ let mkOption mkPackageOption optional + optionals + optionalAttrs recursiveUpdate ; @@ -24,6 +26,8 @@ let bool enum listOf + nullOr + path port str ; @@ -59,6 +63,12 @@ let TimeBasedCredentials = false; }; + Relay = { + Addresses = cfg.relayAddresses; + CredentialsTTL = "24h"; + Secret = if cfg.relaySecretFile != null then { _secret = cfg.relaySecretFile; } else ""; + }; + Signal = { Proto = "https"; URI = "${cfg.domain}:443"; @@ -75,7 +85,17 @@ let Datadir = "${stateDir}/data"; DataStoreEncryptionKey = "very-insecure-key"; StoreConfig = { - Engine = "sqlite"; + Engine = cfg.store.engine; + } + // optionalAttrs (cfg.store.engine == "postgres" && cfg.store.postgres.dsnFile != null) { + DataSourcePath = { + _secret = cfg.store.postgres.dsnFile; + }; + } + // optionalAttrs (cfg.store.engine == "mysql" && cfg.store.mysql.dsnFile != null) { + DataSourcePath = { + _secret = cfg.store.mysql.dsnFile; + }; }; HttpConfig = { @@ -87,13 +107,12 @@ let IdpManagerConfig = { ManagerType = "none"; ClientConfig = { - Issuer = ""; + Issuer = if cfg.idp.embedded.enable then "https://${cfg.domain}/oauth2" else ""; TokenEndpoint = ""; ClientID = "netbird"; ClientSecret = ""; GrantType = "client_credentials"; }; - ExtraConfig = { }; Auth0ClientCredentials = null; AzureClientCredentials = null; @@ -126,6 +145,30 @@ let UseIDToken = false; }; }; + } + // optionalAttrs cfg.idp.embedded.enable { + EmbeddedIdP = { + Enabled = true; + Issuer = "https://${cfg.domain}/oauth2"; + LocalAddress = "127.0.0.1:${toString cfg.port}"; + Storage = { + Type = "sqlite3"; + Config.File = "${stateDir}/idp.db"; + }; + DashboardRedirectURIs = [ + "https://${cfg.domain}/nb-auth" + "https://${cfg.domain}/nb-silent-auth" + ]; + CLIRedirectURIs = [ + "http://localhost:53000/" + "http://localhost:54000/" + ]; + Owner = { + Email = ""; + Password = ""; + Username = ""; + }; + }; }; managementConfig = recursiveUpdate defaultSettings cfg.settings; @@ -136,6 +179,33 @@ let in { + imports = [ + (lib.mkRenamedOptionModule + [ + "services" + "netbird" + "server" + "management" + "singleAccountModeDomain" + ] + [ + "services" + "netbird" + "server" + "management" + "singleAccountMode" + "domain" + ] + ) + (lib.mkRemovedOptionModule [ + "services" + "netbird" + "server" + "management" + "disableSingleAccountMode" + ] "Use services.netbird.server.management.singleAccountMode.enable = false instead.") + ]; + options.services.netbird.server.management = { enable = mkEnableOption "Netbird Management Service"; @@ -165,14 +235,24 @@ in description = "Domain used for peer resolution."; }; - singleAccountModeDomain = mkOption { - type = str; - default = "netbird.selfhosted"; - description = '' - Enables single account mode. - This means that all the users will be under the same account grouped by the specified domain. - If the installation has more than one account, the property is ineffective. - ''; + singleAccountMode = { + enable = mkOption { + type = bool; + default = true; + description = '' + Enable single account mode where all users are grouped under a single account. + If the installation already has more than one account, this setting is ineffective. + ''; + }; + + domain = mkOption { + type = str; + default = "netbird.selfhosted"; + description = '' + Domain used to group users in single account mode. + Only used when `singleAccountMode.enable` is true. + ''; + }; }; disableAnonymousMetrics = mkOption { @@ -181,15 +261,6 @@ in description = "Disables push of anonymous usage metrics to NetBird."; }; - disableSingleAccountMode = mkOption { - type = bool; - default = false; - description = '' - If set to true, disables single account mode. - The `singleAccountModeDomain` property will be ignored and every new user will have a separate NetBird account. - ''; - }; - port = mkOption { type = port; default = 8011; @@ -212,10 +283,105 @@ in oidcConfigEndpoint = mkOption { type = str; - description = "The oidc discovery endpoint."; + default = ""; + description = "The oidc discovery endpoint. Not required when using embedded IDP."; example = "https://example.eu.auth0.com/.well-known/openid-configuration"; }; + # Relay configuration + relayAddresses = mkOption { + type = listOf str; + default = [ ]; + description = '' + List of relay server addresses to advertise to clients. + ''; + example = [ "rels://relay.example.com:443" ]; + }; + + relaySecretFile = mkOption { + type = nullOr path; + default = null; + description = '' + Path to file containing the shared secret for relay authentication. + This must match the auth-secret configured on the relay server. + ''; + }; + + # TLS configuration + tls = { + enable = mkEnableOption "TLS for the management server"; + + letsencrypt = { + domain = mkOption { + type = nullOr str; + default = null; + description = '' + Domain for automatic Let's Encrypt certificate. + When set, the management server will automatically obtain and renew certificates. + ''; + }; + }; + + certFile = mkOption { + type = nullOr path; + default = null; + description = "Path to the TLS certificate file."; + }; + + certKey = mkOption { + type = nullOr path; + default = null; + description = "Path to the TLS certificate key file."; + }; + }; + + # Embedded IDP + idp.embedded.enable = mkEnableOption '' + the embedded identity provider. + When enabled, configures the EmbeddedIdP section and provides + default EmbeddedIdP values derived from the domain. + Customize the embedded IDP via the `settings` freeform option + (e.g. `settings.EmbeddedIdP.Owner.Email = "admin@example.com"`) + ''; + + # Database backend configuration + store = { + engine = mkOption { + type = enum [ + "sqlite" + "postgres" + "mysql" + ]; + default = "sqlite"; + description = '' + Database engine for the management server. + Use postgres or mysql for larger deployments. + ''; + }; + + postgres = { + dsnFile = mkOption { + type = nullOr path; + default = null; + description = '' + Path to file containing the PostgreSQL connection DSN. + Example content: postgres://user:password@localhost:5432/netbird?sslmode=disable + ''; + }; + }; + + mysql = { + dsnFile = mkOption { + type = nullOr path; + default = null; + description = '' + Path to file containing the MySQL connection DSN. + Example content: user:password@tcp(localhost:3306)/netbird + ''; + }; + }; + }; + settings = mkOption { inherit (settingsFormat) type; @@ -245,6 +411,12 @@ in TimeBasedCredentials = false; }; + Relay = { + Addresses = cfg.relayAddresses; + CredentialsTTL = "24h"; + Secret = ""; + }; + Signal = { Proto = "https"; URI = "''${cfg.domain}:443"; @@ -259,7 +431,7 @@ in }; Datadir = "''${stateDir}/data"; - DataStoreEncryptionKey = "genEVP6j/Yp2EeVujm0zgqXrRos29dQkpvX0hHdEUlQ="; + DataStoreEncryptionKey = "very-insecure-key"; StoreConfig = { Engine = "sqlite"; }; HttpConfig = { @@ -293,7 +465,7 @@ in ClientID = "netbird"; TokenEndpoint = null; DeviceAuthEndpoint = ""; - Scope = "openid profile email offline_access api"; + Scope = "openid profile email"; UseIDToken = false; }; }; @@ -305,8 +477,8 @@ in ClientSecret = ""; AuthorizationEndpoint = ""; TokenEndpoint = ""; - Scope = "openid profile email offline_access api"; - RedirectURLs = "http://localhost:53000"; + Scope = "openid profile email"; + RedirectURLs = [ "http://localhost:53000" ]; UseIDToken = false; }; }; @@ -354,7 +526,7 @@ in [ { check = builtins.isString managementConfig.TURNConfig.Secret; - name = "The TURNConfig.secret"; + name = "The TURNConfig.Secret"; } { check = builtins.isString managementConfig.DataStoreEncryptionKey; @@ -362,7 +534,14 @@ in } { check = any (T: (T ? Password) && builtins.isString T.Password) managementConfig.TURNConfig.Turns; - name = "A Turn configuration's password"; + name = "A TURNConfig.Turns password"; + } + { + check = + cfg.relayAddresses != [ ] + && managementConfig ? Relay + && builtins.isString (managementConfig.Relay.Secret or ""); + name = "The Relay.Secret"; } ]; @@ -371,6 +550,32 @@ in assertion = cfg.port != cfg.metricsPort; message = "The primary listen port cannot be the same as the listen port for the metrics endpoint"; } + { + assertion = + cfg.tls.enable + -> (cfg.tls.letsencrypt.domain != null || (cfg.tls.certFile != null && cfg.tls.certKey != null)); + message = "When TLS is enabled, either letsencrypt.domain or both certFile and certKey must be set"; + } + { + assertion = cfg.tls.certFile != null -> cfg.tls.certKey != null; + message = "certKey must be set when certFile is set"; + } + { + assertion = cfg.tls.certKey != null -> cfg.tls.certFile != null; + message = "certFile must be set when certKey is set"; + } + { + assertion = cfg.store.engine == "postgres" -> cfg.store.postgres.dsnFile != null; + message = "store.postgres.dsnFile must be set when using postgres engine"; + } + { + assertion = cfg.store.engine == "mysql" -> cfg.store.mysql.dsnFile != null; + message = "store.mysql.dsnFile must be set when using mysql engine"; + } + { + assertion = !cfg.idp.embedded.enable || cfg.oidcConfigEndpoint == ""; + message = "oidcConfigEndpoint should not be set when using embedded IDP"; + } ]; systemd.services.netbird-management = { @@ -388,35 +593,44 @@ in [ (getExe' cfg.package "netbird-mgmt") "management" - # Config file "--config" "${stateDir}/management.json" - # Data directory "--datadir" "${stateDir}/data" - # DNS domain "--dns-domain" cfg.dnsDomain - # Port to listen on "--port" cfg.port - # Port the internal prometheus server listens on "--metrics-port" cfg.metricsPort - # Log to stdout "--log-file" "console" - # Log level "--log-level" cfg.logLevel - # "--idp-sign-key-refresh-enabled" - # Domain for internal resolution + ] + # Single account mode + ++ optionals cfg.singleAccountMode.enable [ "--single-account-mode-domain" - cfg.singleAccountModeDomain + cfg.singleAccountMode.domain ] + ++ (optional (!cfg.singleAccountMode.enable) "--disable-single-account-mode") ++ (optional cfg.disableAnonymousMetrics "--disable-anonymous-metrics") - ++ (optional cfg.disableSingleAccountMode "--disable-single-account-mode") + # Always disable GeoLite updates for self-hosted (privacy default) + ++ [ "--disable-geolite-update" ] + # TLS options + ++ optionals (cfg.tls.letsencrypt.domain != null) [ + "--letsencrypt-domain" + cfg.tls.letsencrypt.domain + ] + ++ optionals (cfg.tls.certFile != null) [ + "--cert-file" + cfg.tls.certFile + ] + ++ optionals (cfg.tls.certKey != null) [ + "--cert-key" + cfg.tls.certKey + ] ++ cfg.extraOptions ); Restart = "always"; @@ -442,7 +656,7 @@ in ProtectKernelLogs = true; ProtectKernelModules = true; ProtectKernelTunables = true; - ProtectSystem = true; + ProtectSystem = "strict"; RemoveIPC = true; RestrictNamespaces = true; RestrictRealtime = true; diff --git a/nixos/modules/services/networking/netbird/relay.nix b/nixos/modules/services/networking/netbird/relay.nix new file mode 100644 index 0000000000000..56a2109d98caa --- /dev/null +++ b/nixos/modules/services/networking/netbird/relay.nix @@ -0,0 +1,249 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) + concatStringsSep + escapeShellArgs + getExe' + mkEnableOption + mkIf + mkMerge + mkOption + mkPackageOption + optionals + ; + + inherit (lib.types) + bool + listOf + nullOr + enum + path + port + str + ; + + cfg = config.services.netbird.server.relay; + stateDir = "/var/lib/netbird-relay"; +in + +{ + options.services.netbird.server.relay = { + enable = mkEnableOption "NetBird Relay Server"; + + package = mkPackageOption pkgs "netbird-relay" { }; + + port = mkOption { + type = port; + default = 33080; + description = '' + Port the relay server listens on. + When behind nginx (enableNginx), this is the internal port that nginx proxies to. + ''; + }; + + exposedAddress = mkOption { + type = str; + description = '' + The public URL where clients can reach this relay server. + This is advertised to clients via the management server. + ''; + example = "rels://relay.example.com:443"; + }; + + authSecretFile = mkOption { + type = path; + description = '' + Path to a file containing the relay authentication secret. + The file should contain only the raw secret value. + This must match the relaySecretFile configured in the management server. + ''; + }; + + logLevel = mkOption { + type = enum [ + "panic" + "fatal" + "error" + "warn" + "info" + "debug" + "trace" + ]; + default = "info"; + description = "Log level for the relay server."; + }; + + stun = { + enable = mkOption { + type = bool; + default = true; + description = '' + Enable the embedded STUN server. + This provides STUN functionality alongside the relay server. + ''; + }; + + ports = mkOption { + type = listOf port; + default = [ 3478 ]; + description = "UDP ports for the embedded STUN server."; + }; + }; + + openFirewall = mkOption { + type = bool; + default = false; + description = '' + Whether to open the relay and STUN ports in the firewall. + ''; + }; + + enableNginx = mkEnableOption "Nginx reverse-proxy for the relay server"; + + domain = mkOption { + type = nullOr str; + default = null; + description = "Domain name for nginx virtual host configuration."; + }; + + metricsPort = mkOption { + type = port; + default = 9092; + description = "Port for the relay metrics endpoint."; + }; + + extraOptions = mkOption { + type = listOf str; + default = [ ]; + description = '' + Additional command-line options passed to the relay server. + Use this for advanced settings like TLS configuration + (e.g. `["--tls-cert-file" "/path/to/cert" "--tls-key-file" "/path/to/key"]`). + ''; + }; + }; + + config = mkIf cfg.enable (mkMerge [ + { + assertions = [ + { + assertion = cfg.enableNginx -> cfg.domain != null; + message = "domain must be set when enableNginx is true"; + } + ]; + + systemd.services.netbird-relay = { + description = "NetBird Relay Server"; + documentation = [ "https://docs.netbird.io/" ]; + + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + restartTriggers = [ + cfg.port + cfg.logLevel + cfg.exposedAddress + cfg.stun.enable + ]; + + serviceConfig = { + LoadCredential = [ "auth-secret:${cfg.authSecretFile}" ]; + + ExecStart = + let + args = [ + (getExe' cfg.package "netbird-relay") + "--listen-address" + ":${toString cfg.port}" + "--exposed-address" + cfg.exposedAddress + "--log-level" + cfg.logLevel + "--log-file" + "console" + ] + ++ [ + "--metrics-port" + (toString cfg.metricsPort) + ] + ++ optionals cfg.stun.enable [ + "--enable-stun" + "--stun-ports" + (concatStringsSep "," (map toString cfg.stun.ports)) + ] + ++ cfg.extraOptions; + in + "${pkgs.writeShellScript "netbird-relay" '' + export NB_AUTH_SECRET=$(< "$CREDENTIALS_DIRECTORY/auth-secret") + exec ${escapeShellArgs args} + ''}"; + + Restart = "always"; + RuntimeDirectory = "netbird-relay"; + RuntimeDirectoryMode = "0750"; + StateDirectory = "netbird-relay"; + StateDirectoryMode = "0750"; + WorkingDirectory = stateDir; + DynamicUser = true; + + # hardening + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateMounts = true; + PrivateTmp = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectSystem = "strict"; + RemoveIPC = true; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + + # Relay may need to bind to privileged ports when not behind nginx + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; + CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; + }; + + stopIfChanged = false; + }; + } + + (mkIf cfg.openFirewall { + networking.firewall = { + allowedTCPPorts = [ cfg.port ]; + allowedUDPPorts = mkIf cfg.stun.enable cfg.stun.ports; + }; + }) + + (mkIf cfg.enableNginx { + services.nginx = { + enable = true; + + virtualHosts.${cfg.domain} = { + locations."/relay".extraConfig = '' + proxy_pass http://127.0.0.1:${toString cfg.port}; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 86400; + ''; + }; + }; + }) + ]); +} diff --git a/nixos/modules/services/networking/netbird/server.md b/nixos/modules/services/networking/netbird/server.md index 1de251b80109a..a7efc9233e50a 100644 --- a/nixos/modules/services/networking/netbird/server.md +++ b/nixos/modules/services/networking/netbird/server.md @@ -4,9 +4,11 @@ NetBird is a VPN built on top of WireGuard® making it easy to create secure pri ## Quickstart {#module-services-netbird-server-quickstart} -To fully setup Netbird as a self-hosted server, we need both a Coturn server and an identity provider, the list of supported SSOs and their setup are available [on Netbird's documentation](https://docs.netbird.io/selfhosted/selfhosted-guide#step-3-configure-identity-provider-idp). +To fully setup Netbird as a self-hosted server, you need an identity provider (or use the embedded IDP) and either a Coturn server or the modern relay server. The list of supported SSOs and their setup are available [on Netbird's documentation](https://docs.netbird.io/selfhosted/selfhosted-guide#step-3-configure-identity-provider-idp). -There are quite a few settings that need to be passed to Netbird for it to function, and a minimal config looks like : +### Minimal Configuration with Coturn {#module-services-netbird-server-quickstart-coturn} + +This is the traditional setup using Coturn as the TURN server: ```nix { @@ -19,7 +21,6 @@ There are quite a few settings that need to be passed to Netbird for it to funct coturn = { enable = true; - passwordFile = "/path/to/a/secret/password"; }; @@ -42,3 +43,172 @@ There are quite a few settings that need to be passed to Netbird for it to funct }; } ``` + +### Modern Setup with Relay Server {#module-services-netbird-server-quickstart-relay} + +NetBird v0.28+ introduced a modern relay server that replaces Coturn with better performance and simpler configuration. The relay server includes an embedded STUN server. + +```nix +{ + services.netbird.server = { + enable = true; + + domain = "netbird.example.selfhosted"; + + enableNginx = true; + + # Use the modern relay instead of Coturn + useRelay = true; + relayAuthSecretFile = "/run/secrets/relay-auth"; + + management = { + oidcConfigEndpoint = "https://sso.example.selfhosted/oauth2/openid/netbird/.well-known/openid-configuration"; + }; + }; +} +``` + +## Relay vs Coturn {#module-services-netbird-server-relay-vs-coturn} + +| Feature | Relay Server | Coturn | +|---------|--------------|--------| +| Protocol | WebSocket/HTTP(S) | TURN (UDP/TCP) | +| Firewall | Single port (443) | Multiple ports + UDP range | +| Setup | Simple | More complex | +| Embedded STUN | Yes | No (separate config) | +| Performance | Optimized for NetBird | General-purpose | + +**Recommendation:** Use the relay server for new deployments. Only use Coturn if you have specific requirements for standard TURN protocol compatibility. + +## Embedded Identity Provider {#module-services-netbird-server-embedded-idp} + +NetBird supports an embedded identity provider for simplified deployments that don't require an external SSO. Enable it with `idp.embedded.enable`, then customize via the freeform `settings` option: + +```nix +{ + services.netbird.server.management = { + enable = true; + domain = "netbird.example.com"; + turnDomain = "netbird.example.com"; + + idp.embedded.enable = true; + + settings = { + ProviderConfig = { + Owner = { + Email = "admin@example.com"; + Username = "admin"; + # Generate with: htpasswd -bnBC 10 "" 'your-password' | tr -d ':\n' + Password._secret = "/run/secrets/admin-password-hash"; + }; + }; + }; + }; +} +``` + +## Database Backends {#module-services-netbird-server-database} + +By default, the management server uses SQLite. For larger deployments, PostgreSQL or MySQL is recommended. + +### PostgreSQL {#module-services-netbird-server-database-postgres} + +```nix +{ + services.netbird.server.management = { + store = { + engine = "postgres"; + postgres.dsnFile = "/run/secrets/postgres-dsn"; + }; + }; + + # Example DSN file content: + # postgres://netbird:password@localhost:5432/netbird?sslmode=disable + + services.postgresql = { + enable = true; + ensureDatabases = [ "netbird" ]; + ensureUsers = [ + { + name = "netbird"; + ensureDBOwnership = true; + } + ]; + }; +} +``` + +## Relay Server Configuration {#module-services-netbird-server-relay-config} + +The relay server can be configured independently. Advanced TLS settings (Let's Encrypt, custom certificates) can be passed via `extraOptions`: + +```nix +{ + services.netbird.server.relay = { + enable = true; + exposedAddress = "rels://relay.example.com:443"; + authSecretFile = "/run/secrets/relay-auth"; + + stun = { + enable = true; + ports = [ 3478 ]; + }; + + openFirewall = true; + + # For direct TLS (without nginx reverse proxy): + extraOptions = [ + "--tls-cert-file" + "/path/to/cert.pem" + "--tls-key-file" + "/path/to/key.pem" + ]; + }; +} +``` + +## Complete Self-Hosted Example {#module-services-netbird-server-complete-example} + +Here's a complete example using the modern relay server with an external identity provider: + +```nix +{ config, ... }: + +{ + services.netbird.server = { + enable = true; + domain = "netbird.example.com"; + enableNginx = true; + + useRelay = true; + relayAuthSecretFile = "/run/secrets/netbird/relay-auth"; + + management = { + oidcConfigEndpoint = "https://auth.example.com/.well-known/openid-configuration"; + + settings = { + DataStoreEncryptionKey._secret = "/run/secrets/netbird/encryption-key"; + }; + }; + + dashboard.settings = { + AUTH_AUTHORITY = "https://auth.example.com"; + AUTH_CLIENT_ID = "netbird-dashboard"; + }; + }; + + # Configure Nginx with ACME + services.nginx.virtualHosts."netbird.example.com" = { + enableACME = true; + forceSSL = true; + }; + + security.acme = { + acceptTerms = true; + defaults.email = "admin@example.com"; + }; + + # Open firewall for STUN (relay handles the rest via nginx) + networking.firewall.allowedUDPPorts = [ 3478 ]; +} +``` diff --git a/nixos/modules/services/networking/netbird/server.nix b/nixos/modules/services/networking/netbird/server.nix index d5f6ebda2bb59..1db7fa64c5f56 100644 --- a/nixos/modules/services/networking/netbird/server.nix +++ b/nixos/modules/services/networking/netbird/server.nix @@ -9,14 +9,18 @@ let optionalAttrs ; - inherit (lib.types) str; + inherit (lib.types) + nullOr + path + str + ; cfg = config.services.netbird.server; in { meta = { - maintainers = with lib.maintainers; [ patrickdag ]; + maintainers = with lib.maintainers; [ shuuri-labs ]; doc = ./server.md; }; @@ -25,6 +29,7 @@ in ./coturn.nix ./dashboard.nix ./management.nix + ./relay.nix ./signal.nix ]; @@ -37,22 +42,51 @@ in type = str; description = "The domain under which the netbird server runs."; }; + + useRelay = mkOption { + type = lib.types.bool; + default = false; + description = '' + Use the modern relay server instead of (or in addition to) Coturn. + When enabled, the relay server will be configured automatically. + ''; + }; + + relayAuthSecretFile = mkOption { + type = nullOr path; + default = null; + description = '' + Path to the shared authentication secret for the relay server. + This secret must be provided when useRelay is enabled. + It will be used by both the relay server and management server. + ''; + }; }; config = mkIf cfg.enable { + assertions = [ + { + assertion = cfg.useRelay -> cfg.relayAuthSecretFile != null; + message = "relayAuthSecretFile must be set when useRelay is enabled"; + } + ]; + services.netbird.server = { dashboard = { domain = mkDefault cfg.domain; enable = mkDefault cfg.enable; enableNginx = mkDefault cfg.enableNginx; - managementServer = "https://${cfg.domain}"; + managementServer = mkDefault "https://${cfg.domain}"; }; management = { domain = mkDefault cfg.domain; enable = mkDefault cfg.enable; enableNginx = mkDefault cfg.enableNginx; + # When using relay without coturn, turnDomain still needs a value. + # Default to the server domain so the management config evaluates. + turnDomain = mkDefault cfg.domain; } // (optionalAttrs cfg.coturn.enable rec { turnDomain = cfg.domain; @@ -72,6 +106,10 @@ in } ]; }; + }) + // (optionalAttrs cfg.useRelay { + relayAddresses = mkDefault [ "rels://${cfg.domain}:443" ]; + relaySecretFile = mkDefault cfg.relayAuthSecretFile; }); signal = { @@ -80,6 +118,14 @@ in enableNginx = mkDefault cfg.enableNginx; }; + relay = mkIf cfg.useRelay { + enable = mkDefault true; + domain = mkDefault cfg.domain; + enableNginx = mkDefault cfg.enableNginx; + exposedAddress = mkDefault "rels://${cfg.domain}:443"; + authSecretFile = mkDefault cfg.relayAuthSecretFile; + }; + coturn = { domain = mkDefault cfg.domain; }; diff --git a/nixos/modules/services/networking/netbird/signal.nix b/nixos/modules/services/networking/netbird/signal.nix index 7063696f28aba..f0d85257ba2b0 100644 --- a/nixos/modules/services/networking/netbird/signal.nix +++ b/nixos/modules/services/networking/netbird/signal.nix @@ -13,11 +13,15 @@ let mkIf mkPackageOption mkOption + optionals ; inherit (lib.types) + bool listOf enum + nullOr + path port str ; @@ -25,6 +29,7 @@ let inherit (utils) escapeSystemdExecArgs; cfg = config.services.netbird.server.signal; + stateDir = "/var/lib/netbird-signal"; in { @@ -52,6 +57,41 @@ in description = "Internal port of the metrics server."; }; + tls = { + enable = mkEnableOption "TLS for the signal server"; + + letsencrypt = { + domain = mkOption { + type = nullOr str; + default = null; + description = '' + Domain for automatic Let's Encrypt certificate. + When set, the signal server will automatically obtain and renew certificates. + ''; + }; + }; + + certFile = mkOption { + type = nullOr path; + default = null; + description = "Path to the TLS certificate file."; + }; + + certKey = mkOption { + type = nullOr path; + default = null; + description = "Path to the TLS certificate key file."; + }; + }; + + openFirewall = mkOption { + type = bool; + default = false; + description = '' + Whether to open the signal server port in the firewall. + ''; + }; + extraOptions = mkOption { type = listOf str; default = [ ]; @@ -79,11 +119,32 @@ in assertion = cfg.port != cfg.metricsPort; message = "The primary listen port cannot be the same as the listen port for the metrics endpoint"; } + { + assertion = + cfg.tls.enable + -> (cfg.tls.letsencrypt.domain != null || (cfg.tls.certFile != null && cfg.tls.certKey != null)); + message = "When TLS is enabled, either letsencrypt.domain or both certFile and certKey must be set"; + } + { + assertion = cfg.tls.certFile != null -> cfg.tls.certKey != null; + message = "certKey must be set when certFile is set"; + } + { + assertion = cfg.tls.certKey != null -> cfg.tls.certFile != null; + message = "certFile must be set when certKey is set"; + } ]; systemd.services.netbird-signal = { + description = "The signal server for Netbird, a wireguard VPN"; + documentation = [ "https://netbird.io/docs/" ]; + after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; + restartTriggers = [ + cfg.port + cfg.logLevel + ]; serviceConfig = { ExecStart = escapeSystemdExecArgs ( @@ -103,13 +164,28 @@ in "--log-level" cfg.logLevel ] + # TLS options + ++ optionals (cfg.tls.letsencrypt.domain != null) [ + "--letsencrypt-domain" + cfg.tls.letsencrypt.domain + ] + ++ optionals (cfg.tls.certFile != null) [ + "--cert-file" + cfg.tls.certFile + ] + ++ optionals (cfg.tls.certKey != null) [ + "--cert-key" + cfg.tls.certKey + ] ++ cfg.extraOptions ); Restart = "always"; - RuntimeDirectory = "netbird-mgmt"; - StateDirectory = "netbird-mgmt"; - WorkingDirectory = "/var/lib/netbird-mgmt"; + RuntimeDirectory = "netbird-signal"; + RuntimeDirectoryMode = "0750"; + StateDirectory = "netbird-signal"; + StateDirectoryMode = "0750"; + WorkingDirectory = stateDir; # hardening LockPersonality = true; @@ -124,7 +200,7 @@ in ProtectKernelLogs = true; ProtectKernelModules = true; ProtectKernelTunables = true; - ProtectSystem = true; + ProtectSystem = "strict"; RemoveIPC = true; RestrictNamespaces = true; RestrictRealtime = true; @@ -134,6 +210,10 @@ in stopIfChanged = false; }; + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = [ cfg.port ]; + }; + services.nginx = mkIf cfg.enableNginx { enable = true; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index f6093eb7e14a0..df49c84158c89 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1060,7 +1060,7 @@ in nebula.connectivity = runTest ./nebula/connectivity.nix; nebula.reload = runTest ./nebula/reload.nix; neo4j = runTest ./neo4j.nix; - netbird = runTest ./netbird.nix; + netbird = import ./netbird { inherit runTest; }; netbox-upgrade = runTest ./web-apps/netbox-upgrade.nix; netbox_4_4 = handleTest ./web-apps/netbox/default.nix { netbox = pkgs.netbox_4_4; }; netbox_4_5 = handleTest ./web-apps/netbox/default.nix { netbox = pkgs.netbox_4_5; }; diff --git a/nixos/tests/netbird.nix b/nixos/tests/netbird/client.nix similarity index 100% rename from nixos/tests/netbird.nix rename to nixos/tests/netbird/client.nix diff --git a/nixos/tests/netbird/default.nix b/nixos/tests/netbird/default.nix new file mode 100644 index 0000000000000..69d1631ba99f7 --- /dev/null +++ b/nixos/tests/netbird/default.nix @@ -0,0 +1,8 @@ +{ runTest }: + +{ + client = runTest ./client.nix; + server-signal = runTest ./server-signal.nix; + server-management = runTest ./server-management.nix; + server-relay = runTest ./server-relay.nix; +} diff --git a/nixos/tests/netbird/server-management.nix b/nixos/tests/netbird/server-management.nix new file mode 100644 index 0000000000000..468bc299ffa05 --- /dev/null +++ b/nixos/tests/netbird/server-management.nix @@ -0,0 +1,123 @@ +{ + lib, + ... +}: +{ + name = "netbird-server-management"; + + meta.maintainers = with lib.maintainers; [ + shuuri-labs + ]; + + nodes = { + management = { + services.netbird.server.management = { + enable = true; + domain = "mgmt.test"; + turnDomain = "turn.test"; + port = 8011; + metricsPort = 9090; + logLevel = "DEBUG"; + settings = { + # Use a test encryption key + DataStoreEncryptionKey = "test-encryption-key-for-testing"; + }; + }; + }; + + managementWithRelay = { + services.netbird.server.management = { + enable = true; + domain = "mgmt-relay.test"; + turnDomain = "turn.test"; + port = 8011; + metricsPort = 9090; + + # Configure relay + relayAddresses = [ "rels://relay.test:443" ]; + relaySecretFile = "/run/secrets/relay-secret"; + + settings = { + DataStoreEncryptionKey = "test-encryption-key-for-testing"; + }; + }; + + # Create a test secret file + systemd.services.netbird-management.preStart = lib.mkBefore '' + mkdir -p /run/secrets + echo "test-relay-secret" > /run/secrets/relay-secret + ''; + }; + + managementWithPostgres = { + services.netbird.server.management = { + enable = true; + domain = "mgmt-pg.test"; + turnDomain = "turn.test"; + port = 8011; + metricsPort = 9090; + + store = { + engine = "postgres"; + postgres.dsnFile = "/run/secrets/postgres-dsn"; + }; + + settings = { + DataStoreEncryptionKey = "test-encryption-key-for-testing"; + }; + }; + + services.postgresql = { + enable = true; + ensureDatabases = [ "netbird" ]; + ensureUsers = [ + { + name = "netbird"; + ensureDBOwnership = true; + } + ]; + }; + + systemd.services.netbird-management = { + after = [ "postgresql.service" ]; + requires = [ "postgresql.service" ]; + preStart = lib.mkBefore '' + mkdir -p /run/secrets + echo "postgres://netbird@localhost/netbird?sslmode=disable" > /run/secrets/postgres-dsn + ''; + }; + }; + }; + + testScript = '' + start_all() + + # Test basic management server + management.wait_for_unit("netbird-management.service") + management.wait_for_open_port(8011) + management.wait_for_open_port(9090) + + # Verify state directory exists + management.succeed("test -d /var/lib/netbird-mgmt") + management.succeed("test -d /var/lib/netbird-mgmt/data") + + # Verify config file was generated + management.succeed("test -f /var/lib/netbird-mgmt/management.json") + + # Test management with relay configuration + managementWithRelay.wait_for_unit("netbird-management.service") + managementWithRelay.wait_for_open_port(8011) + + # Verify relay config is in the generated config + managementWithRelay.succeed("grep -q 'Relay' /var/lib/netbird-mgmt/management.json") + managementWithRelay.succeed("grep -q 'rels://relay.test:443' /var/lib/netbird-mgmt/management.json") + + # Test management with PostgreSQL + managementWithPostgres.wait_for_unit("postgresql.service") + managementWithPostgres.wait_for_unit("netbird-management.service") + managementWithPostgres.wait_for_open_port(8011) + + # Verify postgres engine is in config + managementWithPostgres.succeed("grep -q 'postgres' /var/lib/netbird-mgmt/management.json") + ''; +} diff --git a/nixos/tests/netbird/server-relay.nix b/nixos/tests/netbird/server-relay.nix new file mode 100644 index 0000000000000..09b431363d3ce --- /dev/null +++ b/nixos/tests/netbird/server-relay.nix @@ -0,0 +1,48 @@ +{ + lib, + ... +}: +{ + name = "netbird-server-relay"; + + meta.maintainers = with lib.maintainers; [ + shuuri-labs + ]; + + nodes = { + relay = + { pkgs, ... }: + { + services.netbird.server.relay = { + enable = true; + port = 8443; + exposedAddress = "rels://relay.test:8443"; + # pkgs.writeText is world-readable but acceptable for tests + authSecretFile = pkgs.writeText "relay-auth" "test-auth-secret"; + logLevel = "debug"; + + stun = { + enable = true; + ports = [ 3478 ]; + }; + + openFirewall = true; + }; + }; + }; + + testScript = '' + start_all() + + # Test basic relay server + relay.wait_for_unit("netbird-relay.service") + relay.wait_for_open_port(8443) + + # Verify state directory exists + relay.succeed("test -d /var/lib/netbird-relay") + + # Verify firewall is configured (relay port and STUN) + relay.succeed("iptables -L -n | grep -q 8443") + relay.succeed("iptables -L -n | grep -q 3478") + ''; +} diff --git a/nixos/tests/netbird/server-signal.nix b/nixos/tests/netbird/server-signal.nix new file mode 100644 index 0000000000000..9291ae7a486a6 --- /dev/null +++ b/nixos/tests/netbird/server-signal.nix @@ -0,0 +1,40 @@ +{ + lib, + ... +}: +{ + name = "netbird-server-signal"; + + meta.maintainers = with lib.maintainers; [ + shuuri-labs + ]; + + nodes = { + signal = { + services.netbird.server.signal = { + enable = true; + domain = "signal.test"; + port = 8012; + metricsPort = 9091; + logLevel = "DEBUG"; + }; + }; + }; + + testScript = '' + start_all() + + # Test basic signal server + signal.wait_for_unit("netbird-signal.service") + + # Verify the service is running on the correct port + signal.wait_for_open_port(8012) + signal.wait_for_open_port(9091) + + # Verify state directory is correct (not netbird-mgmt) + signal.succeed("test -d /var/lib/netbird-signal") + + # Verify working directory is correct + signal.succeed("systemctl show netbird-signal -p WorkingDirectory | grep '/var/lib/netbird-signal'") + ''; +}