nixos/uwsgi: add support for POSIX capabilities
This commit is contained in:
parent
01c7c2815c
commit
c00240e41e
@ -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;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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")
|
|
||||||
'';
|
'';
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user