nixos/uwsgi: add support for POSIX capabilities

This commit is contained in:
rnhmjoj 2020-11-06 09:58:49 +01:00
parent 01c7c2815c
commit c00240e41e
No known key found for this signature in database
GPG Key ID: BFBAF4C975F76450
2 changed files with 104 additions and 28 deletions

View File

@ -5,11 +5,24 @@ with lib;
let let
cfg = config.services.uwsgi; cfg = config.services.uwsgi;
isEmperor = cfg.instance.type == "emperor";
imperialPowers =
[
# spawn other user processes
"CAP_SETUID" "CAP_SETGID"
"CAP_SYS_CHROOT"
# transfer capabilities
"CAP_SETPCAP"
# create other user sockets
"CAP_CHOWN"
];
buildCfg = name: c: buildCfg = name: c:
let let
plugins = plugins =
if any (n: !any (m: m == n) cfg.plugins) (c.plugins or []) if any (n: !any (m: m == n) cfg.plugins) (c.plugins or [])
then throw "`plugins` attribute in UWSGI configuration contains plugins not in config.services.uwsgi.plugins" then throw "`plugins` attribute in uWSGI configuration contains plugins not in config.services.uwsgi.plugins"
else c.plugins or cfg.plugins; else c.plugins or cfg.plugins;
hasPython = v: filter (n: n == "python${v}") plugins != []; hasPython = v: filter (n: n == "python${v}") plugins != [];
@ -18,7 +31,7 @@ let
python = python =
if hasPython2 && hasPython3 then if hasPython2 && hasPython3 then
throw "`plugins` attribute in UWSGI configuration shouldn't contain both python2 and python3" throw "`plugins` attribute in uWSGI configuration shouldn't contain both python2 and python3"
else if hasPython2 then cfg.package.python2 else if hasPython2 then cfg.package.python2
else if hasPython3 then cfg.package.python3 else if hasPython3 then cfg.package.python3
else null; else null;
@ -43,7 +56,7 @@ let
oldPaths = filter (x: x != null) (map getPath env'); oldPaths = filter (x: x != null) (map getPath env');
in env' ++ [ "PATH=${optionalString (oldPaths != []) "${last oldPaths}:"}${pythonEnv}/bin" ]; in env' ++ [ "PATH=${optionalString (oldPaths != []) "${last oldPaths}:"}${pythonEnv}/bin" ];
} }
else if c.type == "emperor" else if isEmperor
then { then {
emperor = if builtins.typeOf c.vassals != "set" then c.vassals emperor = if builtins.typeOf c.vassals != "set" then c.vassals
else pkgs.buildEnv { else pkgs.buildEnv {
@ -51,7 +64,7 @@ let
paths = mapAttrsToList buildCfg c.vassals; paths = mapAttrsToList buildCfg c.vassals;
}; };
} // removeAttrs c [ "type" "vassals" ] } // removeAttrs c [ "type" "vassals" ]
else throw "`type` attribute in UWSGI configuration should be either 'normal' or 'emperor'"; else throw "`type` attribute in uWSGI configuration should be either 'normal' or 'emperor'";
}; };
in pkgs.writeTextDir "${name}.json" (builtins.toJSON uwsgiCfg); in pkgs.writeTextDir "${name}.json" (builtins.toJSON uwsgiCfg);
@ -79,7 +92,7 @@ in {
}; };
instance = mkOption { instance = mkOption {
type = with lib.types; let type = with types; let
valueType = nullOr (oneOf [ valueType = nullOr (oneOf [
bool bool
int int
@ -137,31 +150,65 @@ in {
user = mkOption { user = mkOption {
type = types.str; type = types.str;
default = "uwsgi"; default = "uwsgi";
description = "User account under which uwsgi runs."; description = "User account under which uWSGI runs.";
}; };
group = mkOption { group = mkOption {
type = types.str; type = types.str;
default = "uwsgi"; default = "uwsgi";
description = "Group account under which uwsgi runs."; description = "Group account under which uWSGI runs.";
};
capabilities = mkOption {
type = types.listOf types.str;
apply = caps: caps ++ optionals isEmperor imperialPowers;
default = [ ];
example = literalExample ''
[
"CAP_NET_BIND_SERVICE" # bind on ports <1024
"CAP_NET_RAW" # open raw sockets
]
'';
description = ''
Grant capabilities to the uWSGI instance. See the
<literal>capabilities(7)</literal> for available values.
<note>
<para>
uWSGI runs as an unprivileged user (even as Emperor) with the minimal
capabilities required. This option can be used to add fine-grained
permissions without running the service as root.
</para>
<para>
When in Emperor mode, any capability to be inherited by a vassal must
be specified again in the vassal configuration using <literal>cap</literal>.
See the uWSGI <link
xlink:href="https://uwsgi-docs.readthedocs.io/en/latest/Capabilities.html">docs</link>
for more information.
</para>
</note>
'';
}; };
}; };
}; };
config = mkIf cfg.enable { config = mkIf cfg.enable {
systemd.tmpfiles.rules = optional (cfg.runDir != "/run/uwsgi") ''
d ${cfg.runDir} 775 ${cfg.user} ${cfg.group}
'';
systemd.services.uwsgi = { systemd.services.uwsgi = {
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
preStart = ''
mkdir -p ${cfg.runDir}
chown ${cfg.user}:${cfg.group} ${cfg.runDir}
'';
serviceConfig = { serviceConfig = {
User = cfg.user;
Group = cfg.group;
Type = "notify"; Type = "notify";
ExecStart = "${cfg.package}/bin/uwsgi --uid ${cfg.user} --gid ${cfg.group} --json ${buildCfg "server" cfg.instance}/server.json"; ExecStart = "${cfg.package}/bin/uwsgi --json ${buildCfg "server" cfg.instance}/server.json";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID"; ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID";
NotifyAccess = "main"; NotifyAccess = "main";
KillSignal = "SIGQUIT"; KillSignal = "SIGQUIT";
AmbientCapabilities = cfg.capabilities;
CapabilityBoundingSet = cfg.capabilities;
}; };
}; };

View File

@ -6,31 +6,48 @@ import ./make-test-python.nix ({ pkgs, ... }:
}; };
machine = { pkgs, ... }: { machine = { pkgs, ... }: {
services.uwsgi.enable = true; users.users.hello =
services.uwsgi.plugins = [ "python3" "php" ]; { isSystemUser = true;
services.uwsgi.instance = { group = "hello";
type = "emperor"; };
vassals.python = { users.groups.hello = { };
services.uwsgi = {
enable = true;
plugins = [ "python3" "php" ];
capabilities = [ "CAP_NET_BIND_SERVICE" ];
instance.type = "emperor";
instance.vassals.hello = {
type = "normal"; type = "normal";
master = true; immediate-uid = "hello";
workers = 2; immediate-gid = "hello";
http = ":8000";
module = "wsgi:application"; module = "wsgi:application";
http = ":80";
cap = "net_bind_service";
pythonPackages = self: [ self.flask ];
chdir = pkgs.writeTextDir "wsgi.py" '' chdir = pkgs.writeTextDir "wsgi.py" ''
from flask import Flask from flask import Flask
import subprocess
application = Flask(__name__) application = Flask(__name__)
@application.route("/") @application.route("/")
def hello(): def hello():
return "Hello World!" return "Hello, World!"
@application.route("/whoami")
def whoami():
whoami = "${pkgs.coreutils}/bin/whoami"
proc = subprocess.run(whoami, capture_output=True)
return proc.stdout.decode().strip()
''; '';
pythonPackages = self: with self; [ flask ];
}; };
vassals.php = {
instance.vassals.php = {
type = "normal"; type = "normal";
master = true; master = true;
workers = 2; workers = 2;
http-socket = ":8001"; http-socket = ":8000";
http-socket-modifier1 = 14; http-socket-modifier1 = 14;
php-index = "index.php"; php-index = "index.php";
php-docroot = pkgs.writeTextDir "index.php" '' php-docroot = pkgs.writeTextDir "index.php" ''
@ -44,9 +61,21 @@ import ./make-test-python.nix ({ pkgs, ... }:
'' ''
machine.wait_for_unit("multi-user.target") machine.wait_for_unit("multi-user.target")
machine.wait_for_unit("uwsgi.service") machine.wait_for_unit("uwsgi.service")
with subtest("uWSGI has started"):
machine.wait_for_unit("uwsgi.service")
with subtest("Vassal can bind on port <1024"):
machine.wait_for_open_port(80)
hello = machine.succeed("curl -f http://machine").strip()
assert "Hello, World!" in hello, f"Excepted 'Hello, World!', got '{hello}'"
with subtest("Vassal is running as dedicated user"):
username = machine.succeed("curl -f http://machine/whoami").strip()
assert username == "hello", f"Excepted 'hello', got '{username}'"
with subtest("PHP plugin is working"):
machine.wait_for_open_port(8000) machine.wait_for_open_port(8000)
machine.wait_for_open_port(8001) assert "Hello World" in machine.succeed("curl -fv http://machine:8000")
assert "Hello World" in machine.succeed("curl -fv 127.0.0.1:8000")
assert "Hello World" in machine.succeed("curl -fv 127.0.0.1:8001")
''; '';
}) })