diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md index 2909c40fa291..3dfec56a98f4 100644 --- a/nixos/doc/manual/release-notes/rl-2405.section.md +++ b/nixos/doc/manual/release-notes/rl-2405.section.md @@ -108,6 +108,8 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m - [Suwayomi Server](https://github.com/Suwayomi/Suwayomi-Server), a free and open source manga reader server that runs extensions built for [Tachiyomi](https://tachiyomi.org). Available as [services.suwayomi-server](#opt-services.suwayomi-server.enable). +- A self-hosted management server for the [Netbird](https://netbird.io). Available as [services.netbird.server](#opt-services.netbird.server.enable). + - [ping_exporter](https://github.com/czerwonk/ping_exporter), a Prometheus exporter for ICMP echo requests. Available as [services.prometheus.exporters.ping](#opt-services.prometheus.exporters.ping.enable). - [TigerBeetle](https://tigerbeetle.com/), a distributed financial accounting database designed for mission critical safety and performance. Available as [services.tigerbeetle](#opt-services.tigerbeetle.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 90b37e878312..425ff4aa995c 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1052,6 +1052,7 @@ ./services/networking/ndppd.nix ./services/networking/nebula.nix ./services/networking/netbird.nix + ./services/networking/netbird/server.nix ./services/networking/netclient.nix ./services/networking/networkd-dispatcher.nix ./services/networking/networkmanager.nix diff --git a/nixos/modules/services/networking/netbird/coturn.nix b/nixos/modules/services/networking/netbird/coturn.nix new file mode 100644 index 000000000000..dd032abb2d75 --- /dev/null +++ b/nixos/modules/services/networking/netbird/coturn.nix @@ -0,0 +1,160 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) + getExe + literalExpression + mkAfter + mkEnableOption + mkIf + mkMerge + mkOption + optionalAttrs + optionalString + ; + + inherit (lib.types) + bool + listOf + nullOr + path + port + str + ; + + cfg = config.services.netbird.server.coturn; +in + +{ + options.services.netbird.server.coturn = { + enable = mkEnableOption "a Coturn server for Netbird, will also open the firewall on the configured range"; + + useAcmeCertificates = mkOption { + type = bool; + default = false; + description = '' + Whether to use ACME certificates corresponding to the given domain for the server. + ''; + }; + + domain = mkOption { + type = str; + description = "The domain under which the coturn server runs."; + }; + + user = mkOption { + type = str; + default = "netbird"; + description = '' + The username used by netbird to connect to the coturn server. + ''; + }; + + password = mkOption { + type = nullOr str; + default = null; + description = '' + The password of the user used by netbird to connect to the coturn server. + ''; + }; + + passwordFile = mkOption { + type = nullOr path; + default = null; + description = '' + The path to a file containing the password of the user used by netbird to connect to the coturn server. + ''; + }; + + openPorts = mkOption { + type = listOf port; + default = with config.services.coturn; [ + listening-port + alt-listening-port + tls-listening-port + alt-tls-listening-port + ]; + defaultText = literalExpression '' + with config.services.coturn; [ + listening-port + alt-listening-port + tls-listening-port + alt-tls-listening-port + ]; + ''; + + description = '' + The list of ports used by coturn for listening to open in the firewall. + ''; + }; + }; + + config = mkIf cfg.enable (mkMerge [ + { + assertions = [ + { + assertion = (cfg.password == null) != (cfg.passwordFile == null); + message = "Exactly one of `password` or `passwordFile` must be given for the coturn setup."; + } + ]; + + services.coturn = + { + enable = true; + + realm = cfg.domain; + lt-cred-mech = true; + no-cli = true; + + extraConfig = '' + fingerprint + user=${cfg.user}:${if cfg.password != null then cfg.password else "@password@"} + no-software-attribute + ''; + } + // (optionalAttrs cfg.useAcmeCertificates { + cert = "@cert@"; + pkey = "@pkey@"; + }); + + systemd.services.coturn = + let + dir = config.security.acme.certs.${cfg.domain}.directory; + preStart' = + (optionalString (cfg.passwordFile != null) '' + ${getExe pkgs.replace-secret} @password@ ${cfg.passwordFile} /run/coturn/turnserver.cfg + '') + + (optionalString cfg.useAcmeCertificates '' + ${getExe pkgs.replace-secret} @cert@ "$CREDENTIALS_DIRECTORY/cert.pem" /run/coturn/turnserver.cfg + ${getExe pkgs.replace-secret} @pkey@ "$CREDENTIALS_DIRECTORY/pkey.pem" /run/coturn/turnserver.cfg + ''); + in + (optionalAttrs (preStart' != "") { preStart = mkAfter preStart'; }) + // (optionalAttrs cfg.useAcmeCertificates { + serviceConfig.LoadCredential = [ + "cert.pem:${dir}/fullchain.pem" + "pkey.pem:${dir}/key.pem" + ]; + }); + + security.acme.certs.${cfg.domain}.postRun = optionalString cfg.useAcmeCertificates "systemctl restart coturn.service"; + + networking.firewall = { + allowedUDPPorts = cfg.openPorts; + allowedTCPPorts = cfg.openPorts; + + allowedUDPPortRanges = [ + { + from = cfg.minPort; + to = cfg.maxPort; + } + ]; + }; + } + ]); +} diff --git a/nixos/modules/services/networking/netbird/dashboard.nix b/nixos/modules/services/networking/netbird/dashboard.nix new file mode 100644 index 000000000000..6fc308615590 --- /dev/null +++ b/nixos/modules/services/networking/netbird/dashboard.nix @@ -0,0 +1,186 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) + boolToString + concatStringsSep + hasAttr + isBool + mapAttrs + mkDefault + mkEnableOption + mkIf + mkOption + mkPackageOption + ; + + inherit (lib.types) + attrsOf + bool + either + package + str + submodule + ; + + toStringEnv = value: if isBool value then boolToString value else toString value; + + cfg = config.services.netbird.server.dashboard; +in + +{ + options.services.netbird.server.dashboard = { + enable = mkEnableOption "the static netbird dashboard frontend"; + + package = mkPackageOption pkgs "netbird-dashboard" { }; + + enableNginx = mkEnableOption "Nginx reverse-proxy to serve the dashboard."; + + domain = mkOption { + type = str; + default = "localhost"; + description = "The domain under which the dashboard runs."; + }; + + managementServer = mkOption { + type = str; + description = "The address of the management server, used for the API endpoints."; + }; + + settings = mkOption { + type = submodule { freeformType = attrsOf (either str bool); }; + + defaultText = '' + { + AUTH_AUDIENCE = "netbird"; + AUTH_CLIENT_ID = "netbird"; + AUTH_SUPPORTED_SCOPES = "openid profile email"; + NETBIRD_TOKEN_SOURCE = "idToken"; + USE_AUTH0 = false; + } + ''; + + description = '' + An attribute set that will be used to substitute variables when building the dashboard. + Any values set here will be templated into the frontend and be public for anyone that can reach your website. + The exact values sadly aren't documented anywhere. + A starting point when searching for valid values is this [script](https://github.com/netbirdio/dashboard/blob/main/docker/init_react_envs.sh) + The only mandatory value is 'AUTH_AUTHORITY' as we cannot set a default value here. + ''; + }; + + finalDrv = mkOption { + readOnly = true; + type = package; + description = '' + The derivation containing the final templated dashboard. + ''; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = hasAttr "AUTH_AUTHORITY" cfg.settings; + message = "The setting AUTH_AUTHORITY is required for the dasboard to function."; + } + ]; + + services.netbird.server.dashboard = { + settings = + { + # Due to how the backend and frontend work this secret will be templated into the backend + # and then served statically from your website + # This enables you to login without the normally needed indirection through the backend + # but this also means anyone that can reach your website can + # fetch this secret, which is why there is no real need to put it into + # special options as its public anyway + # As far as I know leaking this secret is just + # an information leak as one can fetch some basic app + # informations from the IDP + # To actually do something one still needs to have login + # data and this secret so this being public will not + # suffice for anything just decreasing security + AUTH_CLIENT_SECRET = ""; + + NETBIRD_MGMT_API_ENDPOINT = cfg.managementServer; + NETBIRD_MGMT_GRPC_API_ENDPOINT = cfg.managementServer; + } + // (mapAttrs (_: mkDefault) { + # Those values have to be easily overridable + AUTH_AUDIENCE = "netbird"; # must be set for your devices to be able to log in + AUTH_CLIENT_ID = "netbird"; + AUTH_SUPPORTED_SCOPES = "openid profile email"; + NETBIRD_TOKEN_SOURCE = "idToken"; + USE_AUTH0 = false; + }); + + # The derivation containing the templated dashboard + finalDrv = + pkgs.runCommand "netbird-dashboard" + { + nativeBuildInputs = [ pkgs.gettext ]; + env = { + ENV_STR = concatStringsSep " " [ + "$AUTH_AUDIENCE" + "$AUTH_AUTHORITY" + "$AUTH_CLIENT_ID" + "$AUTH_CLIENT_SECRET" + "$AUTH_REDIRECT_URI" + "$AUTH_SILENT_REDIRECT_URI" + "$AUTH_SUPPORTED_SCOPES" + "$NETBIRD_DRAG_QUERY_PARAMS" + "$NETBIRD_GOOGLE_ANALYTICS_ID" + "$NETBIRD_HOTJAR_TRACK_ID" + "$NETBIRD_MGMT_API_ENDPOINT" + "$NETBIRD_MGMT_GRPC_API_ENDPOINT" + "$NETBIRD_TOKEN_SOURCE" + "$USE_AUTH0" + ]; + } // (mapAttrs (_: toStringEnv) cfg.settings); + } + '' + cp -R ${cfg.package} build + + find build -type d -exec chmod 755 {} \; + OIDC_TRUSTED_DOMAINS="build/OidcTrustedDomains.js" + + envsubst "$ENV_STR" < "$OIDC_TRUSTED_DOMAINS.tmpl" > "$OIDC_TRUSTED_DOMAINS" + + for f in $(grep -R -l AUTH_SUPPORTED_SCOPES build/); do + mv "$f" "$f.copy" + envsubst "$ENV_STR" < "$f.copy" > "$f" + rm "$f.copy" + done + + cp -R build $out + ''; + }; + + services.nginx = mkIf cfg.enableNginx { + enable = true; + + virtualHosts.${cfg.domain} = { + locations = { + "/" = { + root = cfg.finalDrv; + tryFiles = "$uri $uri.html $uri/ =404"; + }; + + "/404.html".extraConfig = '' + internal; + ''; + }; + + extraConfig = '' + error_page 404 /404.html; + ''; + }; + }; + }; +} diff --git a/nixos/modules/services/networking/netbird/management.nix b/nixos/modules/services/networking/netbird/management.nix new file mode 100644 index 000000000000..52f033959143 --- /dev/null +++ b/nixos/modules/services/networking/netbird/management.nix @@ -0,0 +1,460 @@ +{ + config, + lib, + pkgs, + utils, + ... +}: + +let + inherit (lib) + any + concatMap + getExe' + literalExpression + mkEnableOption + mkIf + mkOption + mkPackageOption + optional + recursiveUpdate + ; + + inherit (lib.types) + bool + enum + listOf + port + str + ; + + inherit (utils) escapeSystemdExecArgs genJqSecretsReplacementSnippet; + + stateDir = "/var/lib/netbird-mgmt"; + + settingsFormat = pkgs.formats.json { }; + + defaultSettings = { + Stuns = [ + { + Proto = "udp"; + URI = "stun:${cfg.turnDomain}:3478"; + Username = ""; + Password = null; + } + ]; + + TURNConfig = { + Turns = [ + { + Proto = "udp"; + URI = "turn:${cfg.turnDomain}:${builtins.toString cfg.turnPort}"; + Username = "netbird"; + Password = "netbird"; + } + ]; + + CredentialsTTL = "12h"; + Secret = "not-secure-secret"; + TimeBasedCredentials = false; + }; + + Signal = { + Proto = "https"; + URI = "${cfg.domain}:443"; + Username = ""; + Password = null; + }; + + ReverseProxy = { + TrustedHTTPProxies = [ ]; + TrustedHTTPProxiesCount = 0; + TrustedPeers = [ "0.0.0.0/0" ]; + }; + + Datadir = "${stateDir}/data"; + DataStoreEncryptionKey = "very-insecure-key"; + StoreConfig = { + Engine = "sqlite"; + }; + + HttpConfig = { + Address = "127.0.0.1:${builtins.toString cfg.port}"; + IdpSignKeyRefreshEnabled = true; + OIDCConfigEndpoint = cfg.oidcConfigEndpoint; + }; + + IdpManagerConfig = { + ManagerType = "none"; + ClientConfig = { + Issuer = ""; + TokenEndpoint = ""; + ClientID = "netbird"; + ClientSecret = ""; + GrantType = "client_credentials"; + }; + + ExtraConfig = { }; + Auth0ClientCredentials = null; + AzureClientCredentials = null; + KeycloakClientCredentials = null; + ZitadelClientCredentials = null; + }; + + DeviceAuthorizationFlow = { + Provider = "none"; + ProviderConfig = { + Audience = "netbird"; + Domain = null; + ClientID = "netbird"; + TokenEndpoint = null; + DeviceAuthEndpoint = ""; + Scope = "openid profile email"; + UseIDToken = false; + }; + }; + + PKCEAuthorizationFlow = { + ProviderConfig = { + Audience = "netbird"; + ClientID = "netbird"; + ClientSecret = ""; + AuthorizationEndpoint = ""; + TokenEndpoint = ""; + Scope = "openid profile email"; + RedirectURLs = [ "http://localhost:53000" ]; + UseIDToken = false; + }; + }; + }; + + managementConfig = recursiveUpdate defaultSettings cfg.settings; + + managementFile = settingsFormat.generate "config.json" managementConfig; + + cfg = config.services.netbird.server.management; +in + +{ + options.services.netbird.server.management = { + enable = mkEnableOption "Netbird Management Service."; + + package = mkPackageOption pkgs "netbird" { }; + + domain = mkOption { + type = str; + description = "The domain under which the management API runs."; + }; + + turnDomain = mkOption { + type = str; + description = "The domain of the TURN server to use."; + }; + + turnPort = mkOption { + type = port; + default = 3478; + description = '' + The port of the TURN server to use. + ''; + }; + + dnsDomain = mkOption { + type = str; + default = "netbird.selfhosted"; + 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. + ''; + }; + + disableAnonymousMetrics = mkOption { + type = bool; + default = true; + 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; + description = "Internal port of the management server."; + }; + + extraOptions = mkOption { + type = listOf str; + default = [ ]; + description = '' + Additional options given to netbird-mgmt as commandline arguments. + ''; + }; + + oidcConfigEndpoint = mkOption { + type = str; + description = "The oidc discovery endpoint."; + example = "https://example.eu.auth0.com/.well-known/openid-configuration"; + }; + + settings = mkOption { + inherit (settingsFormat) type; + + defaultText = literalExpression '' + defaultSettings = { + Stuns = [ + { + Proto = "udp"; + URI = "stun:''${cfg.turnDomain}:3478"; + Username = ""; + Password = null; + } + ]; + + TURNConfig = { + Turns = [ + { + Proto = "udp"; + URI = "turn:''${cfg.turnDomain}:3478"; + Username = "netbird"; + Password = "netbird"; + } + ]; + + CredentialsTTL = "12h"; + Secret = "not-secure-secret"; + TimeBasedCredentials = false; + }; + + Signal = { + Proto = "https"; + URI = "''${cfg.domain}:443"; + Username = ""; + Password = null; + }; + + ReverseProxy = { + TrustedHTTPProxies = [ ]; + TrustedHTTPProxiesCount = 0; + TrustedPeers = [ "0.0.0.0/0" ]; + }; + + Datadir = "''${stateDir}/data"; + DataStoreEncryptionKey = "genEVP6j/Yp2EeVujm0zgqXrRos29dQkpvX0hHdEUlQ="; + StoreConfig = { Engine = "sqlite"; }; + + HttpConfig = { + Address = "127.0.0.1:''${builtins.toString cfg.port}"; + IdpSignKeyRefreshEnabled = true; + OIDCConfigEndpoint = cfg.oidcConfigEndpoint; + }; + + IdpManagerConfig = { + ManagerType = "none"; + ClientConfig = { + Issuer = ""; + TokenEndpoint = ""; + ClientID = "netbird"; + ClientSecret = ""; + GrantType = "client_credentials"; + }; + + ExtraConfig = { }; + Auth0ClientCredentials = null; + AzureClientCredentials = null; + KeycloakClientCredentials = null; + ZitadelClientCredentials = null; + }; + + DeviceAuthorizationFlow = { + Provider = "none"; + ProviderConfig = { + Audience = "netbird"; + Domain = null; + ClientID = "netbird"; + TokenEndpoint = null; + DeviceAuthEndpoint = ""; + Scope = "openid profile email offline_access api"; + UseIDToken = false; + }; + }; + + PKCEAuthorizationFlow = { + ProviderConfig = { + Audience = "netbird"; + ClientID = "netbird"; + ClientSecret = ""; + AuthorizationEndpoint = ""; + TokenEndpoint = ""; + Scope = "openid profile email offline_access api"; + RedirectURLs = "http://localhost:53000"; + UseIDToken = false; + }; + }; + }; + ''; + + default = { }; + + description = '' + Configuration of the netbird management server. + Options containing secret data should be set to an attribute set containing the attribute _secret + - a string pointing to a file containing the value the option should be set to. + See the example to get a better picture of this: in the resulting management.json file, + the `DataStoreEncryptionKey` key will be set to the contents of the /run/agenix/netbird_mgmt-data_store_encryption_key file. + ''; + + example = { + DataStoreEncryptionKey = { + _secret = "/run/agenix/netbird_mgmt-data_store_encryption_key"; + }; + }; + }; + + logLevel = mkOption { + type = enum [ + "ERROR" + "WARN" + "INFO" + "DEBUG" + ]; + default = "INFO"; + description = "Log level of the netbird services."; + }; + + enableNginx = mkEnableOption "Nginx reverse-proxy for the netbird management service."; + }; + + config = mkIf cfg.enable { + warnings = + concatMap + ( + { check, name }: + optional check "${name} is world-readable in the Nix Store, you should provide it as a _secret." + ) + [ + { + check = builtins.isString managementConfig.TURNConfig.Secret; + name = "The TURNConfig.secret"; + } + { + check = builtins.isString managementConfig.DataStoreEncryptionKey; + name = "The DataStoreEncryptionKey"; + } + { + check = any (T: (T ? Password) && builtins.isString T.Password) managementConfig.TURNConfig.Turns; + name = "A Turn configuration's password"; + } + ]; + + systemd.services.netbird-management = { + description = "The management server for Netbird, a wireguard VPN"; + documentation = [ "https://netbird.io/docs/" ]; + + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + restartTriggers = [ managementFile ]; + + preStart = genJqSecretsReplacementSnippet managementConfig "${stateDir}/management.json"; + + serviceConfig = { + ExecStart = escapeSystemdExecArgs ( + [ + (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 + # Log to stdout + "--log-file" + "console" + # Log level + "--log-level" + cfg.logLevel + # + "--idp-sign-key-refresh-enabled" + # Domain for internal resolution + "--single-account-mode-domain" + cfg.singleAccountModeDomain + ] + ++ (optional cfg.disableAnonymousMetrics "--disable-anonymous-metrics") + ++ (optional cfg.disableSingleAccountMode "--disable-single-account-mode") + ++ cfg.extraOptions + ); + Restart = "always"; + RuntimeDirectory = "netbird-mgmt"; + StateDirectory = [ + "netbird-mgmt" + "netbird-mgmt/data" + ]; + WorkingDirectory = stateDir; + + # 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 = true; + RemoveIPC = true; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + }; + + stopIfChanged = false; + }; + + services.nginx = mkIf cfg.enableNginx { + enable = true; + + virtualHosts.${cfg.domain} = { + locations = { + "/api".proxyPass = "http://localhost:${builtins.toString cfg.port}"; + + "/management.ManagementService/".extraConfig = '' + # This is necessary so that grpc connections do not get closed early + # see https://stackoverflow.com/a/67805465 + client_body_timeout 1d; + + grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + grpc_pass grpc://localhost:${builtins.toString cfg.port}; + grpc_read_timeout 1d; + grpc_send_timeout 1d; + grpc_socket_keepalive on; + ''; + }; + }; + }; + }; +} diff --git a/nixos/modules/services/networking/netbird/server.md b/nixos/modules/services/networking/netbird/server.md new file mode 100644 index 000000000000..3649e97b379e --- /dev/null +++ b/nixos/modules/services/networking/netbird/server.md @@ -0,0 +1,42 @@ +# Netbird server {#module-services-netbird-server} + +NetBird is a VPN built on top of WireGuard® making it easy to create secure private networks for your organization or home. + +## 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). + +There are quite a few settings that need to be passed to Netbird for it to function, and a minimal config looks like : + +```nix +services.netbird.server = { + enable = true; + + domain = "netbird.example.selfhosted"; + + enableNginx = true; + + coturn = { + enable = true; + + passwordFile = "/path/to/a/secret/password"; + }; + + management = { + oidcConfigEndpoint = "https://sso.example.selfhosted/oauth2/openid/netbird/.well-known/openid-configuration"; + + settings = { + TURNConfig = { + Turns = [ + { + Proto = "udp"; + URI = "turn:netbird.example.selfhosted:3478"; + Username = "netbird"; + Password._secret = "/path/to/a/secret/password"; + } + ]; + }; + }; + }; +}; +``` diff --git a/nixos/modules/services/networking/netbird/server.nix b/nixos/modules/services/networking/netbird/server.nix new file mode 100644 index 000000000000..a4de0fda6a13 --- /dev/null +++ b/nixos/modules/services/networking/netbird/server.nix @@ -0,0 +1,67 @@ +{ config, lib, ... }: + +let + inherit (lib) + mkEnableOption + mkIf + mkOption + optionalAttrs + ; + + inherit (lib.types) str; + + cfg = config.services.netbird.server; +in + +{ + meta = { + maintainers = with lib.maintainers; [ thubrecht ]; + doc = ./server.md; + }; + + # Import the separate components + imports = [ + ./coturn.nix + ./dashboard.nix + ./management.nix + ./signal.nix + ]; + + options.services.netbird.server = { + enable = mkEnableOption "Netbird Server stack, comprising the dashboard, management API and signal service"; + + enableNginx = mkEnableOption "Nginx reverse-proxy for the netbird server services."; + + domain = mkOption { + type = str; + description = "The domain under which the netbird server runs."; + }; + }; + + config = mkIf cfg.enable { + services.netbird.server = { + dashboard = { + inherit (cfg) enable domain enableNginx; + + managementServer = "https://${cfg.domain}"; + }; + + management = + { + inherit (cfg) enable domain enableNginx; + } + // (optionalAttrs cfg.coturn.enable { + turnDomain = cfg.domain; + turnPort = config.services.coturn.tls-listening-port; + }); + + signal = { + inherit (cfg) enable domain enableNginx; + }; + + coturn = { + inherit (cfg) domain; + }; + }; + }; +} diff --git a/nixos/modules/services/networking/netbird/signal.nix b/nixos/modules/services/networking/netbird/signal.nix new file mode 100644 index 000000000000..8408d20e874b --- /dev/null +++ b/nixos/modules/services/networking/netbird/signal.nix @@ -0,0 +1,123 @@ +{ + config, + lib, + pkgs, + utils, + ... +}: + +let + inherit (lib) + getExe' + mkEnableOption + mkIf + mkPackageOption + mkOption + ; + + inherit (lib.types) enum port str; + + inherit (utils) escapeSystemdExecArgs; + + cfg = config.services.netbird.server.signal; +in + +{ + options.services.netbird.server.signal = { + enable = mkEnableOption "Netbird's Signal Service"; + + package = mkPackageOption pkgs "netbird" { }; + + enableNginx = mkEnableOption "Nginx reverse-proxy for the netbird signal service."; + + domain = mkOption { + type = str; + description = "The domain name for the signal service."; + }; + + port = mkOption { + type = port; + default = 8012; + description = "Internal port of the signal server."; + }; + + logLevel = mkOption { + type = enum [ + "ERROR" + "WARN" + "INFO" + "DEBUG" + ]; + default = "INFO"; + description = "Log level of the netbird signal service."; + }; + }; + + config = mkIf cfg.enable { + systemd.services.netbird-signal = { + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + ExecStart = escapeSystemdExecArgs [ + (getExe' cfg.package "netbird-signal") + "run" + # Port to listen on + "--port" + cfg.port + # Log to stdout + "--log-file" + "console" + # Log level + "--log-level" + cfg.logLevel + ]; + + Restart = "always"; + RuntimeDirectory = "netbird-mgmt"; + StateDirectory = "netbird-mgmt"; + WorkingDirectory = "/var/lib/netbird-mgmt"; + + # 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 = true; + RemoveIPC = true; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + }; + + stopIfChanged = false; + }; + + services.nginx = mkIf cfg.enableNginx { + enable = true; + + virtualHosts.${cfg.domain} = { + locations."/signalexchange.SignalExchange/".extraConfig = '' + # This is necessary so that grpc connections do not get closed early + # see https://stackoverflow.com/a/67805465 + client_body_timeout 1d; + + grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + grpc_pass grpc://localhost:${builtins.toString cfg.port}; + grpc_read_timeout 1d; + grpc_send_timeout 1d; + grpc_socket_keepalive on; + ''; + }; + }; + }; +}