about summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
authorTom Hubrecht <tom@hubrecht.ovh>2023-08-04 15:04:44 +0200
committerTom Hubrecht <tom@hubrecht.ovh>2024-04-18 15:33:18 +0200
commit6d91c95fce2747ba06a09063349a4cfc18f5f216 (patch)
treed07d47e94dd027c79d5eaa6e29c50957e0e5580f /nixos
parent106b852a304e2acba34eadec4500b3ac00cce10c (diff)
nixos/netbird-server: init module
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/release-notes/rl-2405.section.md2
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/services/networking/netbird/coturn.nix160
-rw-r--r--nixos/modules/services/networking/netbird/dashboard.nix186
-rw-r--r--nixos/modules/services/networking/netbird/management.nix460
-rw-r--r--nixos/modules/services/networking/netbird/server.md42
-rw-r--r--nixos/modules/services/networking/netbird/server.nix67
-rw-r--r--nixos/modules/services/networking/netbird/signal.nix123
8 files changed, 1041 insertions, 0 deletions
diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md
index 2909c40fa291d..3dfec56a98f47 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 90b37e8783125..425ff4aa995c8 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 0000000000000..dd032abb2d75e
--- /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 0000000000000..6fc3086155900
--- /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 0000000000000..52f033959143c
--- /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 0000000000000..3649e97b379e5
--- /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 0000000000000..a4de0fda6a134
--- /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 0000000000000..8408d20e874b5
--- /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;
+        '';
+      };
+    };
+  };
+}