Merge pull request #27438 from bluca/dump_ratelimit

manager: restrict Dump*() to privileged callers or ratelimit
This commit is contained in:
Luca Boccassi 2023-05-19 21:29:58 +01:00 committed by GitHub
commit 382e18fd95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 107 additions and 5 deletions

View File

@ -1403,7 +1403,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>
@ -1778,7 +1779,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>

View File

@ -278,7 +278,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>

View File

@ -49,3 +49,12 @@ usec_t ratelimit_end(const RateLimit *rl) {
return usec_add(rl->begin, rl->interval);
}
usec_t ratelimit_left(const RateLimit *rl) {
assert(rl);
if (rl->begin == 0)
return 0;
return usec_sub_unsigned(ratelimit_end(rl), now(CLOCK_MONOTONIC));
}

View File

@ -25,3 +25,4 @@ bool ratelimit_below(RateLimit *r);
unsigned ratelimit_num_dropped(RateLimit *r);
usec_t ratelimit_end(const RateLimit *rl);
usec_t ratelimit_left(const RateLimit *rl);

View File

@ -1413,17 +1413,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) {

View File

@ -1203,6 +1203,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;

View File

@ -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);

View File

@ -165,6 +165,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);
@ -550,6 +558,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;

View File

@ -913,6 +913,11 @@ int manager_new(RuntimeScope runtime_scope, ManagerTestRunFlags test_run_flags,
.default_memory_pressure_watch = CGROUP_PRESSURE_WATCH_AUTO,
.default_memory_pressure_threshold_usec = USEC_INFINITY,
.dump_ratelimit = {
.interval = 10 * USEC_PER_MINUTE,
.burst = 10,
},
};
#if ENABLE_EFI

View File

@ -471,6 +471,8 @@ struct Manager {
/* Allow users to configure a rate limit for Reload() operations */
RateLimit reload_ratelimit;
/* Dump*() are slow, so always rate limit them to 10 per 10 minutes */
RateLimit dump_ratelimit;
sd_event_source *memory_pressure_event_source;
};

View File

@ -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>

View File

@ -53,6 +53,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