Merge pull request #318599 from pacien/nixos-fcgiwrap-isolation

nixos/fcgiwrap: refactor to fix permissions
This commit is contained in:
Thomas Gerbet 2024-07-02 21:52:33 +02:00 committed by GitHub
commit 8ddb1bb721
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 187 additions and 82 deletions

View File

@ -60,6 +60,20 @@
it is set, instead of the previous hardcoded default of it is set, instead of the previous hardcoded default of
`${networking.hostName}.${security.ipa.domain}`. `${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` 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 nvimpager settings: user commands in `-c` and `--cmd` now override the
respective default settings because they are executed later. respective default settings because they are executed later.

View File

@ -202,10 +202,10 @@ in {
]; ];
services = { services = {
fcgiwrap = lib.mkIf useNginx { fcgiwrap.zoneminder = lib.mkIf useNginx {
enable = true; process.prefork = cfg.cameras;
preforkProcesses = cfg.cameras; process.user = user;
inherit user group; process.group = group;
}; };
mysql = lib.mkIf cfg.database.createLocally { mysql = lib.mkIf cfg.database.createLocally {
@ -225,9 +225,7 @@ in {
default = true; default = true;
root = "${pkg}/share/zoneminder/www"; root = "${pkg}/share/zoneminder/www";
listen = [ { addr = "0.0.0.0"; inherit (cfg) port; } ]; listen = [ { addr = "0.0.0.0"; inherit (cfg) port; } ];
extraConfig = let extraConfig = ''
fcgi = config.services.fcgiwrap;
in ''
index index.php; index index.php;
location / { location / {
@ -257,7 +255,7 @@ in {
fastcgi_param HTTP_PROXY ""; fastcgi_param HTTP_PROXY "";
fastcgi_intercept_errors on; fastcgi_intercept_errors on;
fastcgi_pass ${fcgi.socketType}:${fcgi.socketAddress}; fastcgi_pass unix:${config.services.fcgiwrap.zoneminder.socket.address};
} }
location /cache/ { location /cache/ {

View File

@ -25,14 +25,14 @@ let
regexLocation = cfg: regexEscape (stripLocation cfg); regexLocation = cfg: regexEscape (stripLocation cfg);
mkFastcgiPass = cfg: '' mkFastcgiPass = name: cfg: ''
${if cfg.nginx.location == "/" then '' ${if cfg.nginx.location == "/" then ''
fastcgi_param PATH_INFO $uri; fastcgi_param PATH_INFO $uri;
'' else '' '' else ''
fastcgi_split_path_info ^(${regexLocation cfg})(/.+)$; fastcgi_split_path_info ^(${regexLocation cfg})(/.+)$;
fastcgi_param PATH_INFO $fastcgi_path_info; 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}=${ cgitrcLine = name: value: "${name}=${
@ -72,25 +72,11 @@ let
${cfg.extraConfig} ${cfg.extraConfig}
''; '';
mkCgitReposDir = cfg: fcgiwrapUnitName = name: "fcgiwrap-cgit-${name}";
if cfg.scanPath != null then fcgiwrapRuntimeDir = name: "/run/${fcgiwrapUnitName name}";
cfg.scanPath gitProjectRoot = name: cfg: if cfg.scanPath != null
else then cfg.scanPath
pkgs.runCommand "cgit-repos" { else "${fcgiwrapRuntimeDir name}/repos";
preferLocalBuild = true;
allowSubstitutes = false;
} ''
mkdir -p "$out"
${
concatStrings (
mapAttrsToList
(name: value: ''
ln -s ${escapeShellArg value.path} "$out"/${escapeShellArg name}
'')
cfg.repos
)
}
'';
in in
{ {
@ -154,6 +140,18 @@ in
type = types.lines; type = types.lines;
default = ""; 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";
};
}; };
})); }));
}; };
@ -165,18 +163,46 @@ in
message = "Exactly one of services.cgit.${vhost}.scanPath or services.cgit.${vhost}.repos must be set."; message = "Exactly one of services.cgit.${vhost}.scanPath or services.cgit.${vhost}.repos must be set.";
}) cfgs; }) 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.enable = true;
services.nginx.virtualHosts = mkMerge (mapAttrsToList (_: cfg: { services.nginx.virtualHosts = mkMerge (mapAttrsToList (name: cfg: {
${cfg.nginx.virtualHost} = { ${cfg.nginx.virtualHost} = {
locations = ( locations = (
genAttrs' genAttrs'
[ "cgit.css" "cgit.png" "favicon.ico" "robots.txt" ] [ "cgit.css" "cgit.png" "favicon.ico" "robots.txt" ]
(name: nameValuePair "= ${stripLocation cfg}/${name}" { (fileName: nameValuePair "= ${stripLocation cfg}/${fileName}" {
extraConfig = '' extraConfig = ''
alias ${cfg.package}/cgit/${name}; alias ${cfg.package}/cgit/${fileName};
''; '';
}) })
) // { ) // {
@ -184,10 +210,10 @@ in
fastcgiParams = rec { fastcgiParams = rec {
SCRIPT_FILENAME = "${pkgs.git}/libexec/git-core/git-http-backend"; SCRIPT_FILENAME = "${pkgs.git}/libexec/git-core/git-http-backend";
GIT_HTTP_EXPORT_ALL = "1"; GIT_HTTP_EXPORT_ALL = "1";
GIT_PROJECT_ROOT = mkCgitReposDir cfg; GIT_PROJECT_ROOT = gitProjectRoot name cfg;
HOME = GIT_PROJECT_ROOT; HOME = GIT_PROJECT_ROOT;
}; };
extraConfig = mkFastcgiPass cfg; extraConfig = mkFastcgiPass name cfg;
}; };
"${stripLocation cfg}/" = { "${stripLocation cfg}/" = {
fastcgiParams = { fastcgiParams = {
@ -196,7 +222,7 @@ in
HTTP_HOST = "$server_name"; HTTP_HOST = "$server_name";
CGIT_CONFIG = mkCgitrc cfg; CGIT_CONFIG = mkCgitrc cfg;
}; };
extraConfig = mkFastcgiPass cfg; extraConfig = mkFastcgiPass name cfg;
}; };
}; };
}; };

View File

@ -337,7 +337,11 @@ in
}; };
# use nginx to serve the smokeping web service # 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 { services.nginx = mkIf cfg.webService {
enable = true; enable = true;
virtualHosts."smokeping" = { virtualHosts."smokeping" = {
@ -349,7 +353,7 @@ in
locations."/smokeping.fcgi" = { locations."/smokeping.fcgi" = {
extraConfig = '' extraConfig = ''
include ${config.services.nginx.package}/conf/fastcgi_params; 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 SCRIPT_FILENAME ${smokepingHome}/smokeping.fcgi;
fastcgi_param DOCUMENT_ROOT ${smokepingHome}; fastcgi_param DOCUMENT_ROOT ${smokepingHome};
''; '';

View File

@ -3,70 +3,128 @@
with lib; with lib;
let let
cfg = config.services.fcgiwrap; forEachInstance = f: flip mapAttrs' config.services.fcgiwrap (name: cfg:
nameValuePair "fcgiwrap-${name}" (f cfg)
);
in { in {
options.services.fcgiwrap = mkOption {
options = { description = "Configuration for fcgiwrap instances.";
services.fcgiwrap = { default = { };
enable = mkOption { type = types.attrsOf (types.submodule ({ config, ... }: { options = {
type = types.bool; process.prefork = mkOption {
default = false; type = types.ints.positive;
description = "Whether to enable fcgiwrap, a server for running CGI applications over FastCGI.";
};
preforkProcesses = mkOption {
type = types.int;
default = 1; default = 1;
description = "Number of processes to prefork."; description = "Number of processes to prefork.";
}; };
socketType = mkOption { 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" ]; type = types.enum [ "unix" "tcp" "tcp6" ];
default = "unix"; default = "unix";
description = "Socket type: 'unix', 'tcp' or 'tcp6'."; description = "Socket type: 'unix', 'tcp' or 'tcp6'.";
}; };
socketAddress = mkOption { socket.address = mkOption {
type = types.str; type = types.str;
default = "/run/fcgiwrap.sock"; default = "/run/fcgiwrap-${config._module.args.name}.sock";
example = "1.2.3.4:5678"; 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; type = types.nullOr types.str;
default = null; 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.
'';
}; };
group = mkOption { socket.group = mkOption {
type = types.nullOr types.str; type = types.nullOr types.str;
default = null; 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 { config = {
systemd.services.fcgiwrap = { 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" ]; after = [ "nss-user-lookup.target" ];
wantedBy = optional (cfg.socketType != "unix") "multi-user.target"; wantedBy = optional (cfg.socket.type != "unix") "multi-user.target";
serviceConfig = { serviceConfig = {
ExecStart = "${pkgs.fcgiwrap}/sbin/fcgiwrap -c ${builtins.toString cfg.preforkProcesses} ${ ExecStart = ''
optionalString (cfg.socketType != "unix") "-s ${cfg.socketType}:${cfg.socketAddress}" ${pkgs.fcgiwrap}/sbin/fcgiwrap ${cli.toGNUCommandLineShell {} ({
}"; c = cfg.process.prefork;
} // (if cfg.user != null && cfg.group != null then { } // (optionalAttrs (cfg.socket.type != "unix") {
User = cfg.user; s = "${cfg.socket.type}:${cfg.socket.address}";
Group = cfg.group; }))}
} else { } ); '';
}; } // (if cfg.process.user != null then {
User = cfg.process.user;
Group = cfg.process.group;
} else {
DynamicUser = true;
});
});
systemd.sockets = if (cfg.socketType == "unix") then { systemd.sockets = forEachInstance (cfg: mkIf (cfg.socket.type == "unix") {
fcgiwrap = { wantedBy = [ "sockets.target" ];
wantedBy = [ "sockets.target" ]; socketConfig = {
socketConfig.ListenStream = cfg.socketAddress; ListenStream = cfg.socket.address;
SocketUser = cfg.socket.user;
SocketGroup = cfg.socket.group;
SocketMode = cfg.socket.mode;
}; };
} else { }; });
}; };
} }

View File

@ -23,7 +23,7 @@ in {
nginx.location = "/(c)git/"; nginx.location = "/(c)git/";
repos = { repos = {
some-repo = { some-repo = {
path = "/srv/git/some-repo"; path = "/tmp/git/some-repo";
desc = "some-repo description"; desc = "some-repo description";
}; };
}; };
@ -50,12 +50,12 @@ in {
server.fail("curl -fsS http://localhost/robots.txt") server.fail("curl -fsS http://localhost/robots.txt")
server.succeed("${pkgs.writeShellScript "setup-cgit-test-repo" '' server.succeed("sudo -u cgit ${pkgs.writeShellScript "setup-cgit-test-repo" ''
set -e set -e
git init --bare -b master /srv/git/some-repo git init --bare -b master /tmp/git/some-repo
git init -b master reference git init -b master reference
cd reference cd reference
git remote add origin /srv/git/some-repo git remote add origin /tmp/git/some-repo
date > date.txt date > date.txt
git add date.txt git add date.txt
git -c user.name=test -c user.email=test@localhost commit -m 'add date' git -c user.name=test -c user.email=test@localhost commit -m 'add date'

View File

@ -24,7 +24,12 @@ import ./make-test-python.nix (
{ {
networking.firewall.allowedTCPPorts = [ 80 ]; networking.firewall.allowedTCPPorts = [ 80 ];
services.fcgiwrap.enable = true; services.fcgiwrap.gitolite = {
process.user = "gitolite";
process.group = "gitolite";
socket = { inherit (config.services.nginx) user group; };
};
services.gitolite = { services.gitolite = {
enable = true; enable = true;
adminPubkey = adminPublicKey; adminPubkey = adminPublicKey;
@ -59,7 +64,7 @@ import ./make-test-python.nix (
fastcgi_param SCRIPT_FILENAME ${pkgs.gitolite}/bin/gitolite-shell; fastcgi_param SCRIPT_FILENAME ${pkgs.gitolite}/bin/gitolite-shell;
# use Unix domain socket or inet socket # use Unix domain socket or inet socket
fastcgi_pass unix:/run/fcgiwrap.sock; fastcgi_pass unix:${config.services.fcgiwrap.gitolite.socket.address};
''; '';
}; };
@ -82,7 +87,7 @@ import ./make-test-python.nix (
server.wait_for_unit("gitolite-init.service") server.wait_for_unit("gitolite-init.service")
server.wait_for_unit("nginx.service") server.wait_for_unit("nginx.service")
server.wait_for_file("/run/fcgiwrap.sock") server.wait_for_file("/run/fcgiwrap-gitolite.sock")
client.wait_for_unit("multi-user.target") client.wait_for_unit("multi-user.target")
client.succeed( client.succeed(