diff --git a/.github/workflows/basic-eval.yml b/.github/workflows/basic-eval.yml
index 9008ca208e17..04e74f774c2e 100644
--- a/.github/workflows/basic-eval.yml
+++ b/.github/workflows/basic-eval.yml
@@ -26,4 +26,4 @@ jobs:
name: nixpkgs-ci
signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}'
# explicit list of supportedSystems is needed until aarch64-darwin becomes part of the trunk jobset
- - run: nix-build pkgs/top-level/release.nix -A tarball.nixpkgs-basic-release-checks --arg supportedSystems '[ "aarch64-darwin" "aarch64-linux" "x86_64-linux" "x86_64-darwin" ]'
+ - run: nix-build pkgs/top-level/release.nix -A release-checks --arg supportedSystems '[ "aarch64-darwin" "aarch64-linux" "x86_64-linux" "x86_64-darwin" ]'
diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md
index 20bcb47dc571..093995d13443 100644
--- a/nixos/doc/manual/release-notes/rl-2405.section.md
+++ b/nixos/doc/manual/release-notes/rl-2405.section.md
@@ -137,6 +137,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).
- [Prometheus DNSSEC Exporter](https://github.com/chrj/prometheus-dnssec-exporter), check for validity and expiration in DNSSEC signatures and expose metrics for Prometheus. Available as [services.prometheus.exporters.dnssec](#opt-services.prometheus.exporters.dnssec.enable).
@@ -173,6 +175,8 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m
- [davis](https://github.com/tchapi/davis), a simple CardDav and CalDav server inspired by Baïkal. Available as [services.davis]($opt-services-davis.enable).
+- [Firefly-iii](https://www.firefly-iii.org), a free and open source personal finance manager. Available as [services.firefly-iii](#opt-services.firefly-iii.enable)
+
- [systemd-lock-handler](https://git.sr.ht/~whynothugo/systemd-lock-handler/), a bridge between logind D-Bus events and systemd targets. Available as [services.systemd-lock-handler.enable](#opt-services.systemd-lock-handler.enable).
- [wastebin](https://github.com/matze/wastebin), a pastebin server written in rust. Available as [services.wastebin](#opt-services.wastebin.enable).
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 2a2556104731..936cea1f3d51 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -1070,6 +1070,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
@@ -1334,6 +1335,7 @@
./services/web-apps/dolibarr.nix
./services/web-apps/engelsystem.nix
./services/web-apps/ethercalc.nix
+ ./services/web-apps/firefly-iii.nix
./services/web-apps/fluidd.nix
./services/web-apps/freshrss.nix
./services/web-apps/galene.nix
diff --git a/nixos/modules/services/misc/ollama.nix b/nixos/modules/services/misc/ollama.nix
index 948c8f17f989..c0341984aa35 100644
--- a/nixos/modules/services/misc/ollama.nix
+++ b/nixos/modules/services/misc/ollama.nix
@@ -21,6 +21,8 @@ in
example = "/home/foo";
description = ''
The home directory that the ollama service is started in.
+
+ See also `services.ollama.writablePaths` and `services.ollama.sandbox`.
'';
};
models = lib.mkOption {
@@ -29,6 +31,37 @@ in
example = "/path/to/ollama/models";
description = ''
The directory that the ollama service will read models from and download new models to.
+
+ See also `services.ollama.writablePaths` and `services.ollama.sandbox`
+ if downloading models or other mutation of the filesystem is required.
+ '';
+ };
+ sandbox = lib.mkOption {
+ type = types.bool;
+ default = true;
+ example = false;
+ description = ''
+ Whether to enable systemd's sandboxing capabilities.
+
+ This sets [`DynamicUser`](
+ https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#DynamicUser=
+ ), which runs the server as a unique user with read-only access to most of the filesystem.
+
+ See also `services.ollama.writablePaths`.
+ '';
+ };
+ writablePaths = lib.mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "/home/foo" "/mnt/foo" ];
+ description = ''
+ Paths that the server should have write access to.
+
+ This sets [`ReadWritePaths`](
+ https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#ReadWritePaths=
+ ), which allows specified paths to be written to through the default sandboxing.
+
+ See also `services.ollama.sandbox`.
'';
};
listenAddress = lib.mkOption {
@@ -59,8 +92,8 @@ in
type = types.attrsOf types.str;
default = { };
example = {
- HOME = "/tmp";
OLLAMA_LLM_LIBRARY = "cpu";
+ HIP_VISIBLE_DEVICES = "0,1";
};
description = ''
Set arbitrary environment variables for the ollama service.
@@ -87,7 +120,8 @@ in
ExecStart = "${lib.getExe ollamaPackage} serve";
WorkingDirectory = cfg.home;
StateDirectory = [ "ollama" ];
- DynamicUser = true;
+ DynamicUser = cfg.sandbox;
+ ReadWritePaths = cfg.writablePaths;
};
};
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;
+ '';
+ };
+ };
+ };
+}
diff --git a/nixos/modules/services/web-apps/firefly-iii.nix b/nixos/modules/services/web-apps/firefly-iii.nix
new file mode 100644
index 000000000000..b0024ce09c38
--- /dev/null
+++ b/nixos/modules/services/web-apps/firefly-iii.nix
@@ -0,0 +1,367 @@
+{ pkgs, config, lib, ... }:
+
+let
+ inherit (lib) optionalString mkDefault mkIf mkOption mkEnableOption literalExpression;
+ inherit (lib.types) nullOr attrsOf oneOf str int bool path package enum submodule;
+ inherit (lib.strings) concatMapStringsSep removePrefix toShellVars removeSuffix hasSuffix;
+ inherit (lib.attrsets) attrValues genAttrs filterAttrs mapAttrs' nameValuePair;
+ inherit (builtins) isInt isString toString typeOf;
+
+ cfg = config.services.firefly-iii;
+
+ user = cfg.user;
+ group = cfg.group;
+
+ defaultUser = "firefly-iii";
+ defaultGroup = "firefly-iii";
+
+ artisan = "${cfg.package}/artisan";
+
+ env-file-values = mapAttrs' (n: v: nameValuePair (removeSuffix "_FILE" n) v)
+ (filterAttrs (n: v: hasSuffix "_FILE" n) cfg.settings);
+ env-nonfile-values = filterAttrs (n: v: ! hasSuffix "_FILE" n) cfg.settings;
+
+ envfile = pkgs.writeText "firefly-iii-env" ''
+ ${toShellVars env-file-values}
+ ${toShellVars env-nonfile-values}
+ '';
+
+ fileenv-func = ''
+ cp --no-preserve=mode ${envfile} /tmp/firefly-iii-env
+ ${concatMapStringsSep "\n"
+ (n: "${pkgs.replace-secret}/bin/replace-secret ${n} ${n} /tmp/firefly-iii-env")
+ (attrValues env-file-values)}
+ set -a
+ . /tmp/firefly-iii-env
+ set +a
+ '';
+
+ firefly-iii-maintenance = pkgs.writeShellScript "firefly-iii-maintenance.sh" ''
+ ${fileenv-func}
+
+ ${optionalString (cfg.settings.DB_CONNECTION == "sqlite")
+ "touch ${cfg.dataDir}/storage/database/database.sqlite"}
+ ${artisan} migrate --seed --no-interaction --force
+ ${artisan} firefly-iii:decrypt-all
+ ${artisan} firefly-iii:upgrade-database
+ ${artisan} firefly-iii:correct-database
+ ${artisan} firefly-iii:report-integrity
+ ${artisan} firefly-iii:laravel-passport-keys
+ ${artisan} cache:clear
+
+ mv /tmp/firefly-iii-env /run/phpfpm/firefly-iii-env
+ '';
+
+ commonServiceConfig = {
+ Type = "oneshot";
+ User = user;
+ Group = group;
+ StateDirectory = "${removePrefix "/var/lib/" cfg.dataDir}";
+ WorkingDirectory = cfg.package;
+ PrivateTmp = true;
+ PrivateDevices = true;
+ CapabilityBoundingSet = "";
+ AmbientCapabilities = "";
+ ProtectSystem = "strict";
+ ProtectKernelTunables = true;
+ ProtectKernelModules = true;
+ ProtectControlGroups = true;
+ ProtectClock = true;
+ ProtectHostname = true;
+ ProtectHome = "tmpfs";
+ ProtectKernelLogs = true;
+ ProtectProc = "invisible";
+ ProcSubset = "pid";
+ PrivateNetwork = false;
+ RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX";
+ SystemCallArchitectures = "native";
+ SystemCallFilter = [
+ "@system-service @resources"
+ "~@obsolete @privileged"
+ ];
+ RestrictSUIDSGID = true;
+ RemoveIPC = true;
+ NoNewPrivileges = true;
+ RestrictRealtime = true;
+ RestrictNamespaces = true;
+ LockPersonality = true;
+ PrivateUsers = true;
+ };
+
+in {
+
+ options.services.firefly-iii = {
+
+ enable = mkEnableOption "Firefly III: A free and open source personal finance manager";
+
+ user = mkOption {
+ type = str;
+ default = defaultUser;
+ description = "User account under which firefly-iii runs.";
+ };
+
+ group = mkOption {
+ type = str;
+ default = if cfg.enableNginx then "nginx" else defaultGroup;
+ defaultText = "If `services.firefly-iii.enableNginx` is true then `nginx` else ${defaultGroup}";
+ description = ''
+ Group under which firefly-iii runs. It is best to set this to the group
+ of whatever webserver is being used as the frontend.
+ '';
+ };
+
+ dataDir = mkOption {
+ type = path;
+ default = "/var/lib/firefly-iii";
+ description = ''
+ The place where firefly-iii stores its state.
+ '';
+ };
+
+ package = mkOption {
+ type = package;
+ default = pkgs.firefly-iii;
+ defaultText = literalExpression "pkgs.firefly-iii";
+ description = ''
+ The firefly-iii package served by php-fpm and the webserver of choice.
+ This option can be used to point the webserver to the correct root. It
+ may also be used to set the package to a different version, say a
+ development version.
+ '';
+ apply = firefly-iii : firefly-iii.override (prev: {
+ dataDir = cfg.dataDir;
+ });
+ };
+
+ enableNginx = mkOption {
+ type = bool;
+ default = false;
+ description = ''
+ Whether to enable nginx or not. If enabled, an nginx virtual host will
+ be created for access to firefly-iii. If not enabled, then you may use
+ `''${config.services.firefly-iii.package}` as your document root in
+ whichever webserver you wish to setup.
+ '';
+ };
+
+ virtualHost = mkOption {
+ type = str;
+ description = ''
+ The hostname at which you wish firefly-iii to be served. If you have
+ enabled nginx using `services.firefly-iii.enableNginx` then this will
+ be used.
+ '';
+ };
+
+ poolConfig = mkOption {
+ type = attrsOf (oneOf [ str int bool ]);
+ default = {
+ "pm" = "dynamic";
+ "pm.max_children" = 32;
+ "pm.start_servers" = 2;
+ "pm.min_spare_servers" = 2;
+ "pm.max_spare_servers" = 4;
+ "pm.max_requests" = 500;
+ };
+ description = ''
+ Options for the Firefly III PHP pool. See the documentation on php-fpm.conf
+ for details on configuration directives.
+ '';
+ };
+
+ settings = mkOption {
+ description = ''
+ Options for firefly-iii configuration. Refer to
+ for
+ details on supported values. All