75aa201631
Resolves: RHEL-13159,RHEL-20322,RHEL-27512,RHEL-30372,RHEL-31070,RHEL-31219,RHEL-33890,RHEL-35703,RHEL-38864,RHEL-40878,RHEL-6589
265 lines
13 KiB
Diff
265 lines
13 KiB
Diff
From 10e811e931efd55cab87b85eb75cbe73139cec43 Mon Sep 17 00:00:00 2001
|
|
From: Luca Boccassi <bluca@debian.org>
|
|
Date: Thu, 27 Apr 2023 23:23:30 +0100
|
|
Subject: [PATCH] manager: restrict Dump*() to privileged callers or ratelimit
|
|
|
|
Dump*() methods can take quite some time due to the amount of data to
|
|
serialize, so they can potentially stall the manager. Make them
|
|
privileged, as they are debugging tools anyway. Use a new 'dump'
|
|
capability for polkit, and the 'reload' capability for SELinux, as
|
|
that's also non-destructive but slow.
|
|
|
|
If the caller is not privileged, allow it but rate limited to 10 calls
|
|
every 10 minutes.
|
|
|
|
(cherry picked from commit d936595672cf3ee7c1c547f8fd30512f82be8784)
|
|
|
|
Resolves: RHEL-35703
|
|
---
|
|
man/org.freedesktop.systemd1.xml | 7 +++--
|
|
man/systemd-analyze.xml | 2 +-
|
|
src/core/dbus-manager.c | 34 +++++++++++++++++++--
|
|
src/core/dbus.c | 3 ++
|
|
src/core/dbus.h | 1 +
|
|
src/core/manager-serialize.c | 23 ++++++++++++++
|
|
src/core/manager.c | 5 +++
|
|
src/core/manager.h | 3 ++
|
|
src/core/org.freedesktop.systemd1.policy.in | 10 ++++++
|
|
test/units/testsuite-65.sh | 15 +++++++++
|
|
10 files changed, 98 insertions(+), 5 deletions(-)
|
|
|
|
diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml
|
|
index c18428a092..7ee649f6a7 100644
|
|
--- a/man/org.freedesktop.systemd1.xml
|
|
+++ b/man/org.freedesktop.systemd1.xml
|
|
@@ -1363,7 +1363,8 @@ node /org/freedesktop/systemd1 {
|
|
<function>DumpByFileDescriptor()</function>/<function>DumpUnitsMatchingPatternsByFileDescriptor()</function>
|
|
are usually the preferred interface, since it ensures the data can be passed reliably from the service
|
|
manager to the client. Note though that they cannot work when communicating with the service manager
|
|
- remotely, as file descriptors are strictly local to a system.</para>
|
|
+ remotely, as file descriptors are strictly local to a system. All the <function>Dump*()</function>
|
|
+ methods are rate limited for unprivileged users.</para>
|
|
|
|
<para><function>Reload()</function> may be invoked to reload all unit files.</para>
|
|
|
|
@@ -1726,7 +1727,9 @@ node /org/freedesktop/systemd1 {
|
|
<function>UnsetAndSetEnvironment()</function>) require
|
|
<interfacename>org.freedesktop.systemd1.set-environment</interfacename>. <function>Reload()</function>
|
|
and <function>Reexecute()</function> require
|
|
- <interfacename>org.freedesktop.systemd1.reload-daemon</interfacename>.
|
|
+ <interfacename>org.freedesktop.systemd1.reload-daemon</interfacename>. Operations which dump internal
|
|
+ state require <interfacename>org.freedesktop.systemd1.bypass-dump-ratelimit</interfacename> to avoid
|
|
+ rate limits.
|
|
</para>
|
|
</refsect2>
|
|
</refsect1>
|
|
diff --git a/man/systemd-analyze.xml b/man/systemd-analyze.xml
|
|
index 5ba0d40fa0..ff5c84108d 100644
|
|
--- a/man/systemd-analyze.xml
|
|
+++ b/man/systemd-analyze.xml
|
|
@@ -249,7 +249,7 @@ multi-user.target @47.820s
|
|
<para>Without any parameter, this command outputs a (usually very long) human-readable serialization of
|
|
the complete service manager state. Optional glob pattern may be specified, causing the output to be
|
|
limited to units whose names match one of the patterns. The output format is subject to change without
|
|
- notice and should not be parsed by applications.</para>
|
|
+ notice and should not be parsed by applications. This command is rate limited for unprivileged users.</para>
|
|
|
|
<example>
|
|
<title>Show the internal state of user manager</title>
|
|
diff --git a/src/core/dbus-manager.c b/src/core/dbus-manager.c
|
|
index 00380cc9c1..44b1027588 100644
|
|
--- a/src/core/dbus-manager.c
|
|
+++ b/src/core/dbus-manager.c
|
|
@@ -1357,17 +1357,47 @@ static int dump_impl(
|
|
|
|
assert(message);
|
|
|
|
- /* Anyone can call this method */
|
|
-
|
|
+ /* 'status' access is the bare minimum always needed for this, as the policy might straight out
|
|
+ * forbid a client from querying any information from systemd, regardless of any rate limiting. */
|
|
r = mac_selinux_access_check(message, "status", error);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
+ /* Rate limit reached? Check if the caller is privileged/allowed by policy to bypass this. We
|
|
+ * check the rate limit first to avoid the expensive roundtrip to polkit when not needed. */
|
|
+ if (!ratelimit_below(&m->dump_ratelimit)) {
|
|
+ /* We need a way for SELinux to constrain the operation when the rate limit is active, even
|
|
+ * if polkit would allow it, but we cannot easily add new named permissions, so we need to
|
|
+ * use an existing one. Reload/reexec are also slow but non-destructive/modifying
|
|
+ * operations, and can cause PID1 to stall. So it seems similar enough in terms of security
|
|
+ * considerations and impact, and thus use the same access check for dumps which, given the
|
|
+ * large amount of data to fetch, can stall PID1 for quite some time. */
|
|
+ r = mac_selinux_access_check(message, "reload", error);
|
|
+ if (r < 0)
|
|
+ goto ratelimited;
|
|
+
|
|
+ r = bus_verify_bypass_dump_ratelimit_async(m, message, error);
|
|
+ if (r < 0)
|
|
+ goto ratelimited;
|
|
+ if (r == 0)
|
|
+ /* No authorization for now, but the async polkit stuff will call us again when it
|
|
+ * has it */
|
|
+ return 1;
|
|
+ }
|
|
+
|
|
r = manager_get_dump_string(m, patterns, &dump);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
return reply(message, dump);
|
|
+
|
|
+ratelimited:
|
|
+ log_warning("Dump request rejected due to rate limit on unprivileged callers, blocked for %s.",
|
|
+ FORMAT_TIMESPAN(ratelimit_left(&m->dump_ratelimit), USEC_PER_SEC));
|
|
+ return sd_bus_error_setf(error,
|
|
+ SD_BUS_ERROR_LIMITS_EXCEEDED,
|
|
+ "Dump request rejected due to rate limit on unprivileged callers, blocked for %s.",
|
|
+ FORMAT_TIMESPAN(ratelimit_left(&m->dump_ratelimit), USEC_PER_SEC));
|
|
}
|
|
|
|
static int reply_dump(sd_bus_message *message, char *dump) {
|
|
diff --git a/src/core/dbus.c b/src/core/dbus.c
|
|
index 141c3ffe12..3cbe9c5cfd 100644
|
|
--- a/src/core/dbus.c
|
|
+++ b/src/core/dbus.c
|
|
@@ -1177,6 +1177,9 @@ int bus_verify_reload_daemon_async(Manager *m, sd_bus_message *call, sd_bus_erro
|
|
int bus_verify_set_environment_async(Manager *m, sd_bus_message *call, sd_bus_error *error) {
|
|
return bus_verify_polkit_async(call, CAP_SYS_ADMIN, "org.freedesktop.systemd1.set-environment", NULL, false, UID_INVALID, &m->polkit_registry, error);
|
|
}
|
|
+int bus_verify_bypass_dump_ratelimit_async(Manager *m, sd_bus_message *call, sd_bus_error *error) {
|
|
+ return bus_verify_polkit_async(call, CAP_SYS_ADMIN, "org.freedesktop.systemd1.bypass-dump-ratelimit", NULL, false, UID_INVALID, &m->polkit_registry, error);
|
|
+}
|
|
|
|
uint64_t manager_bus_n_queued_write(Manager *m) {
|
|
uint64_t c = 0;
|
|
diff --git a/src/core/dbus.h b/src/core/dbus.h
|
|
index 369d9f56a2..50e7bb400e 100644
|
|
--- a/src/core/dbus.h
|
|
+++ b/src/core/dbus.h
|
|
@@ -27,6 +27,7 @@ int bus_verify_manage_units_async(Manager *m, sd_bus_message *call, sd_bus_error
|
|
int bus_verify_manage_unit_files_async(Manager *m, sd_bus_message *call, sd_bus_error *error);
|
|
int bus_verify_reload_daemon_async(Manager *m, sd_bus_message *call, sd_bus_error *error);
|
|
int bus_verify_set_environment_async(Manager *m, sd_bus_message *call, sd_bus_error *error);
|
|
+int bus_verify_bypass_dump_ratelimit_async(Manager *m, sd_bus_message *call, sd_bus_error *error);
|
|
|
|
int bus_forward_agent_released(Manager *m, const char *path);
|
|
|
|
diff --git a/src/core/manager-serialize.c b/src/core/manager-serialize.c
|
|
index 27cb0925ae..f3b2d7ee16 100644
|
|
--- a/src/core/manager-serialize.c
|
|
+++ b/src/core/manager-serialize.c
|
|
@@ -164,6 +164,14 @@ int manager_serialize(
|
|
(void) serialize_item_format(f, "user-lookup", "%i %i", copy0, copy1);
|
|
}
|
|
|
|
+ (void) serialize_item_format(f,
|
|
+ "dump-ratelimit",
|
|
+ USEC_FMT " " USEC_FMT " %u %u",
|
|
+ m->dump_ratelimit.begin,
|
|
+ m->dump_ratelimit.interval,
|
|
+ m->dump_ratelimit.num,
|
|
+ m->dump_ratelimit.burst);
|
|
+
|
|
bus_track_serialize(m->subscribed, f, "subscribed");
|
|
|
|
r = dynamic_user_serialize(m, f, fds);
|
|
@@ -549,6 +557,21 @@ int manager_deserialize(Manager *m, FILE *f, FDSet *fds) {
|
|
* remains set until all serialized contents are handled. */
|
|
if (deserialize_varlink_sockets)
|
|
(void) varlink_server_deserialize_one(m->varlink_server, val, fds);
|
|
+ } else if ((val = startswith(l, "dump-ratelimit="))) {
|
|
+ usec_t begin, interval;
|
|
+ unsigned num, burst;
|
|
+
|
|
+ if (sscanf(val, USEC_FMT " " USEC_FMT " %u %u", &begin, &interval, &num, &burst) != 4)
|
|
+ log_notice("Failed to parse dump ratelimit, ignoring: %s", val);
|
|
+ else {
|
|
+ /* If we changed the values across versions, flush the counter */
|
|
+ if (interval != m->dump_ratelimit.interval || burst != m->dump_ratelimit.burst)
|
|
+ m->dump_ratelimit.num = 0;
|
|
+ else
|
|
+ m->dump_ratelimit.num = num;
|
|
+ m->dump_ratelimit.begin = begin;
|
|
+ }
|
|
+
|
|
} else {
|
|
ManagerTimestamp q;
|
|
|
|
diff --git a/src/core/manager.c b/src/core/manager.c
|
|
index eeee395b90..b44c7785cf 100644
|
|
--- a/src/core/manager.c
|
|
+++ b/src/core/manager.c
|
|
@@ -866,6 +866,11 @@ int manager_new(LookupScope scope, ManagerTestRunFlags test_run_flags, Manager *
|
|
.test_run_flags = test_run_flags,
|
|
|
|
.default_oom_policy = OOM_STOP,
|
|
+
|
|
+ .dump_ratelimit = {
|
|
+ .interval = 10 * USEC_PER_MINUTE,
|
|
+ .burst = 10,
|
|
+ },
|
|
};
|
|
|
|
#if ENABLE_EFI
|
|
diff --git a/src/core/manager.h b/src/core/manager.h
|
|
index 87e63c3b68..86e7e40989 100644
|
|
--- a/src/core/manager.h
|
|
+++ b/src/core/manager.h
|
|
@@ -461,6 +461,9 @@ struct Manager {
|
|
struct restrict_fs_bpf *restrict_fs;
|
|
|
|
char *default_smack_process_label;
|
|
+
|
|
+ /* Dump*() are slow, so always rate limit them to 10 per 10 minutes */
|
|
+ RateLimit dump_ratelimit;
|
|
};
|
|
|
|
static inline usec_t manager_default_timeout_abort_usec(Manager *m) {
|
|
diff --git a/src/core/org.freedesktop.systemd1.policy.in b/src/core/org.freedesktop.systemd1.policy.in
|
|
index 74adeadf38..9e9a20f66f 100644
|
|
--- a/src/core/org.freedesktop.systemd1.policy.in
|
|
+++ b/src/core/org.freedesktop.systemd1.policy.in
|
|
@@ -70,4 +70,14 @@
|
|
</defaults>
|
|
</action>
|
|
|
|
+ <action id="org.freedesktop.systemd1.bypass-dump-ratelimit">
|
|
+ <description gettext-domain="systemd">Dump the systemd state without rate limits</description>
|
|
+ <message gettext-domain="systemd">Authentication is required to dump the systemd state without rate limits.</message>
|
|
+ <defaults>
|
|
+ <allow_any>auth_admin</allow_any>
|
|
+ <allow_inactive>auth_admin</allow_inactive>
|
|
+ <allow_active>auth_admin_keep</allow_active>
|
|
+ </defaults>
|
|
+ </action>
|
|
+
|
|
</policyconfig>
|
|
diff --git a/test/units/testsuite-65.sh b/test/units/testsuite-65.sh
|
|
index 7c34948f82..f416194922 100755
|
|
--- a/test/units/testsuite-65.sh
|
|
+++ b/test/units/testsuite-65.sh
|
|
@@ -51,6 +51,21 @@ systemd-analyze dot --require systemd-journald.service systemd-logind.service >/
|
|
systemd-analyze dot "systemd-*.service" >/dev/null
|
|
(! systemd-analyze dot systemd-journald.service systemd-logind.service "*" bbb ccc)
|
|
# dump
|
|
+# this should be rate limited to 10 calls in 10 minutes for unprivileged callers
|
|
+for _ in {1..10}; do
|
|
+ runas testuser systemd-analyze dump systemd-journald.service >/dev/null
|
|
+done
|
|
+(! runas testuser systemd-analyze dump >/dev/null)
|
|
+# still limited after a reload
|
|
+systemctl daemon-reload
|
|
+(! runas testuser systemd-analyze dump >/dev/null)
|
|
+# and a re-exec
|
|
+systemctl daemon-reexec
|
|
+(! runas testuser systemd-analyze dump >/dev/null)
|
|
+# privileged call, so should not be rate limited
|
|
+for _ in {1..10}; do
|
|
+ systemd-analyze dump systemd-journald.service >/dev/null
|
|
+done
|
|
systemd-analyze dump >/dev/null
|
|
systemd-analyze dump "*" >/dev/null
|
|
systemd-analyze dump "*.socket" >/dev/null
|