Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[24.05] backport fcgiwrap instances fix for local privilege escalation issue #331465

Merged
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 7 additions & 8 deletions nixos/modules/services/misc/zoneminder.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 / {
Expand Down Expand Up @@ -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/ {
Expand Down
103 changes: 75 additions & 28 deletions nixos/modules/services/networking/cgit.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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}=${
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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";
};
};
}));
};
Expand All @@ -165,29 +175,66 @@ 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};
'';
})
) // {
"~ ${regexLocation cfg}/.+/(info/refs|git-upload-pack)" = {
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 = {
Expand All @@ -196,7 +243,7 @@ in
HTTP_HOST = "$server_name";
CGIT_CONFIG = mkCgitrc cfg;
};
extraConfig = mkFastcgiPass cfg;
extraConfig = mkFastcgiPass name cfg;
};
};
};
Expand Down
9 changes: 7 additions & 2 deletions nixos/modules/services/networking/smokeping.nix
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ in
};
preStart = ''
mkdir -m 0755 -p ${smokepingHome}/cache ${smokepingHome}/data
chown -R ${cfg.user}:${cfg.user} ${smokepingHome}/{cache,data}
bendlas marked this conversation as resolved.
Show resolved Hide resolved
ln -snf ${cfg.package}/htdocs/css ${smokepingHome}/css
ln -snf ${cfg.package}/htdocs/js ${smokepingHome}/js
ln -snf ${cgiHome} ${smokepingHome}/smokeping.fcgi
Expand All @@ -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;
bendlas marked this conversation as resolved.
Show resolved Hide resolved
socket = { inherit (config.services.nginx) user group; };
};
services.nginx = mkIf cfg.webService {
enable = true;
virtualHosts."smokeping" = {
Expand All @@ -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};
'';
Expand Down
136 changes: 136 additions & 0 deletions nixos/modules/services/web-servers/fcgiwrap-instances.nix
Original file line number Diff line number Diff line change
@@ -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;
};
});
};
}
Loading