From 01c7c2815cd20ee869bbd3f9b67aa6d88355d2d2 Mon Sep 17 00:00:00 2001 From: rnhmjoj Date: Wed, 4 Nov 2020 19:21:02 +0100 Subject: [PATCH 1/4] uwsgi: build with capabilities support --- pkgs/servers/uwsgi/default.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkgs/servers/uwsgi/default.nix b/pkgs/servers/uwsgi/default.nix index d9ed810cdf49..449b97c58e77 100644 --- a/pkgs/servers/uwsgi/default.nix +++ b/pkgs/servers/uwsgi/default.nix @@ -3,6 +3,7 @@ , plugins ? [] , pam, withPAM ? stdenv.isLinux , systemd, withSystemd ? stdenv.isLinux +, libcap, withCap ? stdenv.isLinux , python2, python3, ncurses , ruby, php, libmysqlclient }: @@ -75,6 +76,7 @@ stdenv.mkDerivation rec { buildInputs = [ jansson pcre ] ++ lib.optional withPAM pam ++ lib.optional withSystemd systemd + ++ lib.optional withCap libcap ++ lib.concatMap (x: x.inputs) needed ; @@ -83,6 +85,8 @@ stdenv.mkDerivation rec { ++ lib.optional withSystemd "systemd_logger" ); + UWSGI_INCLUDES = lib.optionalString withCap "${libcap.dev}/include"; + passthru = { inherit python2 python3; }; From c00240e41e89becffe6f05a2df5f781ab76f863f Mon Sep 17 00:00:00 2001 From: rnhmjoj Date: Fri, 6 Nov 2020 09:58:49 +0100 Subject: [PATCH 2/4] nixos/uwsgi: add support for POSIX capabilities --- nixos/modules/services/web-servers/uwsgi.nix | 71 ++++++++++++++++---- nixos/tests/uwsgi.nix | 61 ++++++++++++----- 2 files changed, 104 insertions(+), 28 deletions(-) diff --git a/nixos/modules/services/web-servers/uwsgi.nix b/nixos/modules/services/web-servers/uwsgi.nix index 936e211ec713..506cd364a65a 100644 --- a/nixos/modules/services/web-servers/uwsgi.nix +++ b/nixos/modules/services/web-servers/uwsgi.nix @@ -5,11 +5,24 @@ with lib; let 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: let plugins = 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; hasPython = v: filter (n: n == "python${v}") plugins != []; @@ -18,7 +31,7 @@ let python = 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 hasPython3 then cfg.package.python3 else null; @@ -43,7 +56,7 @@ let oldPaths = filter (x: x != null) (map getPath env'); in env' ++ [ "PATH=${optionalString (oldPaths != []) "${last oldPaths}:"}${pythonEnv}/bin" ]; } - else if c.type == "emperor" + else if isEmperor then { emperor = if builtins.typeOf c.vassals != "set" then c.vassals else pkgs.buildEnv { @@ -51,7 +64,7 @@ let paths = mapAttrsToList buildCfg c.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); @@ -79,7 +92,7 @@ in { }; instance = mkOption { - type = with lib.types; let + type = with types; let valueType = nullOr (oneOf [ bool int @@ -137,31 +150,65 @@ in { user = mkOption { type = types.str; default = "uwsgi"; - description = "User account under which uwsgi runs."; + description = "User account under which uWSGI runs."; }; group = mkOption { type = types.str; 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 + capabilities(7) for available values. + + + 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. + + + When in Emperor mode, any capability to be inherited by a vassal must + be specified again in the vassal configuration using cap. + See the uWSGI docs + for more information. + + + ''; }; }; }; config = mkIf cfg.enable { + systemd.tmpfiles.rules = optional (cfg.runDir != "/run/uwsgi") '' + d ${cfg.runDir} 775 ${cfg.user} ${cfg.group} + ''; + systemd.services.uwsgi = { wantedBy = [ "multi-user.target" ]; - preStart = '' - mkdir -p ${cfg.runDir} - chown ${cfg.user}:${cfg.group} ${cfg.runDir} - ''; serviceConfig = { + User = cfg.user; + Group = cfg.group; 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"; ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID"; NotifyAccess = "main"; KillSignal = "SIGQUIT"; + AmbientCapabilities = cfg.capabilities; + CapabilityBoundingSet = cfg.capabilities; }; }; diff --git a/nixos/tests/uwsgi.nix b/nixos/tests/uwsgi.nix index 5c0b294e2d29..879965586877 100644 --- a/nixos/tests/uwsgi.nix +++ b/nixos/tests/uwsgi.nix @@ -6,31 +6,48 @@ import ./make-test-python.nix ({ pkgs, ... }: }; machine = { pkgs, ... }: { - services.uwsgi.enable = true; - services.uwsgi.plugins = [ "python3" "php" ]; - services.uwsgi.instance = { - type = "emperor"; - vassals.python = { + users.users.hello = + { isSystemUser = true; + group = "hello"; + }; + users.groups.hello = { }; + + services.uwsgi = { + enable = true; + plugins = [ "python3" "php" ]; + capabilities = [ "CAP_NET_BIND_SERVICE" ]; + instance.type = "emperor"; + + instance.vassals.hello = { type = "normal"; - master = true; - workers = 2; - http = ":8000"; + immediate-uid = "hello"; + immediate-gid = "hello"; module = "wsgi:application"; + http = ":80"; + cap = "net_bind_service"; + pythonPackages = self: [ self.flask ]; chdir = pkgs.writeTextDir "wsgi.py" '' from flask import Flask + import subprocess application = Flask(__name__) @application.route("/") 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"; master = true; workers = 2; - http-socket = ":8001"; + http-socket = ":8000"; http-socket-modifier1 = 14; php-index = "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("uwsgi.service") - machine.wait_for_open_port(8000) - machine.wait_for_open_port(8001) - 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") + + 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) + assert "Hello World" in machine.succeed("curl -fv http://machine:8000") ''; }) From 5597f6ded8bf15a1b51dc717a9bf4611abf3435a Mon Sep 17 00:00:00 2001 From: rnhmjoj Date: Sun, 10 Jan 2021 19:13:12 +0100 Subject: [PATCH 3/4] nixos/ihatemoney: run uwsgi emperor as normal user --- nixos/modules/services/web-apps/ihatemoney/default.nix | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/nixos/modules/services/web-apps/ihatemoney/default.nix b/nixos/modules/services/web-apps/ihatemoney/default.nix index 68769ac8c031..b4987fa4702c 100644 --- a/nixos/modules/services/web-apps/ihatemoney/default.nix +++ b/nixos/modules/services/web-apps/ihatemoney/default.nix @@ -44,7 +44,7 @@ let in { 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 { type = types.enum [ "sqlite" "postgresql" ]; default = "sqlite"; @@ -116,16 +116,13 @@ in services.uwsgi = { enable = true; plugins = [ "python3" ]; - # the vassal needs to be able to setuid - user = "root"; - group = "root"; instance = { type = "emperor"; vassals.ihatemoney = { type = "normal"; strict = true; - uid = user; - gid = group; + immediate-uid = user; + immediate-gid = group; # apparently flask uses threads: https://github.com/spiral-project/ihatemoney/commit/c7815e48781b6d3a457eaff1808d179402558f8c enable-threads = true; module = "wsgi:application"; From 3a17a9b05eec0189d82ebb84f327f386727474cd Mon Sep 17 00:00:00 2001 From: rnhmjoj Date: Sat, 7 Nov 2020 02:33:03 +0100 Subject: [PATCH 4/4] nixos/docs: add uWSGI changes to the relase notes --- nixos/doc/manual/release-notes/rl-2103.xml | 24 ++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/nixos/doc/manual/release-notes/rl-2103.xml b/nixos/doc/manual/release-notes/rl-2103.xml index 05daca1d710c..68d98ffad5e9 100644 --- a/nixos/doc/manual/release-notes/rl-2103.xml +++ b/nixos/doc/manual/release-notes/rl-2103.xml @@ -176,6 +176,30 @@ with mkfs.xfs -m reflink=0. + + + 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 uwsgi user. Any additional + capability can be added via the new option + . + The previous behaviour can be restored by setting: + + = "root"; + = "root"; + = + { + uid = "uwsgi"; + gid = "uwsgi"; + }; + + + + Another incompatibility from the previous release is that vassals running under a + different user or group need to use immediate-{uid,gid} + instead of the usual uid,gid options. + + btc1 has been abandoned upstream, and removed.