diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 2479b219a3bc268..0cff23251a09530 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1456,6 +1456,7 @@ ./services/web-servers/caddy/default.nix ./services/web-servers/darkhttpd.nix ./services/web-servers/fcgiwrap.nix + ./services/web-servers/fcgiwrap-instances.nix ./services/web-servers/garage.nix ./services/web-servers/hitch/default.nix ./services/web-servers/hydron.nix diff --git a/nixos/modules/services/misc/zoneminder.nix b/nixos/modules/services/misc/zoneminder.nix index d09cd87febfff15..469af04d106d067 100644 --- a/nixos/modules/services/misc/zoneminder.nix +++ b/nixos/modules/services/misc/zoneminder.nix @@ -202,10 +202,11 @@ in { ]; services = { - fcgiwrap = lib.mkIf useNginx { - enable = true; - preforkProcesses = cfg.cameras; - inherit user group; + fcgiwrap.instances.zoneminder = lib.mkIf useNginx { + process.prefork = cfg.cameras; + process.user = user; + process.group = group; + socket = { inherit (config.services.nginx) user group; }; }; mysql = lib.mkIf cfg.database.createLocally { @@ -225,9 +226,7 @@ in { default = true; root = "${pkg}/share/zoneminder/www"; listen = [ { addr = "0.0.0.0"; inherit (cfg) port; } ]; - extraConfig = let - fcgi = config.services.fcgiwrap; - in '' + extraConfig = '' index index.php; location / { @@ -257,7 +256,7 @@ in { fastcgi_param HTTP_PROXY ""; fastcgi_intercept_errors on; - fastcgi_pass ${fcgi.socketType}:${fcgi.socketAddress}; + fastcgi_pass unix:${config.services.fcgiwrap.instances.zoneminder.socket.address}; } location /cache/ { diff --git a/nixos/modules/services/networking/cgit.nix b/nixos/modules/services/networking/cgit.nix index 0ccbef756812e7b..c4ecc86c123537f 100644 --- a/nixos/modules/services/networking/cgit.nix +++ b/nixos/modules/services/networking/cgit.nix @@ -25,14 +25,14 @@ let regexLocation = cfg: regexEscape (stripLocation cfg); - mkFastcgiPass = cfg: '' + mkFastcgiPass = name: cfg: '' ${if cfg.nginx.location == "/" then '' fastcgi_param PATH_INFO $uri; '' else '' fastcgi_split_path_info ^(${regexLocation cfg})(/.+)$; fastcgi_param PATH_INFO $fastcgi_path_info; '' - }fastcgi_pass unix:${config.services.fcgiwrap.socketAddress}; + }fastcgi_pass unix:${config.services.fcgiwrap.instances."cgit-${name}".socket.address}; ''; cgitrcLine = name: value: "${name}=${ @@ -72,25 +72,11 @@ let ${cfg.extraConfig} ''; - mkCgitReposDir = cfg: - if cfg.scanPath != null then - cfg.scanPath - else - pkgs.runCommand "cgit-repos" { - preferLocalBuild = true; - allowSubstitutes = false; - } '' - mkdir -p "$out" - ${ - concatStrings ( - mapAttrsToList - (name: value: '' - ln -s ${escapeShellArg value.path} "$out"/${escapeShellArg name} - '') - cfg.repos - ) - } - ''; + fcgiwrapUnitName = name: "fcgiwrap-cgit-${name}"; + fcgiwrapRuntimeDir = name: "/run/${fcgiwrapUnitName name}"; + gitProjectRoot = name: cfg: if cfg.scanPath != null + then cfg.scanPath + else "${fcgiwrapRuntimeDir name}/repos"; in { @@ -154,6 +140,30 @@ in type = types.lines; default = ""; }; + + user = mkOption { + description = '' + User to run the cgit service as. + + Defaults to "root" for compatibility with legacy setups. + Will default to the unprivileged user "cgit" in NixOS 24.11. + ''; + type = types.str; + default = "root"; + example = "cgit"; + }; + + group = mkOption { + description = '' + Group to run the cgit service as. + + Defaults to "root" for compatibility with legacy setups. + Will default to the unprivileged user "cgit" in NixOS 24.11. + ''; + type = types.str; + default = "root"; + example = "cgit"; + }; }; })); }; @@ -165,18 +175,55 @@ in message = "Exactly one of services.cgit.${vhost}.scanPath or services.cgit.${vhost}.repos must be set."; }) cfgs; - services.fcgiwrap.enable = true; + warnings = flatten (flip mapAttrsToList cfgs (inst: cfg: + optional (cfg.user == "root") '' + `services.cgit.${inst}` is configured to run as root. + This has security implications. See advisory: https://discourse.nixos.org/t/51419 + It is recommended to set an unprivileged user explicitly. + This default user will be set to "cgit" in NixOS 24.11. + '' + )); + + users = mkMerge (flip mapAttrsToList cfgs (_: cfg: { + users.${cfg.user} = { + isSystemUser = true; + inherit (cfg) group; + }; + groups.${cfg.group} = { }; + })); + + services.fcgiwrap.instances = flip mapAttrs' cfgs (name: cfg: + nameValuePair "cgit-${name}" { + process = { inherit (cfg) user group; }; + socket = { inherit (config.services.nginx) user group; }; + } + ); + + systemd.services = flip mapAttrs' cfgs (name: cfg: + nameValuePair (fcgiwrapUnitName name) + (mkIf (cfg.repos != { }) { + serviceConfig.RuntimeDirectory = fcgiwrapUnitName name; + preStart = '' + GIT_PROJECT_ROOT=${escapeShellArg (gitProjectRoot name cfg)} + mkdir -p "$GIT_PROJECT_ROOT" + cd "$GIT_PROJECT_ROOT" + ${concatLines (flip mapAttrsToList cfg.repos (name: repo: '' + ln -s ${escapeShellArg repo.path} ${escapeShellArg name} + ''))} + ''; + } + )); services.nginx.enable = true; - services.nginx.virtualHosts = mkMerge (mapAttrsToList (_: cfg: { + services.nginx.virtualHosts = mkMerge (mapAttrsToList (name: cfg: { ${cfg.nginx.virtualHost} = { locations = ( genAttrs' [ "cgit.css" "cgit.png" "favicon.ico" "robots.txt" ] - (name: nameValuePair "= ${stripLocation cfg}/${name}" { + (fileName: nameValuePair "= ${stripLocation cfg}/${fileName}" { extraConfig = '' - alias ${cfg.package}/cgit/${name}; + alias ${cfg.package}/cgit/${fileName}; ''; }) ) // { @@ -184,10 +231,10 @@ in fastcgiParams = rec { SCRIPT_FILENAME = "${pkgs.git}/libexec/git-core/git-http-backend"; GIT_HTTP_EXPORT_ALL = "1"; - GIT_PROJECT_ROOT = mkCgitReposDir cfg; + GIT_PROJECT_ROOT = gitProjectRoot name cfg; HOME = GIT_PROJECT_ROOT; }; - extraConfig = mkFastcgiPass cfg; + extraConfig = mkFastcgiPass name cfg; }; "${stripLocation cfg}/" = { fastcgiParams = { @@ -196,7 +243,7 @@ in HTTP_HOST = "$server_name"; CGIT_CONFIG = mkCgitrc cfg; }; - extraConfig = mkFastcgiPass cfg; + extraConfig = mkFastcgiPass name cfg; }; }; }; diff --git a/nixos/modules/services/networking/smokeping.nix b/nixos/modules/services/networking/smokeping.nix index 3fb3eac45cc821a..9973c8cefbbcd82 100644 --- a/nixos/modules/services/networking/smokeping.nix +++ b/nixos/modules/services/networking/smokeping.nix @@ -328,6 +328,7 @@ in }; preStart = '' mkdir -m 0755 -p ${smokepingHome}/cache ${smokepingHome}/data + chown -R ${cfg.user}:${cfg.user} ${smokepingHome}/{cache,data} ln -snf ${cfg.package}/htdocs/css ${smokepingHome}/css ln -snf ${cfg.package}/htdocs/js ${smokepingHome}/js ln -snf ${cgiHome} ${smokepingHome}/smokeping.fcgi @@ -337,7 +338,11 @@ in }; # use nginx to serve the smokeping web service - services.fcgiwrap.enable = mkIf cfg.webService true; + services.fcgiwrap.instances.smokeping = mkIf cfg.webService { + process.user = cfg.user; + process.group = cfg.user; + socket = { inherit (config.services.nginx) user group; }; + }; services.nginx = mkIf cfg.webService { enable = true; virtualHosts."smokeping" = { @@ -349,7 +354,7 @@ in locations."/smokeping.fcgi" = { extraConfig = '' include ${config.services.nginx.package}/conf/fastcgi_params; - fastcgi_pass unix:${config.services.fcgiwrap.socketAddress}; + fastcgi_pass unix:${config.services.fcgiwrap.instances.smokeping.socket.address}; fastcgi_param SCRIPT_FILENAME ${smokepingHome}/smokeping.fcgi; fastcgi_param DOCUMENT_ROOT ${smokepingHome}; ''; diff --git a/nixos/modules/services/web-servers/fcgiwrap-instances.nix b/nixos/modules/services/web-servers/fcgiwrap-instances.nix new file mode 100644 index 000000000000000..4c02af7867d5453 --- /dev/null +++ b/nixos/modules/services/web-servers/fcgiwrap-instances.nix @@ -0,0 +1,136 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + forEachInstance = f: flip mapAttrs' config.services.fcgiwrap.instances ( + name: cfg: nameValuePair "fcgiwrap-${name}" (f cfg) + ); + +in { + options.services.fcgiwrap.instances = mkOption { + description = "Configuration for fcgiwrap instances."; + default = { }; + type = types.attrsOf (types.submodule ({ config, ... }: { options = { + process.prefork = mkOption { + type = types.ints.positive; + default = 1; + description = "Number of processes to prefork."; + }; + + process.user = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + User as which this instance of fcgiwrap will be run. + Set to `null` (the default) to use a dynamically allocated user. + ''; + }; + + process.group = mkOption { + type = types.nullOr types.str; + default = null; + description = "Group as which this instance of fcgiwrap will be run."; + }; + + socket.type = mkOption { + type = types.enum [ "unix" "tcp" "tcp6" ]; + default = "unix"; + description = "Socket type: 'unix', 'tcp' or 'tcp6'."; + }; + + socket.address = mkOption { + type = types.str; + default = "/run/fcgiwrap-${config._module.args.name}.sock"; + example = "1.2.3.4:5678"; + description = '' + Socket address. + In case of a UNIX socket, this should be its filesystem path. + ''; + }; + + socket.user = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + User to be set as owner of the UNIX socket. + ''; + }; + + socket.group = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Group to be set as owner of the UNIX socket. + ''; + }; + + socket.mode = mkOption { + type = types.nullOr types.str; + default = if config.socket.type == "unix" then "0600" else null; + defaultText = literalExpression '' + if config.socket.type == "unix" then "0600" else null + ''; + description = '' + Mode to be set on the UNIX socket. + Defaults to private to the socket's owner. + ''; + }; + }; })); + }; + + config = { + assertions = concatLists (mapAttrsToList (name: cfg: [ + { + assertion = cfg.socket.type == "unix" -> cfg.socket.user != null; + message = "Socket owner is required for the UNIX socket type."; + } + { + assertion = cfg.socket.type == "unix" -> cfg.socket.group != null; + message = "Socket owner is required for the UNIX socket type."; + } + { + assertion = cfg.socket.user != null -> cfg.socket.type == "unix"; + message = "Socket owner can only be set for the UNIX socket type."; + } + { + assertion = cfg.socket.group != null -> cfg.socket.type == "unix"; + message = "Socket owner can only be set for the UNIX socket type."; + } + { + assertion = cfg.socket.mode != null -> cfg.socket.type == "unix"; + message = "Socket mode can only be set for the UNIX socket type."; + } + ]) config.services.fcgiwrap.instances); + + systemd.services = forEachInstance (cfg: { + after = [ "nss-user-lookup.target" ]; + wantedBy = optional (cfg.socket.type != "unix") "multi-user.target"; + + serviceConfig = { + ExecStart = '' + ${pkgs.fcgiwrap}/sbin/fcgiwrap ${cli.toGNUCommandLineShell {} ({ + c = cfg.process.prefork; + } // (optionalAttrs (cfg.socket.type != "unix") { + s = "${cfg.socket.type}:${cfg.socket.address}"; + }))} + ''; + } // (if cfg.process.user != null then { + User = cfg.process.user; + Group = cfg.process.group; + } else { + DynamicUser = true; + }); + }); + + systemd.sockets = forEachInstance (cfg: mkIf (cfg.socket.type == "unix") { + wantedBy = [ "sockets.target" ]; + socketConfig = { + ListenStream = cfg.socket.address; + SocketUser = cfg.socket.user; + SocketGroup = cfg.socket.group; + SocketMode = cfg.socket.mode; + }; + }); + }; +} diff --git a/nixos/modules/services/web-servers/fcgiwrap.nix b/nixos/modules/services/web-servers/fcgiwrap.nix index 3250e9c05ed664d..1c89ae261bee106 100644 --- a/nixos/modules/services/web-servers/fcgiwrap.nix +++ b/nixos/modules/services/web-servers/fcgiwrap.nix @@ -4,6 +4,18 @@ with lib; let cfg = config.services.fcgiwrap; + deprecationNote = '' + + This global instance option is deprecated in favour of per-instance + options configured through `services.fcgiwrap.instances.*`. + ''; + securityWarning = '' + The fcgiwrap module is configured with a global shared instance. + This has security implications. See advisory: https://discourse.nixos.org/t/51419 + Isolated instances should instead be configured through `services.fcgiwrap.instances.*'. + The global options at `services.fcgiwrap.*` will be removed in NixOS 24.11. + ''; + in { options = { @@ -11,43 +23,66 @@ in { enable = mkOption { type = types.bool; default = false; - description = "Whether to enable fcgiwrap, a server for running CGI applications over FastCGI."; + description = "Whether to enable fcgiwrap, a server for running CGI applications over FastCGI." + deprecationNote; + }; + + allowGlobalInstanceLocalPrivilegeEscalation = mkOption { + type = types.bool; + default = false; + description = '' + The global instance of fcgiwrap configured through this module + has a local privilege escalation vulnerability. + Set this option to true to accept the risk and bypass the evaluation + failure regardless. + ''; }; preforkProcesses = mkOption { type = types.int; default = 1; - description = "Number of processes to prefork."; + description = "Number of processes to prefork." + deprecationNote; }; socketType = mkOption { type = types.enum [ "unix" "tcp" "tcp6" ]; default = "unix"; - description = "Socket type: 'unix', 'tcp' or 'tcp6'."; + description = "Socket type: 'unix', 'tcp' or 'tcp6'." + deprecationNote; }; socketAddress = mkOption { type = types.str; default = "/run/fcgiwrap.sock"; example = "1.2.3.4:5678"; - description = "Socket address. In case of a UNIX socket, this should be its filesystem path."; + description = "Socket address. In case of a UNIX socket, this should be its filesystem path." + deprecationNote; }; user = mkOption { type = types.nullOr types.str; default = null; - description = "User permissions for the socket."; + description = "User permissions for the socket." + deprecationNote; }; group = mkOption { type = types.nullOr types.str; default = null; - description = "Group permissions for the socket."; + description = "Group permissions for the socket." + deprecationNote; }; }; }; config = mkIf cfg.enable { + assertions = [ + { + assertion = cfg.allowGlobalInstanceLocalPrivilegeEscalation; + message = securityWarning + '' + To temporarily accept the risk and continue using the global instance, + set `services.fcgiwrap.allowGlobalInstanceLocalPrivilegeEscalation` to true. + ''; + } + ]; + + warnings = [ securityWarning ]; + systemd.services.fcgiwrap = { after = [ "nss-user-lookup.target" ]; wantedBy = optional (cfg.socketType != "unix") "multi-user.target";