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

nixos/fcgiwrap: refactor to fix permissions #318599

Merged
merged 11 commits into from
Jul 2, 2024
14 changes: 14 additions & 0 deletions nixos/doc/manual/release-notes/rl-2411.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@
it is set, instead of the previous hardcoded default of
`${networking.hostName}.${security.ipa.domain}`.

- The fcgiwrap module now allows multiple instances running as distinct users.
The option `services.fgciwrap` now takes an attribute set of the
configuration of each individual instance.
This requires migrating any previous configuration keys from
`services.fcgiwrap.*` to `services.fcgiwrap.some-instance.*`.
The ownership and mode of the UNIX sockets created by this service are now
configurable and private by default.
Processes also now run as a dynamically allocated user by default instead of
root.

- `services.cgit` now runs as the cgit user by default instead of root.
This change requires granting access to the repositories to this user or
setting the appropriate one through `services.cgit.some-instance.user`.

- `nvimpager` was updated to version 0.13.0, which changes the order of user and
nvimpager settings: user commands in `-c` and `--cmd` now override the
respective default settings because they are executed later.
Expand Down
14 changes: 6 additions & 8 deletions nixos/modules/services/misc/zoneminder.nix
Original file line number Diff line number Diff line change
Expand Up @@ -202,10 +202,10 @@ in {
];

services = {
fcgiwrap = lib.mkIf useNginx {
enable = true;
preforkProcesses = cfg.cameras;
inherit user group;
fcgiwrap.zoneminder = lib.mkIf useNginx {
process.prefork = cfg.cameras;
process.user = user;
process.group = group;
};

mysql = lib.mkIf cfg.database.createLocally {
Expand All @@ -225,9 +225,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 +255,7 @@ in {
fastcgi_param HTTP_PROXY "";
fastcgi_intercept_errors on;

fastcgi_pass ${fcgi.socketType}:${fcgi.socketAddress};
fastcgi_pass unix:${config.services.fcgiwrap.zoneminder.socket.address};
}

location /cache/ {
Expand Down
82 changes: 54 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."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,18 @@ in
type = types.lines;
default = "";
};

user = mkOption {
description = "User to run the cgit service as.";
type = types.str;
default = "cgit";
};

group = mkOption {
description = "Group to run the cgit service as.";
type = types.str;
default = "cgit";
};
};
}));
};
Expand All @@ -165,29 +163,57 @@ in
message = "Exactly one of services.cgit.${vhost}.scanPath or services.cgit.${vhost}.repos must be set.";
}) cfgs;

services.fcgiwrap.enable = true;
users = mkMerge (flip mapAttrsToList cfgs (_: cfg: {
users.${cfg.user} = {
isSystemUser = true;
inherit (cfg) group;
};
groups.${cfg.group} = { };
}));

services.fcgiwrap = 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 +222,7 @@ in
HTTP_HOST = "$server_name";
CGIT_CONFIG = mkCgitrc cfg;
};
extraConfig = mkFastcgiPass cfg;
extraConfig = mkFastcgiPass name cfg;
};
};
};
Expand Down
8 changes: 6 additions & 2 deletions nixos/modules/services/networking/smokeping.nix
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,11 @@ in
};

# use nginx to serve the smokeping web service
services.fcgiwrap.enable = mkIf cfg.webService true;
services.fcgiwrap.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" = {
Expand All @@ -349,7 +353,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.smokeping.socket.address};
fastcgi_param SCRIPT_FILENAME ${smokepingHome}/smokeping.fcgi;
fastcgi_param DOCUMENT_ROOT ${smokepingHome};
'';
Expand Down
130 changes: 94 additions & 36 deletions nixos/modules/services/web-servers/fcgiwrap.nix
Original file line number Diff line number Diff line change
Expand Up @@ -3,70 +3,128 @@
with lib;

let
cfg = config.services.fcgiwrap;
forEachInstance = f: flip mapAttrs' config.services.fcgiwrap (name: cfg:
nameValuePair "fcgiwrap-${name}" (f cfg)
);

in {
options.services.fcgiwrap = 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.";
};

options = {
services.fcgiwrap = {
enable = mkOption {
type = types.bool;
default = false;
description = "Whether to enable fcgiwrap, a server for running CGI applications over FastCGI.";
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.
'';
};

preforkProcesses = mkOption {
type = types.int;
default = 1;
description = "Number of processes to prefork.";
process.group = mkOption {
type = types.nullOr types.str;
default = null;
description = "Group as which this instance of fcgiwrap will be run.";
};

socketType = mkOption {
socket.type = mkOption {
type = types.enum [ "unix" "tcp" "tcp6" ];
default = "unix";
description = "Socket type: 'unix', 'tcp' or 'tcp6'.";
};

socketAddress = mkOption {
socket.address = mkOption {
type = types.str;
default = "/run/fcgiwrap.sock";
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.";
description = ''
Socket address.
In case of a UNIX socket, this should be its filesystem path.
'';
};

user = mkOption {
socket.user = mkOption {
type = types.nullOr types.str;
default = null;
description = "User permissions for the socket.";
description = ''
User to be set as owner of the UNIX socket.
Defaults to the process running user.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not true (also in the case of true). If unset, it defaults to the empty string in the systemd configuration which results in the socket being owned by root.

Copy link
Contributor Author

@pacien pacien Jul 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!
While fixing the options, I copied the mistake from the previous documentation…
Here are two possible solutions (pick one):

'';
};

group = mkOption {
socket.group = mkOption {
type = types.nullOr types.str;
default = null;
description = "Group permissions for the socket.";
description = ''
Group to be set as owner of the UNIX socket.
Defaults to the process running group.
'';
};
};

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 = mkIf cfg.enable {
systemd.services.fcgiwrap = {
config = {
assertions = concatLists (mapAttrsToList (name: cfg: [
{
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);

systemd.services = forEachInstance (cfg: {
after = [ "nss-user-lookup.target" ];
wantedBy = optional (cfg.socketType != "unix") "multi-user.target";
wantedBy = optional (cfg.socket.type != "unix") "multi-user.target";

serviceConfig = {
ExecStart = "${pkgs.fcgiwrap}/sbin/fcgiwrap -c ${builtins.toString cfg.preforkProcesses} ${
optionalString (cfg.socketType != "unix") "-s ${cfg.socketType}:${cfg.socketAddress}"
}";
} // (if cfg.user != null && cfg.group != null then {
User = cfg.user;
Group = cfg.group;
} else { } );
};
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 = if (cfg.socketType == "unix") then {
fcgiwrap = {
wantedBy = [ "sockets.target" ];
socketConfig.ListenStream = cfg.socketAddress;
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;
};
} else { };
});
};
}
Loading