From 72eb3081b21a12252159e24188e25d04f14af83c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 19 Dec 2023 19:10:52 +0100 Subject: [PATCH] run: when invoked as "uid0", expose some sudo-like behaviour 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 | 1 + man/systemd-run.xml | 3 +- man/uid0.xml | 214 ++++++++++++++++++++++++++++++++ src/run/meson.build | 14 +++ src/run/run.c | 269 +++++++++++++++++++++++++++++++++++++++- src/run/systemd-uid0.in | 23 ++++ 6 files changed, 521 insertions(+), 3 deletions(-) create mode 100644 man/uid0.xml create mode 100644 src/run/systemd-uid0.in diff --git a/man/rules/meson.build b/man/rules/meson.build index 3d63cf1131a..622921f8d63 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1257,6 +1257,7 @@ manpages = [ ''], ['udev_new', '3', ['udev_ref', 'udev_unref'], ''], ['udevadm', '8', [], ''], + ['uid0', '1', [], ''], ['ukify', '1', [], 'ENABLE_UKIFY'], ['user@.service', '5', diff --git a/man/systemd-run.xml b/man/systemd-run.xml index 5be9823c373..bc77fd13ab3 100644 --- a/man/systemd-run.xml +++ b/man/systemd-run.xml @@ -677,7 +677,8 @@ $ systemd-run --user --wait -p SuccessExitStatus=SIGUSR1 --expand-environment=no systemd.resource-control5, systemd.timer5, systemd-mount1, - machinectl1 + machinectl1, + uid01 diff --git a/man/uid0.xml b/man/uid0.xml new file mode 100644 index 00000000000..6ef868a8e53 --- /dev/null +++ b/man/uid0.xml @@ -0,0 +1,214 @@ + + + + + + + + uid0 + systemd + + + + uid0 + 1 + + + + uid0 + Elevate privileges + + + + + uid0 + OPTIONS + COMMAND + + + + + Description + + uid0 may be used to temporarily and interactively acquire elavated or different + privileges. It serves a similar purpose as sudo8, but + operates differently in a couple of key areas: + + + 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. + + Authentication takes place via polkit, thus isolating the + authentication prompt from the terminal (if possible). + + An independent pseudo-tty is allocated for the invoked command, detaching its lifecycle and + isolating it for security. + + No SetUID/SetGID file access bit functionality is used for the implementation. + + + Altogether this should provide a safer and more robust alternative to the sudo + mechanism, in particular in OS environments where SetUID/SetGID support is not available (for example by + setting the NoNewPrivileges= variable in + systemd-system.conf5). + + Any session invoked via uid0 will run through the + systemd-uid0 PAM stack. + + Note that uid0 is implemented as an alternative multi-call invocation of + systemd-run1. + + + + Options + + The following options are understood: + + + + + + Do not query the user for authentication for privileged operations. + + + + + + + + Use this unit name instead of an automatically generated one. + + + + + + + + Sets a property on the service unit that is created. This option takes an assignment + in the same format as + systemctl1's + set-property command. + + + + + + + + + Provide a description for the service unit that is invoked. If not specified, + the command itself will be used as a description. See Description= in + systemd.unit5. + + + + + + + + + Make the new .service unit part of the specified slice, instead + of user.slice. + + + + + + + + + Make the new .service unit part of the slice the + uid0 itself has been invoked in. This option may be combined with + , in which case the slice specified via is placed + within the slice the uid0 command is invoked in. + + Example: consider uid0 being invoked in the slice + foo.slice, and the argument is + bar. The unit will then be placed under + foo-bar.slice. + + + + + + + + + + + + + Switches to the specified user/group instead of root. + + + + + + + + + Runs the invoked session with the specified nice level. + + + + + + + + + + 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. + + + + + + + + + Runs the invoked session with the specified environment variable set. This parameter + may be used more than once to set multiple variables. When = and + VALUE are omitted, the value of the variable with the same name in the + invoking environment will be used. + + + + + + + + + + + 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 and currently defaults to the + originating user's shell (i.e. not the target user's!) if operating locally, or + /bin/sh when operating with . + + + + Exit status + + On success, 0 is returned. If uid0 failed to start the session or the specified command fails, a + non-zero return value will be returned. + + + + See Also + + systemd1, + systemd-run1, + sudo8, + machinectl1 + + + + diff --git a/src/run/meson.build b/src/run/meson.build index 597a25abeb6..95336b86fe5 100644 --- a/src/run/meson.build +++ b/src/run/meson.build @@ -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) diff --git a/src/run/run.c b/src/run/run.c index 06c00cbf56a..335838c32f8 100644 --- a/src/run/run.c +++ b/src/run/run.c @@ -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 index 00000000000..57bd5e38b92 --- /dev/null +++ b/src/run/systemd-uid0.in @@ -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