diff options
author | github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> | 2024-04-20 12:01:42 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-04-20 12:01:42 +0000 |
commit | 3aa8ef1d54f4910c8bf22a420374b8145cc60cfa (patch) | |
tree | c007c71218715c4fc8701de4b9266188ec16ed78 /nixos | |
parent | 917d4f32ffa84fbcc67f52589f7867a9d9391132 (diff) | |
parent | 3d8a73d563851361fe53855176b13315e2175672 (diff) |
Merge staging-next into staging
Diffstat (limited to 'nixos')
-rw-r--r-- | nixos/doc/manual/release-notes/rl-2405.section.md | 4 | ||||
-rw-r--r-- | nixos/modules/module-list.nix | 2 | ||||
-rw-r--r-- | nixos/modules/services/misc/ollama.nix | 38 | ||||
-rw-r--r-- | nixos/modules/services/networking/netbird/coturn.nix | 160 | ||||
-rw-r--r-- | nixos/modules/services/networking/netbird/dashboard.nix | 186 | ||||
-rw-r--r-- | nixos/modules/services/networking/netbird/management.nix | 460 | ||||
-rw-r--r-- | nixos/modules/services/networking/netbird/server.md | 42 | ||||
-rw-r--r-- | nixos/modules/services/networking/netbird/server.nix | 67 | ||||
-rw-r--r-- | nixos/modules/services/networking/netbird/signal.nix | 123 | ||||
-rw-r--r-- | nixos/modules/services/web-apps/firefly-iii.nix | 367 | ||||
-rw-r--r-- | nixos/release-combined.nix | 5 | ||||
-rw-r--r-- | nixos/release-small.nix | 1 | ||||
-rw-r--r-- | nixos/tests/all-tests.nix | 1 | ||||
-rw-r--r-- | nixos/tests/firefly-iii.nix | 26 |
14 files changed, 1479 insertions, 3 deletions
diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md index 20bcb47dc571f..093995d13443a 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 2a25561047316..936cea1f3d51d 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 948c8f17f9894..c0341984aa351 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 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; + ''; + }; + }; + }; +} 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 0000000000000..b0024ce09c38e --- /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 <literal>php-fpm.conf</literal> + for details on configuration directives. + ''; + }; + + settings = mkOption { + description = '' + Options for firefly-iii configuration. Refer to + <https://github.com/firefly-iii/firefly-iii/blob/main/.env.example> for + details on supported values. All <option>_FILE values supported by + upstream are supported here. + + APP_URL will be set by `services.firefly-iii.virtualHost`, do not + redefine it here. + ''; + example = literalExpression '' + { + APP_ENV = "production"; + APP_KEY_FILE = "/var/secrets/firefly-iii-app-key.txt"; + SITE_OWNER = "mail@example.com"; + DB_CONNECTION = "mysql"; + DB_HOST = "db"; + DB_PORT = 3306; + DB_DATABASE = "firefly"; + DB_USERNAME = "firefly"; + DB_PASSWORD_FILE = "/var/secrets/firefly-iii-mysql-password.txt; + } + ''; + default = {}; + type = submodule { + freeformType = attrsOf (oneOf [str int bool]); + options = { + DB_CONNECTION = mkOption { + type = enum [ "sqlite" "pgsql" "mysql" ]; + default = "sqlite"; + example = "pgsql"; + description = '' + The type of database you wish to use. Can be one of "sqlite", + "mysql" or "pgsql". + ''; + }; + APP_ENV = mkOption { + type = enum [ "local" "production" "testing" ]; + default = "local"; + example = "production"; + description = '' + The app environment. It is recommended to keep this at "local". + Possible values are "local", "production" and "testing" + ''; + }; + DB_PORT = mkOption { + type = nullOr int; + default = if cfg.settings.DB_CONNECTION == "sqlite" then null + else if cfg.settings.DB_CONNECTION == "mysql" then 3306 + else 5432; + defaultText = '' + `null` if DB_CONNECTION is "sqlite", `3306` if "mysql", `5432` if "pgsql" + ''; + description = '' + The port your database is listening at. sqlite does not require + this value to be filled. + ''; + }; + APP_KEY_FILE = mkOption { + type = path; + description = '' + The path to your appkey. The file should contain a 32 character + random app key. This may be set using `echo "base64:$(head -c 32 + /dev/urandom | base64)" > /path/to/key-file`. + ''; + }; + }; + }; + }; + }; + + config = mkIf cfg.enable { + + services.firefly-iii = { + settings = { + APP_URL = cfg.virtualHost; + }; + }; + + services.phpfpm.pools.firefly-iii = { + inherit user group; + phpPackage = cfg.package.phpPackage; + phpOptions = '' + log_errors = on + ''; + settings = { + "listen.mode" = "0660"; + "listen.owner" = user; + "listen.group" = group; + "clear_env" = "no"; + } // cfg.poolConfig; + }; + + systemd.services.phpfpm-firefly-iii.serviceConfig = { + EnvironmentFile = "/run/phpfpm/firefly-iii-env"; + ExecStartPost = "${pkgs.coreutils}/bin/rm /run/phpfpm/firefly-iii-env"; + }; + + systemd.services.firefly-iii-setup = { + requiredBy = [ "phpfpm-firefly-iii.service" ]; + before = [ "phpfpm-firefly-iii.service" ]; + serviceConfig = { + ExecStart = firefly-iii-maintenance; + RuntimeDirectory = "phpfpm"; + RuntimeDirectoryPreserve = true; + } // commonServiceConfig; + unitConfig.JoinsNamespaceOf = "phpfpm-firefly-iii.service"; + }; + + systemd.services.firefly-iii-cron = { + description = "Daily Firefly III cron job"; + script = '' + ${fileenv-func} + ${artisan} firefly-iii:cron + ''; + serviceConfig = commonServiceConfig; + }; + + systemd.timers.firefly-iii-cron = { + description = "Trigger Firefly Cron"; + timerConfig = { + OnCalendar = "Daily"; + RandomizedDelaySec = "1800s"; + Persistent = true; + }; + wantedBy = [ "timers.target" ]; + }; + + services.nginx = mkIf cfg.enableNginx { + enable = true; + recommendedTlsSettings = mkDefault true; + recommendedOptimisation = mkDefault true; + recommendedGzipSettings = mkDefault true; + virtualHosts.${cfg.virtualHost} = { + root = "${cfg.package}/public"; + locations = { + "/" = { + tryFiles = "$uri $uri/ /index.php?$query_string"; + index = "index.php"; + extraConfig = '' + sendfile off; + ''; + }; + "~ \.php$" = { + extraConfig = '' + include ${config.services.nginx.package}/conf/fastcgi_params ; + fastcgi_param SCRIPT_FILENAME $request_filename; + fastcgi_param modHeadersAvailable true; #Avoid sending the security headers twice + fastcgi_pass unix:${config.services.phpfpm.pools.firefly-iii.socket}; + ''; + }; + }; + }; + }; + + systemd.tmpfiles.settings."10-firefly-iii" = genAttrs [ + "${cfg.dataDir}/storage" + "${cfg.dataDir}/storage/app" + "${cfg.dataDir}/storage/database" + "${cfg.dataDir}/storage/export" + "${cfg.dataDir}/storage/framework" + "${cfg.dataDir}/storage/framework/cache" + "${cfg.dataDir}/storage/framework/sessions" + "${cfg.dataDir}/storage/framework/views" + "${cfg.dataDir}/storage/logs" + "${cfg.dataDir}/storage/upload" + "${cfg.dataDir}/cache" + ] (n: { + d = { + group = group; + mode = "0700"; + user = user; + }; + }) // { + "${cfg.dataDir}".d = { + group = group; + mode = "0710"; + user = user; + }; + }; + + users = { + users = mkIf (user == defaultUser) { + ${defaultUser} = { + description = "Firefly-iii service user"; + inherit group; + isSystemUser = true; + home = cfg.dataDir; + }; + }; + groups = mkIf (group == defaultGroup) { + ${defaultGroup} = {}; + }; + }; + }; +} diff --git a/nixos/release-combined.nix b/nixos/release-combined.nix index 96b24feeb0631..39cbe85579ae9 100644 --- a/nixos/release-combined.nix +++ b/nixos/release-combined.nix @@ -168,7 +168,10 @@ in rec { (onFullSupported "nixpkgs.emacs") (onFullSupported "nixpkgs.jdk") (onSystems ["x86_64-linux"] "nixpkgs.mesa_i686") # i686 sanity check + useful - ["nixpkgs.tarball"] + [ + "nixpkgs.tarball" + "nixpkgs.release-checks" + ] ]; }; } diff --git a/nixos/release-small.nix b/nixos/release-small.nix index cac20b63925f0..d4de2fd8df4fb 100644 --- a/nixos/release-small.nix +++ b/nixos/release-small.nix @@ -103,6 +103,7 @@ in rec { [ "nixos.channel" "nixpkgs.tarball" + "nixpkgs.release-checks" ] (map (onSystems [ "x86_64-linux" ]) [ "nixos.tests.boot.biosCdrom" diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 23c5a94ed12ca..5abcbabf18e11 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -308,6 +308,7 @@ in { ferm = handleTest ./ferm.nix {}; ferretdb = handleTest ./ferretdb.nix {}; filesystems-overlayfs = runTest ./filesystems-overlayfs.nix; + firefly-iii = handleTest ./firefly-iii.nix {}; firefox = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox; }; firefox-beta = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-beta; }; firefox-devedition = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-devedition; }; diff --git a/nixos/tests/firefly-iii.nix b/nixos/tests/firefly-iii.nix new file mode 100644 index 0000000000000..c93d799320a48 --- /dev/null +++ b/nixos/tests/firefly-iii.nix @@ -0,0 +1,26 @@ +import ./make-test-python.nix ({ lib, pkgs, ... }: { + name = "firefly-iii"; + meta.maintainers = [ lib.maintainers.savyajha ]; + + nodes.machine = { config, ... }: { + environment.etc = { + "firefly-iii-appkey".text = "TestTestTestTestTestTestTestTest"; + }; + services.firefly-iii = { + enable = true; + virtualHost = "http://localhost"; + enableNginx = true; + settings = { + APP_KEY_FILE = "/etc/firefly-iii-appkey"; + LOG_CHANNEL = "stdout"; + SITE_OWNER = "mail@example.com"; + }; + }; + }; + + testScript = '' + machine.wait_for_unit("phpfpm-firefly-iii.service") + machine.wait_for_unit("nginx.service") + machine.succeed("curl -fvvv -Ls http://localhost/ | grep 'Firefly III'") + ''; +}) |