From b65a4aec052dcebdc70fa3f958fc71e49cf30ed2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 22 Feb 2023 13:04:46 +0100 Subject: [PATCH 1/6] mkosi: add some really basic tools to default mkosi image "passwd" and "pscap" are extremely useful to debug basic OS behaviour, and tiny. So let's add them to our default development images, just to save us some headaches. --- mkosi.conf.d/arch/10-arch.conf | 1 + mkosi.conf.d/centos/10-centos.conf | 2 ++ mkosi.conf.d/debian/10-debian.conf | 2 ++ mkosi.conf.d/fedora/10-fedora.conf | 2 ++ mkosi.conf.d/opensuse/10-opensuse.conf | 2 ++ mkosi.conf.d/ubuntu/10-ubuntu.conf | 2 ++ 6 files changed, 11 insertions(+) diff --git a/mkosi.conf.d/arch/10-arch.conf b/mkosi.conf.d/arch/10-arch.conf index ee51396e04..a1bff77096 100644 --- a/mkosi.conf.d/arch/10-arch.conf +++ b/mkosi.conf.d/arch/10-arch.conf @@ -32,6 +32,7 @@ Packages= popt python-pefile quota-tools + shadow tpm2-tss vim diff --git a/mkosi.conf.d/centos/10-centos.conf b/mkosi.conf.d/centos/10-centos.conf index 94aeaed9c9..91099a7f0a 100644 --- a/mkosi.conf.d/centos/10-centos.conf +++ b/mkosi.conf.d/centos/10-centos.conf @@ -28,6 +28,7 @@ Packages= libasan libbpf libcap-ng + libcap-ng-utils libfido2 libmicrohttpd libmnl @@ -38,6 +39,7 @@ Packages= numactl-libs p11-kit pam + passwd polkit popt procps-ng diff --git a/mkosi.conf.d/debian/10-debian.conf b/mkosi.conf.d/debian/10-debian.conf index 905378a9b4..0712a70bea 100644 --- a/mkosi.conf.d/debian/10-debian.conf +++ b/mkosi.conf.d/debian/10-debian.conf @@ -20,6 +20,7 @@ Packages= libasound2 libbpf1 libc6-i386 + libcap-ng-utils libcap-ng0 libfido2-1 libglib2.0-0 @@ -35,6 +36,7 @@ Packages= libqrencode4 libtss2-dev # Use the -dev package to avoid churn in updating version numbers netcat-openbsd + passwd policykit-1 procps python3-pefile diff --git a/mkosi.conf.d/fedora/10-fedora.conf b/mkosi.conf.d/fedora/10-fedora.conf index 5d4959ba16..5c85eebc03 100644 --- a/mkosi.conf.d/fedora/10-fedora.conf +++ b/mkosi.conf.d/fedora/10-fedora.conf @@ -24,6 +24,7 @@ Packages= libasan libbpf libcap-ng + libcap-ng-utils libfido2 libgcrypt libmicrohttpd @@ -34,6 +35,7 @@ Packages= netcat numactl-libs pam + passwd polkit popt procps-ng diff --git a/mkosi.conf.d/opensuse/10-opensuse.conf b/mkosi.conf.d/opensuse/10-opensuse.conf index 6034b8a87e..3b0a643efd 100644 --- a/mkosi.conf.d/opensuse/10-opensuse.conf +++ b/mkosi.conf.d/opensuse/10-opensuse.conf @@ -18,6 +18,7 @@ Packages= glibc-locale-base libasound2 libbpf1 + libcap-ng-utils libcap-ng0 libcrypt1 libcryptsetup12 @@ -38,6 +39,7 @@ Packages= libxkbcommon0 pam python3-pefile + shadow tpm2-0-tss vim diff --git a/mkosi.conf.d/ubuntu/10-ubuntu.conf b/mkosi.conf.d/ubuntu/10-ubuntu.conf index 647a71826c..a34fe8d94f 100644 --- a/mkosi.conf.d/ubuntu/10-ubuntu.conf +++ b/mkosi.conf.d/ubuntu/10-ubuntu.conf @@ -21,6 +21,7 @@ Packages= libasound2 libbpf0 libc6-i386 + libcap-ng-utils libcap-ng0 libfdisk1 libfido2-1 @@ -37,6 +38,7 @@ Packages= linux-tools-common linux-tools-generic netcat-openbsd + passwd policykit-1 procps python3-pefile From 8e1bc689de4c0824f674c0dea7e4c741c601be4c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 17 Feb 2023 22:22:16 +0100 Subject: [PATCH 2/6] user-record: extend user records with an ambient and bounding caps set field In particular the ambieht caps field is useful: we can use it later to pass caps such as CAP_WAKE_ALARM to regular users on login. --- src/shared/user-record-show.c | 17 ++++++++++++++ src/shared/user-record.c | 44 +++++++++++++++++++++++++++++++++++ src/shared/user-record.h | 5 ++++ 3 files changed, 66 insertions(+) diff --git a/src/shared/user-record-show.c b/src/shared/user-record-show.c index da181c6078..28fa7a8632 100644 --- a/src/shared/user-record-show.c +++ b/src/shared/user-record-show.c @@ -1,5 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "cap-list.h" #include "format-util.h" #include "fs-util.h" #include "process-util.h" @@ -287,6 +288,22 @@ void user_record_show(UserRecord *hr, bool show_full_group_info) { if (hr->access_mode != MODE_INVALID) printf(" Access Mode: 0%03o\n", user_record_access_mode(hr)); + uint64_t caps = user_record_capability_bounding_set(hr); + if (caps != UINT64_MAX) { + _cleanup_free_ char *scaps = NULL; + + (void) capability_set_to_string_negative(caps, &scaps); + printf(" Bound. Caps: %s\n", strna(scaps)); + } + + caps = user_record_capability_ambient_set(hr); + if (caps != UINT64_MAX) { + _cleanup_free_ char *scaps = NULL; + + (void) capability_set_to_string(caps, &scaps); + printf("Ambient Caps: %s\n", strna(scaps)); + } + if (storage == USER_LUKS) { printf("LUKS Discard: online=%s offline=%s\n", yes_no(user_record_luks_discard(hr)), yes_no(user_record_luks_offline_discard(hr))); diff --git a/src/shared/user-record.c b/src/shared/user-record.c index 06bc699572..b51d8add1a 100644 --- a/src/shared/user-record.c +++ b/src/shared/user-record.c @@ -2,6 +2,7 @@ #include +#include "cap-list.h" #include "cgroup-util.h" #include "dns-domain.h" #include "env-util.h" @@ -165,6 +166,8 @@ static UserRecord* user_record_free(UserRecord *h) { free(h->home_directory_auto); strv_free(h->member_of); + strv_free(h->capability_bounding_set); + strv_free(h->capability_ambient_set); free(h->file_system_type); free(h->luks_cipher); @@ -1203,6 +1206,8 @@ static int dispatch_per_machine(const char *name, JsonVariant *variant, JsonDisp { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, uid), 0 }, { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, gid), 0 }, { "memberOf", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(UserRecord, member_of), JSON_RELAX}, + { "capabilityBoundingSet", JSON_VARIANT_ARRAY, json_dispatch_strv, offsetof(UserRecord, capability_bounding_set), JSON_SAFE }, + { "capabilityAmbientSet", JSON_VARIANT_ARRAY, json_dispatch_strv, offsetof(UserRecord, capability_ambient_set), JSON_SAFE }, { "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, file_system_type), JSON_SAFE }, { "partitionUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, partition_uuid), 0 }, { "luksUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, luks_uuid), 0 }, @@ -1557,6 +1562,8 @@ int user_record_load(UserRecord *h, JsonVariant *v, UserRecordLoadFlags load_fla { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, uid), 0 }, { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, gid), 0 }, { "memberOf", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(UserRecord, member_of), JSON_RELAX}, + { "capabilityBoundingSet", JSON_VARIANT_ARRAY, json_dispatch_strv, offsetof(UserRecord, capability_bounding_set), JSON_SAFE }, + { "capabilityAmbientSet", JSON_VARIANT_ARRAY, json_dispatch_strv, offsetof(UserRecord, capability_ambient_set), JSON_SAFE }, { "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, file_system_type), JSON_SAFE }, { "partitionUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, partition_uuid), 0 }, { "luksUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, luks_uuid), 0 }, @@ -2018,6 +2025,43 @@ uint64_t user_record_rebalance_weight(UserRecord *h) { return h->rebalance_weight; } +static uint64_t parse_caps_strv(char **l) { + uint64_t c = 0; + int r; + + STRV_FOREACH(i, l) { + r = capability_from_name(*i); + if (r < 0) + log_debug_errno(r, "Don't know capability '%s', ignoring: %m", *i); + else + c |= UINT64_C(1) << r; + } + + return c; +} + +uint64_t user_record_capability_bounding_set(UserRecord *h) { + assert(h); + + /* Returns UINT64_MAX if no bounding set is configured (!) */ + + if (!h->capability_bounding_set) + return UINT64_MAX; + + return parse_caps_strv(h->capability_bounding_set); +} + +uint64_t user_record_capability_ambient_set(UserRecord *h) { + assert(h); + + /* Returns UINT64_MAX if no ambient set is configured (!) */ + + if (!h->capability_ambient_set) + return UINT64_MAX; + + return parse_caps_strv(h->capability_ambient_set) & user_record_capability_bounding_set(h); +} + uint64_t user_record_ratelimit_next_try(UserRecord *h) { assert(h); diff --git a/src/shared/user-record.h b/src/shared/user-record.h index 73fb86cc94..298dc244fe 100644 --- a/src/shared/user-record.h +++ b/src/shared/user-record.h @@ -366,6 +366,9 @@ typedef struct UserRecord { RecoveryKey *recovery_key; size_t n_recovery_key; + char **capability_bounding_set; + char **capability_ambient_set; + JsonVariant *json; } UserRecord; @@ -410,6 +413,8 @@ bool user_record_can_authenticate(UserRecord *h); bool user_record_drop_caches(UserRecord *h); AutoResizeMode user_record_auto_resize_mode(UserRecord *h); uint64_t user_record_rebalance_weight(UserRecord *h); +uint64_t user_record_capability_bounding_set(UserRecord *h); +uint64_t user_record_capability_ambient_set(UserRecord *h); int user_record_build_image_path(UserStorage storage, const char *user_name_and_realm, char **ret); From fada2c75a4ced4f8275a57ec2227389c070238f2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 17 Feb 2023 22:25:47 +0100 Subject: [PATCH 3/6] homectl: make the new caps field configurable via homectl --- man/homectl.xml | 13 +++++++++ src/home/homectl.c | 69 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/man/homectl.xml b/man/homectl.xml index 634b95a081..36c1741eb5 100644 --- a/man/homectl.xml +++ b/man/homectl.xml @@ -271,6 +271,19 @@ the user will be removed from the group. + + CAPABILITIES + CAPABILITIES + + These options take a space separated list of process capabilities + (e.g. CAP_WAKE_ALARM, CAP_BLOCK_SUSPEND, …) that shall be + set in the capability bounding and ambient sets for all the user's sessions. See capabilities7 + for details on the capabilities concept. These options may be used more than once, in which case the + specified lists are combined. If the parameter begins with a ~ character the + effect is inverted: the specified capability is dropped from the specific set. + + PATH diff --git a/src/home/homectl.c b/src/home/homectl.c index cd24b5e9a5..531b685e22 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -9,6 +9,8 @@ #include "bus-common-errors.h" #include "bus-error.h" #include "bus-locator.h" +#include "cap-list.h" +#include "capability-util.h" #include "cgroup-util.h" #include "dns-domain.h" #include "env-util.h" @@ -38,10 +40,10 @@ #include "spawn-polkit-agent.h" #include "terminal-util.h" #include "uid-alloc-range.h" +#include "user-record.h" #include "user-record-pwquality.h" #include "user-record-show.h" #include "user-record-util.h" -#include "user-record.h" #include "user-util.h" #include "verbs.h" @@ -76,6 +78,8 @@ static enum { EXPORT_FORMAT_STRIPPED, /* strip "state" + "binding", but leave signature in place */ EXPORT_FORMAT_MINIMAL, /* also strip signature */ } arg_export_format = EXPORT_FORMAT_FULL; +static uint64_t arg_capability_bounding_set = UINT64_MAX; +static uint64_t arg_capability_ambient_set = UINT64_MAX; STATIC_DESTRUCTOR_REGISTER(arg_identity_extra, json_variant_unrefp); STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_this_machine, json_variant_unrefp); @@ -2210,6 +2214,10 @@ static int help(int argc, char *argv[], void *userdata) { " -d --home-dir=PATH Home directory\n" " -u --uid=UID Numeric UID for user\n" " -G --member-of=GROUP Add user to group\n" + " --capability-bounding-set=CAPS\n" + " Bounding POSIX capability set\n" + " --capability-ambient-set=CAPS\n" + " Ambient POSIX capability set\n" " --skel=PATH Skeleton directory to use\n" " --shell=PATH Shell for account\n" " --setenv=VARIABLE[=VALUE] Set an environment variable at log-in\n" @@ -2402,6 +2410,8 @@ static int parse_argv(int argc, char *argv[]) { ARG_AUTO_RESIZE_MODE, ARG_REBALANCE_WEIGHT, ARG_FIDO2_CRED_ALG, + ARG_CAPABILITY_BOUNDING_SET, + ARG_CAPABILITY_AMBIENT_SET, }; static const struct option options[] = { @@ -2492,6 +2502,8 @@ static int parse_argv(int argc, char *argv[]) { { "luks-extra-mount-options", required_argument, NULL, ARG_LUKS_EXTRA_MOUNT_OPTIONS }, { "auto-resize-mode", required_argument, NULL, ARG_AUTO_RESIZE_MODE }, { "rebalance-weight", required_argument, NULL, ARG_REBALANCE_WEIGHT }, + { "capability-bounding-set", required_argument, NULL, ARG_CAPABILITY_BOUNDING_SET }, + { "capability-ambient-set", required_argument, NULL, ARG_CAPABILITY_AMBIENT_SET }, {} }; @@ -3735,6 +3747,61 @@ static int parse_argv(int argc, char *argv[]) { break; } + case ARG_CAPABILITY_AMBIENT_SET: + case ARG_CAPABILITY_BOUNDING_SET: { + _cleanup_strv_free_ char **l = NULL; + bool subtract = false; + uint64_t parsed, *which, updated; + const char *p, *field; + + if (c == ARG_CAPABILITY_AMBIENT_SET) { + which = &arg_capability_ambient_set; + field = "capabilityAmbientSet"; + } else { + assert(c == ARG_CAPABILITY_BOUNDING_SET); + which = &arg_capability_bounding_set; + field = "capabilityBoundingSet"; + } + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + + *which = UINT64_MAX; + break; + } + + p = optarg; + if (*p == '~') { + subtract = true; + p++; + } + + r = capability_set_from_string(p, &parsed); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid capabilities in capability string '%s'.", p); + if (r < 0) + return log_error_errno(r, "Failed to parse capability string '%s': %m", p); + + if (*which == UINT64_MAX) + updated = subtract ? all_capabilities() & ~parsed : parsed; + else if (subtract) + updated = *which & ~parsed; + else + updated = *which | parsed; + + if (capability_set_to_strv(updated, &l) < 0) + return log_oom(); + + r = json_variant_set_field_strv(&arg_identity_extra, field, l); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + + *which = updated; + break; + } + case '?': return -EINVAL; From bf1b9ae487b65b1cb1639b222724fab95e508cf5 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 17 Feb 2023 22:49:16 +0100 Subject: [PATCH 4/6] pam_systemd: process the two new capabilities user records fields in pam_systemd And also: by default, for the systemd-user service and for local sessions (i.e. those assigned to a seat): let's imply CAP_WAKE_SYSTEM for them by default. Yes, let's pass one specific capability by default to local unprivileged users. The capability services exactly once purpose: to allow system wake-up from suspend via alarm clocks, hence is relatively limited in focus. By adding this tools such as GNOME's Alarm Clock app can simply allocate a CLOCK_REALTIME_ALARM (or ask systemd --user to do this) timer and it will wake up the system as necessary. Note that systemd --user will not pass the ambient caps on by default, so even with this change, individual services need to use AmbientCapabilities= to pass this on to the individual programs. Fixes: #17564 #21382 --- man/pam_systemd.xml | 16 +++++ src/login/pam_systemd.c | 148 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 151 insertions(+), 13 deletions(-) diff --git a/man/pam_systemd.xml b/man/pam_systemd.xml index 60b8577822..f2bd3de0b0 100644 --- a/man/pam_systemd.xml +++ b/man/pam_systemd.xml @@ -122,6 +122,22 @@ further details. + + default-capability-bounding-set= + default-capability-ambient-set= + + Takes a comma-separated list of process capabilities + (e.g. CAP_WAKE_ALARM, CAP_BLOCK_SUSPEND, …) to set for the + invoked session's processes, if the user record does not encode appropriate sets of capabilities + directly. See capabilities7 + for details on the capabilities concept. If not specified, the default bounding set is left as is + (i.e. usually contains the full set of capabilities). The default ambient set is set to + CAP_WAKE_ALARM for regular users if the PAM session is associated with a local + seat or if it is invoked for the systemd-user service. Otherwise defaults to the + empty set. + + debug= diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index 2cfe52288e..65d09886be 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -21,6 +21,8 @@ #include "bus-error.h" #include "bus-internal.h" #include "bus-locator.h" +#include "cap-list.h" +#include "capability-util.h" #include "cgroup-setup.h" #include "devnum-util.h" #include "errno-util.h" @@ -47,20 +49,79 @@ #define LOGIN_SLOW_BUS_CALL_TIMEOUT_USEC (2*USEC_PER_MINUTE) +static int parse_caps( + pam_handle_t *handle, + const char *value, + uint64_t *caps) { + + bool subtract; + int r; + + assert(handle); + assert(value); + + if (value[0] == '~') { + subtract = true; + value++; + } else + subtract = false; + + for (;;) { + _cleanup_free_ char *s = NULL; + uint64_t b, m; + int c; + + /* We can't use spaces as separators here, as PAM's simplistic argument parser doesn't allow + * spaces inside of arguments. We use commas instead (which is similar to cap_from_text(), + * which also uses commas). */ + r = extract_first_word(&value, &s, ",", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r < 0) + return r; + if (r == 0) + break; + + c = capability_from_name(s); + if (c < 0) { + pam_syslog(handle, LOG_WARNING, "Unknown capability, ignoring: %s", s); + continue; + } + + m = UINT64_C(1) << c; + + if (!caps) + continue; + + if (*caps == UINT64_MAX) + b = subtract ? all_capabilities() : 0; + else + b = *caps; + + if (subtract) + *caps = b & ~m; + else + *caps = b | m; + } + + return 0; +} + static int parse_argv( pam_handle_t *handle, int argc, const char **argv, const char **class, const char **type, const char **desktop, - bool *debug) { + bool *debug, + uint64_t *default_capability_bounding_set, + uint64_t *default_capability_ambient_set) { - unsigned i; + int r; + assert(handle); assert(argc >= 0); assert(argc == 0 || argv); - for (i = 0; i < (unsigned) argc; i++) { + for (int i = 0; i < argc; i++) { const char *p; if ((p = startswith(argv[i], "class="))) { @@ -80,16 +141,24 @@ static int parse_argv( *debug = true; } else if ((p = startswith(argv[i], "debug="))) { - int k; - - k = parse_boolean(p); - if (k < 0) + r = parse_boolean(p); + if (r < 0) pam_syslog(handle, LOG_WARNING, "Failed to parse debug= argument, ignoring: %s", p); else if (debug) - *debug = k; + *debug = r; + + } else if ((p = startswith(argv[i], "default-capability-bounding-set="))) { + r = parse_caps(handle, p, default_capability_bounding_set); + if (r < 0) + pam_syslog(handle, LOG_WARNING, "Failed to parse default-capability-bounding-set= argument, ignoring: %s", p); + + } else if ((p = startswith(argv[i], "default-capability-ambient-set="))) { + r = parse_caps(handle, p, default_capability_ambient_set); + if (r < 0) + pam_syslog(handle, LOG_WARNING, "Failed to parse default-capability-ambient-set= argument, ignoring: %s", p); } else - pam_syslog(handle, LOG_WARNING, "Unknown parameter '%s', ignoring", argv[i]); + pam_syslog(handle, LOG_WARNING, "Unknown parameter '%s', ignoring.", argv[i]); } return 0; @@ -531,7 +600,12 @@ static int pam_putenv_and_log(pam_handle_t *handle, const char *e, bool debug) { return PAM_SUCCESS; } -static int apply_user_record_settings(pam_handle_t *handle, UserRecord *ur, bool debug) { +static int apply_user_record_settings( + pam_handle_t *handle, + UserRecord *ur, + bool debug, + uint64_t default_capability_bounding_set, + uint64_t default_capability_ambient_set) { int r; assert(handle); @@ -645,6 +719,31 @@ static int apply_user_record_settings(pam_handle_t *handle, UserRecord *ur, bool "Resource limit %s set, based on user record.", rlimit_to_string(rl)); } + uint64_t a, b; + a = user_record_capability_ambient_set(ur); + if (a == UINT64_MAX) + a = default_capability_ambient_set; + + b = user_record_capability_bounding_set(ur); + if (b == UINT64_MAX) + b = default_capability_bounding_set; + + if (a != UINT64_MAX && a != 0) { + a &= b; + + r = capability_ambient_set_apply(a, /* also_inherit= */ true); + if (r < 0) + pam_syslog_errno(handle, LOG_ERR, r, + "Failed to set ambient capabilities, ignoring: %m"); + } + + if (b != UINT64_MAX && !cap_test_all(b)) { + r = capability_bounding_set_drop(b, /* right_now= */ false); + if (r < 0) + pam_syslog_errno(handle, LOG_ERR, r, + "Failed to set bounding capabilities, ignoring: %m"); + } + return PAM_SUCCESS; } @@ -669,6 +768,21 @@ static int configure_runtime_directory( return export_legacy_dbus_address(handle, rt); } +static uint64_t pick_default_capability_ambient_set( + UserRecord *ur, + const char *service, + const char *seat) { + + /* If not configured otherwise, let's enable CAP_WAKE_ALARM for regular users when logging in on a + * seat (i.e. when they are present physically on the device), or when invoked for the systemd --user + * instances. This allows desktops to install CAP_WAKE_ALARM to implement alarm clock apps without + * much fuss. */ + + return ur && + user_record_disposition(ur) == USER_REGULAR && + (streq_ptr(service, "systemd-user") || !isempty(seat)) ? (UINT64_C(1) << CAP_WAKE_ALARM) : UINT64_MAX; +} + _public_ PAM_EXTERN int pam_sm_open_session( pam_handle_t *handle, int flags, @@ -685,6 +799,7 @@ _public_ PAM_EXTERN int pam_sm_open_session( *type = NULL, *class = NULL, *class_pam = NULL, *type_pam = NULL, *cvtnr = NULL, *desktop = NULL, *desktop_pam = NULL, *memory_max = NULL, *tasks_max = NULL, *cpu_weight = NULL, *io_weight = NULL, *runtime_max_sec = NULL; + uint64_t default_capability_bounding_set = UINT64_MAX, default_capability_ambient_set = UINT64_MAX; _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; _cleanup_(user_record_unrefp) UserRecord *ur = NULL; int session_fd = -EBADF, existing, r; @@ -699,7 +814,9 @@ _public_ PAM_EXTERN int pam_sm_open_session( &class_pam, &type_pam, &desktop_pam, - &debug) < 0) + &debug, + &default_capability_bounding_set, + &default_capability_ambient_set) < 0) return PAM_SESSION_ERR; if (debug) @@ -988,7 +1105,10 @@ _public_ PAM_EXTERN int pam_sm_open_session( } success: - r = apply_user_record_settings(handle, ur, debug); + if (default_capability_ambient_set == UINT64_MAX) + default_capability_ambient_set = pick_default_capability_ambient_set(ur, service, seat); + + r = apply_user_record_settings(handle, ur, debug, default_capability_bounding_set, default_capability_ambient_set); if (r != PAM_SUCCESS) return r; @@ -1016,7 +1136,9 @@ _public_ PAM_EXTERN int pam_sm_close_session( NULL, NULL, NULL, - &debug) < 0) + &debug, + NULL, + NULL) < 0) return PAM_SESSION_ERR; if (debug) From d7fce219aedfea378dcbc04c68b41d22d31ffae5 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 22 Feb 2023 18:05:18 +0100 Subject: [PATCH 5/6] test: test setting ambient caps via pam_systemd.so --- test/units/testsuite-35.sh | 53 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/test/units/testsuite-35.sh b/test/units/testsuite-35.sh index 85925f2471..02f22cf2a2 100755 --- a/test/units/testsuite-35.sh +++ b/test/units/testsuite-35.sh @@ -573,6 +573,58 @@ EOF assert_eq "$(loginctl --no-legend | grep -c "logind-test-user")" 0 } +test_ambient_caps() { + local PAMSERVICE TRANSIENTUNIT SCRIPT + + # Verify that pam_systemd works and assigns ambient caps as it should + + if ! grep -q 'CapAmb:' /proc/self/status ; then + echo "ambient caps not available, skipping test." >&2 + return + fi + + typeset -i BND MASK + + # Get PID 1's bounding set + BND="0x$(grep 'CapBnd:' /proc/1/status | cut -d: -f2 | tr -d '[:space:]')" + + # CAP_CHOWN | CAP_KILL + MASK=$(((1 << 0) | (1 << 5))) + + if [ $(("$BND" & "$MASK")) -ne "$MASK" ] ; then + echo "CAP_CHOWN or CAP_KILL not available in bounding set, skipping test." >&2 + return + fi + + PAMSERVICE="pamserv$RANDOM" + TRANSIENTUNIT="capwakealarm$RANDOM.service" + SCRIPT="/tmp/capwakealarm$RANDOM.sh" + + cat > /etc/pam.d/"$PAMSERVICE" < "$SCRIPT" <<'EOF' +#!/bin/bash +set -ex +typeset -i AMB MASK +AMB="0x$(grep 'CapAmb:' /proc/self/status | cut -d: -f2 | tr -d '[:space:]')" +MASK=$(((1 << 0) | (1 << 5))) +test "$AMB" -eq "$MASK" +EOF + + chmod +x "$SCRIPT" + + systemd-run -u "$TRANSIENTUNIT" -p PAMName="$PAMSERVICE" -p Type=oneshot -p User=logind-test-user -p StandardError=tty "$SCRIPT" + + rm -f "$SCRIPT" "$PAMSERVICE" +} + : >/failed setup_test_user @@ -587,6 +639,7 @@ test_lock_idle_action test_session_properties test_list_users test_stop_idle_session +test_ambient_caps touch /testok rm /failed From a1012609f3a1dfc7512caaf78c2b90bcd311a52c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 22 Feb 2023 18:43:45 +0100 Subject: [PATCH 6/6] update NEWS --- NEWS | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/NEWS b/NEWS index a5f882c5ec..4ef7fb2af3 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,23 @@ systemd System and Service Manager +CHANGES WITH 254 in spe: + + Security relevant changes: + + * pam_systemd will now by default pass the CAP_WAKE_ALARM ambient + process capability to invoked session processes of regular users on + local seats (as well as to systemd --user), unless configured + otherwise via data from JSON user records, or via the PAM module's + parameter list. This is useful in order allow desktop tools such as + GNOME's Alarm Clock application to set a timer for + CLOCK_REALTIME_ALARM that wakes up the system when it elapses. A + per-user service unit file may thus use AmbientCapability= to pass + the capability to invoked processes. Note that this capability is + relatively narrow in focus (in particular compared to other process + capabilities such as CAP_SYS_ADMIN) and we already — by default — + permit more impactful operations such as system suspend to local + users. + CHANGES WITH 253: Announcements of Future Feature Removals and Incompatible Changes: