Merge pull request #106082 from rnhmjoj/uwsgi
nixos/uwsgi: run with capabilities instead of root
This commit is contained in:
commit
0fbc0976db
|
@ -185,6 +185,30 @@
|
||||||
with <literal>mkfs.xfs -m reflink=0</literal>.
|
with <literal>mkfs.xfs -m reflink=0</literal>.
|
||||||
</para>
|
</para>
|
||||||
</listitem>
|
</listitem>
|
||||||
|
<listitem>
|
||||||
|
<para>
|
||||||
|
The uWSGI server is now built with POSIX capabilities. As a consequence,
|
||||||
|
root is no longer required in emperor mode and the service defaults to
|
||||||
|
running as the unprivileged <literal>uwsgi</literal> user. Any additional
|
||||||
|
capability can be added via the new option
|
||||||
|
<xref linkend="opt-services.uwsgi.capabilities"/>.
|
||||||
|
The previous behaviour can be restored by setting:
|
||||||
|
<programlisting>
|
||||||
|
<xref linkend="opt-services.uwsgi.user"/> = "root";
|
||||||
|
<xref linkend="opt-services.uwsgi.group"/> = "root";
|
||||||
|
<xref linkend="opt-services.uwsgi.instance"/> =
|
||||||
|
{
|
||||||
|
uid = "uwsgi";
|
||||||
|
gid = "uwsgi";
|
||||||
|
};
|
||||||
|
</programlisting>
|
||||||
|
</para>
|
||||||
|
<para>
|
||||||
|
Another incompatibility from the previous release is that vassals running under a
|
||||||
|
different user or group need to use <literal>immediate-{uid,gid}</literal>
|
||||||
|
instead of the usual <literal>uid,gid</literal> options.
|
||||||
|
</para>
|
||||||
|
</listitem>
|
||||||
<listitem>
|
<listitem>
|
||||||
<para>
|
<para>
|
||||||
<package>btc1</package> has been abandoned upstream, and removed.
|
<package>btc1</package> has been abandoned upstream, and removed.
|
||||||
|
|
|
@ -44,7 +44,7 @@ let
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.services.ihatemoney = {
|
options.services.ihatemoney = {
|
||||||
enable = mkEnableOption "ihatemoney webapp. Note that this will set uwsgi to emperor mode running as root";
|
enable = mkEnableOption "ihatemoney webapp. Note that this will set uwsgi to emperor mode";
|
||||||
backend = mkOption {
|
backend = mkOption {
|
||||||
type = types.enum [ "sqlite" "postgresql" ];
|
type = types.enum [ "sqlite" "postgresql" ];
|
||||||
default = "sqlite";
|
default = "sqlite";
|
||||||
|
@ -116,16 +116,13 @@ in
|
||||||
services.uwsgi = {
|
services.uwsgi = {
|
||||||
enable = true;
|
enable = true;
|
||||||
plugins = [ "python3" ];
|
plugins = [ "python3" ];
|
||||||
# the vassal needs to be able to setuid
|
|
||||||
user = "root";
|
|
||||||
group = "root";
|
|
||||||
instance = {
|
instance = {
|
||||||
type = "emperor";
|
type = "emperor";
|
||||||
vassals.ihatemoney = {
|
vassals.ihatemoney = {
|
||||||
type = "normal";
|
type = "normal";
|
||||||
strict = true;
|
strict = true;
|
||||||
uid = user;
|
immediate-uid = user;
|
||||||
gid = group;
|
immediate-gid = group;
|
||||||
# apparently flask uses threads: https://github.com/spiral-project/ihatemoney/commit/c7815e48781b6d3a457eaff1808d179402558f8c
|
# apparently flask uses threads: https://github.com/spiral-project/ihatemoney/commit/c7815e48781b6d3a457eaff1808d179402558f8c
|
||||||
enable-threads = true;
|
enable-threads = true;
|
||||||
module = "wsgi:application";
|
module = "wsgi:application";
|
||||||
|
|
|
@ -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")
|
|
||||||
'';
|
'';
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
, plugins ? []
|
, plugins ? []
|
||||||
, pam, withPAM ? stdenv.isLinux
|
, pam, withPAM ? stdenv.isLinux
|
||||||
, systemd, withSystemd ? stdenv.isLinux
|
, systemd, withSystemd ? stdenv.isLinux
|
||||||
|
, libcap, withCap ? stdenv.isLinux
|
||||||
, python2, python3, ncurses
|
, python2, python3, ncurses
|
||||||
, ruby, php, libmysqlclient
|
, ruby, php, libmysqlclient
|
||||||
}:
|
}:
|
||||||
|
@ -75,6 +76,7 @@ stdenv.mkDerivation rec {
|
||||||
buildInputs = [ jansson pcre ]
|
buildInputs = [ jansson pcre ]
|
||||||
++ lib.optional withPAM pam
|
++ lib.optional withPAM pam
|
||||||
++ lib.optional withSystemd systemd
|
++ lib.optional withSystemd systemd
|
||||||
|
++ lib.optional withCap libcap
|
||||||
++ lib.concatMap (x: x.inputs) needed
|
++ lib.concatMap (x: x.inputs) needed
|
||||||
;
|
;
|
||||||
|
|
||||||
|
@ -83,6 +85,8 @@ stdenv.mkDerivation rec {
|
||||||
++ lib.optional withSystemd "systemd_logger"
|
++ lib.optional withSystemd "systemd_logger"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
UWSGI_INCLUDES = lib.optionalString withCap "${libcap.dev}/include";
|
||||||
|
|
||||||
passthru = {
|
passthru = {
|
||||||
inherit python2 python3;
|
inherit python2 python3;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue