]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
run: when invoked as "uid0", expose some sudo-like behaviour
authorLennart Poettering <lennart@poettering.net>
Tue, 19 Dec 2023 18:10:52 +0000 (19:10 +0100)
committerLennart Poettering <lennart@poettering.net>
Thu, 21 Dec 2023 18:14:43 +0000 (19:14 +0100)
This turns "systemd-run" into a multi-call binary. When invoked under
the name "uid0", then it behaves a bit more like traditional "sudo".
This mostly means defaults appropriuate for that, for example a PAM
stack, interactivity and more.

Fixes: #29199
man/rules/meson.build
man/systemd-run.xml
man/uid0.xml [new file with mode: 0644]
src/run/meson.build
src/run/run.c
src/run/systemd-uid0.in [new file with mode: 0644]

index 3d63cf1131ac9a0beca92699b39dbbdd633a160a..622921f8d63db890dbe71f48c03e7ea8aadfae68 100644 (file)
@@ -1257,6 +1257,7 @@ manpages = [
   ''],
  ['udev_new', '3', ['udev_ref', 'udev_unref'], ''],
  ['udevadm', '8', [], ''],
+ ['uid0', '1', [], ''],
  ['ukify', '1', [], 'ENABLE_UKIFY'],
  ['user@.service',
   '5',
index 5be9823c373de5143472db3bbd15ac5e9880f190..bc77fd13ab3cf9f6d048a547acb49a389b993a1d 100644 (file)
@@ -677,7 +677,8 @@ $ systemd-run --user --wait -p SuccessExitStatus=SIGUSR1 --expand-environment=no
       <citerefentry><refentrytitle>systemd.resource-control</refentrytitle><manvolnum>5</manvolnum></citerefentry>,
       <citerefentry><refentrytitle>systemd.timer</refentrytitle><manvolnum>5</manvolnum></citerefentry>,
       <citerefentry><refentrytitle>systemd-mount</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
-      <citerefentry><refentrytitle>machinectl</refentrytitle><manvolnum>1</manvolnum></citerefentry>
+      <citerefentry><refentrytitle>machinectl</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+      <citerefentry><refentrytitle>uid0</refentrytitle><manvolnum>1</manvolnum></citerefentry>
     </para>
   </refsect1>
 
diff --git a/man/uid0.xml b/man/uid0.xml
new file mode 100644 (file)
index 0000000..6ef868a
--- /dev/null
@@ -0,0 +1,214 @@
+<?xml version='1.0'?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
+  "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
+<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
+
+<refentry id="uid0"
+          xmlns:xi="http://www.w3.org/2001/XInclude">
+
+  <refentryinfo>
+    <title>uid0</title>
+    <productname>systemd</productname>
+  </refentryinfo>
+
+  <refmeta>
+    <refentrytitle>uid0</refentrytitle>
+    <manvolnum>1</manvolnum>
+  </refmeta>
+
+  <refnamediv>
+    <refname>uid0</refname>
+    <refpurpose>Elevate privileges</refpurpose>
+  </refnamediv>
+
+  <refsynopsisdiv>
+    <cmdsynopsis>
+      <command>uid0</command>
+      <arg choice="opt" rep="repeat">OPTIONS</arg>
+      <arg choice="opt" rep="repeat">COMMAND</arg>
+    </cmdsynopsis>
+  </refsynopsisdiv>
+
+  <refsect1>
+    <title>Description</title>
+
+    <para><command>uid0</command> may be used to temporarily and interactively acquire elavated or different
+    privileges. It serves a similar purpose as <citerefentry
+    project='man-pages'><refentrytitle>sudo</refentrytitle><manvolnum>8</manvolnum></citerefentry>, but
+    operates differently in a couple of key areas:</para>
+
+    <itemizedlist>
+      <listitem><para>No execution or security context credentials are inherited from the caller into the
+      invoked commands, as they are invoked from a fresh, isolated service forked off the service
+      manager.</para></listitem>
+
+      <listitem><para>Authentication takes place via <ulink
+      url="https://www.freedesktop.org/wiki/Software/polkit">polkit</ulink>, thus isolating the
+      authentication prompt from the terminal (if possible).</para></listitem>
+
+      <listitem><para>An independent pseudo-tty is allocated for the invoked command, detaching its lifecycle and
+      isolating it for security.</para></listitem>
+
+      <listitem><para>No SetUID/SetGID file access bit functionality is used for the implementation.</para></listitem>
+    </itemizedlist>
+
+    <para>Altogether this should provide a safer and more robust alternative to the <command>sudo</command>
+    mechanism, in particular in OS environments where SetUID/SetGID support is not available (for example by
+    setting the <varname>NoNewPrivileges=</varname> variable in
+    <citerefentry><refentrytitle>systemd-system.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry>).</para>
+
+    <para>Any session invoked via <command>uid0</command> will run through the
+    <literal>systemd-uid0</literal> PAM stack.</para>
+
+    <para>Note that <command>uid0</command> is implemented as an alternative multi-call invocation of
+    <citerefentry><refentrytitle>systemd-run</refentrytitle><manvolnum>1</manvolnum></citerefentry>.</para>
+  </refsect1>
+
+  <refsect1>
+    <title>Options</title>
+
+    <para>The following options are understood:</para>
+
+    <variablelist>
+      <varlistentry>
+        <term><option>--no-ask-password</option></term>
+
+        <listitem><para>Do not query the user for authentication for privileged operations.</para>
+
+        <xi:include href="version-info.xml" xpointer="v256"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--unit=</option></term>
+
+        <listitem><para>Use this unit name instead of an automatically generated one.</para>
+
+        <xi:include href="version-info.xml" xpointer="v256"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--property=</option></term>
+
+        <listitem><para>Sets a property on the service unit that is created. This option takes an assignment
+        in the same format as
+        <citerefentry><refentrytitle>systemctl</refentrytitle><manvolnum>1</manvolnum></citerefentry>'s
+        <command>set-property</command> command.</para>
+
+        <xi:include href="version-info.xml" xpointer="v256"/>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--description=</option></term>
+
+        <listitem><para>Provide a description for the service unit that is invoked. If not specified,
+        the command itself will be used as a description. See <varname>Description=</varname> in
+        <citerefentry><refentrytitle>systemd.unit</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
+        </para>
+
+        <xi:include href="version-info.xml" xpointer="v256"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--slice=</option></term>
+
+        <listitem><para>Make the new <filename>.service</filename> unit part of the specified slice, instead
+        of <filename>user.slice</filename>.</para>
+
+        <xi:include href="version-info.xml" xpointer="v256"/>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--slice-inherit</option></term>
+
+        <listitem><para>Make the new <filename>.service</filename> unit part of the slice the
+        <command>uid0</command> itself has been invoked in. This option may be combined with
+        <option>--slice=</option>, in which case the slice specified via <option>--slice=</option> is placed
+        within the slice the <command>uid0</command> command is invoked in.</para>
+
+        <para>Example: consider <command>uid0</command> being invoked in the slice
+        <filename>foo.slice</filename>, and the <option>--slice=</option> argument is
+        <filename>bar</filename>. The unit will then be placed under
+        <filename>foo-bar.slice</filename>.</para>
+
+        <xi:include href="version-info.xml" xpointer="v256"/>
+
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--user=</option></term>
+        <term><option>-u</option></term>
+        <term><option>--group=</option></term>
+        <term><option>-g</option></term>
+
+        <listitem><para>Switches to the specified user/group instead of root.</para>
+
+        <xi:include href="version-info.xml" xpointer="v256"/>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--nice=</option></term>
+
+        <listitem><para>Runs the invoked session with the specified nice level.</para>
+
+        <xi:include href="version-info.xml" xpointer="v256"/>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--chdir=</option></term>
+        <term><option>-D</option></term>
+
+        <listitem><para>Runs the invoked session with the specified working directory. If not specified
+        defaults to the client's current working directory if switching to the root user, or the target
+        user's home directory otherwise.</para>
+
+        <xi:include href="version-info.xml" xpointer="v256"/>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--setenv=<replaceable>NAME</replaceable>[=<replaceable>VALUE</replaceable>]</option></term>
+
+        <listitem><para>Runs the invoked session with the specified environment variable set. This parameter
+        may be used more than once to set multiple variables. When <literal>=</literal> and
+        <replaceable>VALUE</replaceable> are omitted, the value of the variable with the same name in the
+        invoking environment will be used.</para>
+
+        <xi:include href="version-info.xml" xpointer="v256"/>
+        </listitem>
+      </varlistentry>
+
+      <xi:include href="user-system-options.xml" xpointer="machine" />
+      <xi:include href="standard-options.xml" xpointer="help" />
+      <xi:include href="standard-options.xml" xpointer="version" />
+    </variablelist>
+
+    <para>All command line arguments after the first non-option argument become part of the command line of
+    the launched process. If no command line is specified an interactive shell is invoked. The shell to
+    invoke may be controlled via <option>--setenv=SHELL=…</option> and currently defaults to the
+    <emphasis>originating user's</emphasis> shell (i.e. not the target user's!) if operating locally, or
+    <filename>/bin/sh</filename> when operating with <option>--machine=</option>.</para>
+  </refsect1>
+
+  <refsect1>
+    <title>Exit status</title>
+
+    <para>On success, 0 is returned. If <command>uid0</command> failed to start the session or the specified command fails, a
+    non-zero return value will be returned.</para>
+  </refsect1>
+
+  <refsect1>
+    <title>See Also</title>
+    <para>
+      <citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+      <citerefentry><refentrytitle>systemd-run</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+      <citerefentry project='man-pages'><refentrytitle>sudo</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
+      <citerefentry><refentrytitle>machinectl</refentrytitle><manvolnum>1</manvolnum></citerefentry>
+    </para>
+  </refsect1>
+
+</refentry>
index 597a25abeb634a9cae463fbb9cdc6633c10e9d1e..95336b86fe5a7143ec1ead09b56317f853e313a9 100644 (file)
@@ -7,3 +7,17 @@ executables += [
                 'sources' : files('run.c'),
         },
 ]
+
+install_emptydir(bindir)
+
+meson.add_install_script(sh, '-c',
+                             ln_s.format(bindir / 'systemd-run',
+                                         bindir / 'uid0'))
+
+custom_target(
+        'systemd-uid0',
+        input : 'systemd-uid0.in',
+        output : 'systemd-uid0',
+        command : [jinja2_cmdline, '@INPUT@', '@OUTPUT@'],
+        install : pamconfdir != 'no',
+        install_dir : pamconfdir)
index 06c00cbf56af3f129c0c2d6898bf7bcc4567a5ec..335838c32f8274398217b6b83958044f3f60d537 100644 (file)
@@ -73,6 +73,7 @@ static bool arg_aggressive_gc = false;
 static char *arg_working_directory = NULL;
 static bool arg_shell = false;
 static char **arg_cmdline = NULL;
+static char *arg_exec_path = NULL;
 
 STATIC_DESTRUCTOR_REGISTER(arg_description, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_environment, strv_freep);
@@ -82,6 +83,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_socket_property, strv_freep);
 STATIC_DESTRUCTOR_REGISTER(arg_timer_property, strv_freep);
 STATIC_DESTRUCTOR_REGISTER(arg_working_directory, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_cmdline, strv_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_exec_path, freep);
 
 static int help(void) {
         _cleanup_free_ char *link = NULL;
@@ -146,6 +148,39 @@ static int help(void) {
         return 0;
 }
 
+static int help_sudo_mode(void) {
+        _cleanup_free_ char *link = NULL;
+        int r;
+
+        r = terminal_urlify_man("uid0", "1", &link);
+        if (r < 0)
+                return log_oom();
+
+        printf("%s [OPTIONS...] COMMAND [ARGUMENTS...]\n"
+               "\n%sElevate privileges interactively.%s\n\n"
+               "  -h --help                       Show this help\n"
+               "  -V --version                    Show package version\n"
+               "     --no-ask-password            Do not prompt for password\n"
+               "     --machine=CONTAINER          Operate on local container\n"
+               "     --unit=UNIT                  Run under the specified unit name\n"
+               "     --property=NAME=VALUE        Set service or scope unit property\n"
+               "     --description=TEXT           Description for unit\n"
+               "     --slice=SLICE                Run in the specified slice\n"
+               "     --slice-inherit              Inherit the slice\n"
+               "  -u --user=USER                  Run as system user\n"
+               "  -g --group=GROUP                Run as system group\n"
+               "     --nice=NICE                  Nice level\n"
+               "  -D --chdir=PATH                 Set working directory\n"
+               "     --setenv=NAME[=VALUE]        Set environment variable\n"
+               "\nSee the %s for details.\n",
+               program_invocation_short_name,
+               ansi_highlight(),
+               ansi_normal(),
+               link);
+
+        return 0;
+}
+
 static int add_timer_property(const char *name, const char *val) {
         char *p;
 
@@ -162,6 +197,18 @@ static int add_timer_property(const char *name, const char *val) {
         return 0;
 }
 
+static char **make_login_shell_cmdline(const char *shell) {
+        _cleanup_free_ char *argv0 = NULL;
+
+        assert(shell);
+
+        argv0 = strjoin("-", shell); /* The - is how shells determine if they shall be consider login shells */
+        if (!argv0)
+                return NULL;
+
+        return strv_new(argv0);
+}
+
 static int parse_argv(int argc, char *argv[]) {
 
         enum {
@@ -651,6 +698,219 @@ static int parse_argv(int argc, char *argv[]) {
         return 1;
 }
 
+static int parse_argv_sudo_mode(int argc, char *argv[]) {
+
+        enum {
+                ARG_NO_ASK_PASSWORD = 0x100,
+                ARG_HOST,
+                ARG_MACHINE,
+                ARG_UNIT,
+                ARG_PROPERTY,
+                ARG_DESCRIPTION,
+                ARG_SLICE,
+                ARG_SLICE_INHERIT,
+                ARG_NICE,
+                ARG_SETENV,
+        };
+
+        /* If invoked as "uid0" binary, let's expose a more sudo-like interface. We add various extensions
+         * though (but limit the extension to long options). */
+
+        static const struct option options[] = {
+                { "help",               no_argument,       NULL, 'h'                    },
+                { "version",            no_argument,       NULL, 'V'                    },
+                { "no-ask-password",    no_argument,       NULL, ARG_NO_ASK_PASSWORD    },
+                { "machine",            required_argument, NULL, ARG_MACHINE            },
+                { "unit",               required_argument, NULL, ARG_UNIT               },
+                { "property",           required_argument, NULL, ARG_PROPERTY           },
+                { "description",        required_argument, NULL, ARG_DESCRIPTION        },
+                { "slice",              required_argument, NULL, ARG_SLICE              },
+                { "slice-inherit",      no_argument,       NULL, ARG_SLICE_INHERIT      },
+                { "user",               required_argument, NULL, 'u'                    },
+                { "group",              required_argument, NULL, 'g'                    },
+                { "nice",               required_argument, NULL, ARG_NICE               },
+                { "chdir",              required_argument, NULL, 'D'                    },
+                { "setenv",             required_argument, NULL, ARG_SETENV             },
+                {},
+        };
+
+        int r, c;
+
+        assert(argc >= 0);
+        assert(argv);
+
+        /* Resetting to 0 forces the invocation of an internal initialization routine of getopt_long()
+         * that checks for GNU extensions in optstring ('-' or '+' at the beginning). */
+        optind = 0;
+        while ((c = getopt_long(argc, argv, "+hVu:g:D:", options, NULL)) >= 0)
+
+                switch (c) {
+
+                case 'h':
+                        return help_sudo_mode();
+
+                case 'V':
+                        return version();
+
+                case ARG_NO_ASK_PASSWORD:
+                        arg_ask_password = false;
+                        break;
+
+                case ARG_MACHINE:
+                        arg_transport = BUS_TRANSPORT_MACHINE;
+                        arg_host = optarg;
+                        break;
+
+                case ARG_UNIT:
+                        arg_unit = optarg;
+                        break;
+
+                case ARG_PROPERTY:
+                        if (strv_extend(&arg_property, optarg) < 0)
+                                return log_oom();
+
+                        break;
+
+                case ARG_DESCRIPTION:
+                        r = free_and_strdup_warn(&arg_description, optarg);
+                        if (r < 0)
+                                return r;
+                        break;
+
+                case ARG_SLICE:
+                        arg_slice = optarg;
+                        break;
+
+                case ARG_SLICE_INHERIT:
+                        arg_slice_inherit = true;
+                        break;
+
+                case 'u':
+                        arg_exec_user = optarg;
+                        break;
+
+                case 'g':
+                        arg_exec_group = optarg;
+                        break;
+
+                case ARG_NICE:
+                        r = parse_nice(optarg, &arg_nice);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to parse nice value: %s", optarg);
+
+                        arg_nice_set = true;
+                        break;
+
+                case 'D':
+                        r = parse_path_argument(optarg, true, &arg_working_directory);
+                        if (r < 0)
+                                return r;
+
+                        break;
+
+                case ARG_SETENV:
+                        r = strv_env_replace_strdup_passthrough(&arg_environment, optarg);
+                        if (r < 0)
+                                return log_error_errno(r, "Cannot assign environment variable %s: %m", optarg);
+
+                        break;
+
+                case '?':
+                        return -EINVAL;
+
+                default:
+                        assert_not_reached();
+                }
+
+        if (!arg_working_directory) {
+                if (arg_exec_user) {
+                        /* When switching to a specific user, also switch to its home directory. */
+                        arg_working_directory = strdup("~");
+                        if (!arg_working_directory)
+                                return log_oom();
+                } else {
+                        /* When switching to root without this being specified, then stay in the current directory */
+                        r = safe_getcwd(&arg_working_directory);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to get current working directory: %m");
+                }
+        }
+
+        arg_service_type = "exec";
+        arg_quiet = true;
+        arg_wait = true;
+        arg_aggressive_gc = true;
+
+        arg_stdio = isatty(STDIN_FILENO) && isatty(STDOUT_FILENO) && isatty(STDERR_FILENO) ? ARG_STDIO_PTY : ARG_STDIO_DIRECT;
+        arg_expand_environment = false;
+        arg_send_sighup = true;
+
+        _cleanup_strv_free_ char **l = NULL;
+        if (argc > optind)
+                l = strv_copy(argv + optind);
+        else {
+                const char *e;
+
+                e = strv_env_get(arg_environment, "SHELL");
+                if (e)
+                        arg_exec_path = strdup(e);
+                else {
+                        if (arg_transport == BUS_TRANSPORT_LOCAL) {
+                                r = get_shell(&arg_exec_path);
+                                if (r < 0)
+                                        return log_error_errno(r, "Failed to determine shell: %m");
+                        } else
+                                arg_exec_path = strdup("/bin/sh");
+                }
+                if (!arg_exec_path)
+                        return log_oom();
+
+                l = make_login_shell_cmdline(arg_exec_path);
+        }
+        if (!l)
+                return log_oom();
+
+        strv_free_and_replace(arg_cmdline, l);
+
+        if (!arg_slice) {
+                arg_slice = strdup("user.slice");
+                if (!arg_slice)
+                        return log_oom();
+        }
+
+        _cleanup_free_ char *un = NULL;
+        un = getusername_malloc();
+        if (!un)
+                return log_oom();
+
+        /* Set a bunch of environment variables in a roughly sudo-compatible way */
+        r = strv_env_assign(&arg_environment, "SUDO_USER", un);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set $SUDO_USER environment variable: %m");
+
+        r = strv_env_assignf(&arg_environment, "SUDO_UID", UID_FMT, getuid());
+        if (r < 0)
+                return log_error_errno(r, "Failed to set $SUDO_UID environment variable: %m");
+
+        r = strv_env_assignf(&arg_environment, "SUDO_GID", GID_FMT, getgid());
+        if (r < 0)
+                return log_error_errno(r, "Failed to set $SUDO_GID environment variable: %m");
+
+        if (strv_extendf(&arg_property, "LogExtraFields=ELEVATED_UID=" UID_FMT, getuid()) < 0)
+                return log_oom();
+
+        if (strv_extendf(&arg_property, "LogExtraFields=ELEVATED_GID=" GID_FMT, getgid()) < 0)
+                return log_oom();
+
+        if (strv_extendf(&arg_property, "LogExtraFields=ELEVATED_USER=%s", un) < 0)
+                return log_oom();
+
+        if (strv_extend(&arg_property, "PAMName=systemd-uid0") < 0)
+                return log_oom();
+
+        return 1;
+}
+
 static int transient_unit_set_properties(sd_bus_message *m, UnitType t, char **properties) {
         int r;
 
@@ -899,7 +1159,7 @@ static int transient_service_set_properties(sd_bus_message *m, const char *pty_p
                 if (r < 0)
                         return bus_log_create_error(r);
 
-                r = sd_bus_message_append(m, "s", arg_cmdline[0]);
+                r = sd_bus_message_append(m, "s", arg_exec_path ?: arg_cmdline[0]);
                 if (r < 0)
                         return bus_log_create_error(r);
 
@@ -1899,6 +2159,8 @@ static int start_transient_trigger(sd_bus *bus, const char *suffix) {
 }
 
 static bool shall_make_executable_absolute(void) {
+        if (arg_exec_path)
+                return false;
         if (strv_isempty(arg_cmdline))
                 return false;
         if (arg_transport != BUS_TRANSPORT_LOCAL)
@@ -1919,7 +2181,10 @@ static int run(int argc, char* argv[]) {
         log_parse_environment();
         log_open();
 
-        r = parse_argv(argc, argv);
+        if (invoked_as(argv, "uid0"))
+                r = parse_argv_sudo_mode(argc, argv);
+        else
+                r = parse_argv(argc, argv);
         if (r <= 0)
                 return r;
 
diff --git a/src/run/systemd-uid0.in b/src/run/systemd-uid0.in
new file mode 100644 (file)
index 0000000..57bd5e3
--- /dev/null
@@ -0,0 +1,23 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# This file is part of systemd.
+#
+# Used by uid0 sessions
+
+{% if ENABLE_HOMED %}
+-account sufficient pam_systemd_home.so
+{% endif %}
+account  required   pam_unix.so
+
+{% if HAVE_SELINUX %}
+session  required   pam_selinux.so close
+session  required   pam_selinux.so open
+{% endif %}
+session  required   pam_loginuid.so
+session  optional   pam_keyinit.so force revoke
+session  required   pam_namespace.so
+{% if ENABLE_HOMED %}
+-session optional   pam_systemd_home.so
+{% endif %}
+session  optional   pam_umask.so silent
+session  optional   pam_systemd.so
+session  required   pam_unix.so