]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
core: Add ProtectHostname=private 35447/head
authorRyan Wilson <ryantimwilson@meta.com>
Mon, 2 Dec 2024 16:10:05 +0000 (08:10 -0800)
committerRyan Wilson <ryantimwilson@meta.com>
Fri, 6 Dec 2024 21:34:04 +0000 (13:34 -0800)
This allows an option for systemd exec units to enable UTS namespaces
but not restrict changing hostname via seccomp. Thus, units can change
hostname without affecting the host.

Fixes: #30348
man/systemd.exec.xml
mkosi.conf
src/core/exec-invoke.c
src/core/namespace.c
src/core/namespace.h
test/units/TEST-07-PID1.protect-hostname.sh [new file with mode: 0755]

index 14075cb4e7d5ceec0f878fbb0fafe1a86d6b7ecc..44ee2022dd25dd73e271913901e6754d6ecfc97d 100644 (file)
@@ -2055,8 +2055,11 @@ BindReadOnlyPaths=/var/lib/systemd</programlisting>
       <varlistentry>
         <term><varname>ProtectHostname=</varname></term>
 
-        <listitem><para>Takes a boolean argument. When set, sets up a new UTS namespace for the executed
-        processes. In addition, changing hostname or domainname is prevented. Defaults to off.</para>
+        <listitem><para>Takes a boolean argument or <literal>private</literal>. If enabled, sets up a new UTS namespace
+        for the executed processes. If set to a true value, changing hostname or domainname via
+        <function>sethostname()</function> and <function>setdomainname()</function> system calls is prevented. If set to
+        <literal>private</literal>, changing hostname or domainname is allowed but only affects the unit's UTS namespace.
+        Defaults to off.</para>
 
         <para>Note that the implementation of this setting might be impossible (for example if UTS namespaces
         are not available), and the unit should be written in a way that does not solely rely on this setting
@@ -2066,6 +2069,12 @@ BindReadOnlyPaths=/var/lib/systemd</programlisting>
         the system into the service, it is hence not suitable for services that need to take notice of system
         hostname changes dynamically.</para>
 
+        <para>Note that this option does not prevent changing system hostname via <command>hostnamectl</command>.
+        However, <varname>User=</varname> and <varname>Group=</varname> may be used to run as an unprivileged user
+        to disallow changing system hostname. See <function>SetHostname()</function> in
+        <citerefentry project="man-pages"><refentrytitle>org.freedesktop.hostname1</refentrytitle><manvolnum>5</manvolnum></citerefentry>
+        for more details.</para>
+
         <xi:include href="system-or-user-ns.xml" xpointer="singular"/>
 
         <xi:include href="version-info.xml" xpointer="v242"/></listitem>
index 35a19a27aad3975b22819b4d56672d57756e06eb..535e2bd79bf4342e66a99c180d3d17c4d2d004d9 100644 (file)
@@ -101,6 +101,7 @@ Packages=
         gdb
         grep
         gzip
+        hostname
         jq
         kbd
         kexec-tools
index f4aacb55b22bd2e19b458a5b7e40508542646762..fd306f1143125ba442b1739ff37080dae421917c 100644 (file)
@@ -1726,15 +1726,17 @@ static int apply_protect_hostname(const ExecContext *c, const ExecParameters *p,
                                  "support UTS namespaces, ignoring namespace setup.");
 
 #if HAVE_SECCOMP
-        int r;
+        if (c->protect_hostname == PROTECT_HOSTNAME_YES) {
+                int r;
 
-        if (skip_seccomp_unavailable(c, p, "ProtectHostname="))
-                return 0;
+                if (skip_seccomp_unavailable(c, p, "ProtectHostname="))
+                        return 0;
 
-        r = seccomp_protect_hostname();
-        if (r < 0) {
-                *ret_exit_status = EXIT_SECCOMP;
-                return log_exec_error_errno(c, p, r, "Failed to apply hostname restrictions: %m");
+                r = seccomp_protect_hostname();
+                if (r < 0) {
+                        *ret_exit_status = EXIT_SECCOMP;
+                        return log_exec_error_errno(c, p, r, "Failed to apply hostname restrictions: %m");
+                }
         }
 #endif
 
@@ -3417,6 +3419,9 @@ static int apply_mount_namespace(
                 .protect_kernel_tunables = needs_sandboxing && context->protect_kernel_tunables,
                 .protect_kernel_modules = needs_sandboxing && context->protect_kernel_modules,
                 .protect_kernel_logs = needs_sandboxing && context->protect_kernel_logs,
+                /* Only mount /proc/sys/kernel/hostname and domainname read-only if ProtectHostname=yes. Otherwise, ProtectHostname=no
+                 * allows changing hostname for the host and ProtectHostname=private allows changing the hostname in the unit's UTS
+                 * namespace. */
                 .protect_hostname = needs_sandboxing && context->protect_hostname == PROTECT_HOSTNAME_YES,
 
                 .private_dev = needs_sandboxing && context->private_devices,
index c327c9a3ca488d6b27dccda3820e989621a1e32a..2f3b8f03d130892afe0f879129bf384dd1667de3 100644 (file)
@@ -3308,6 +3308,7 @@ DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(protect_home, ProtectHome, PROTECT_HOME_
 static const char *const protect_hostname_table[_PROTECT_HOSTNAME_MAX] = {
         [PROTECT_HOSTNAME_NO]      = "no",
         [PROTECT_HOSTNAME_YES]     = "yes",
+        [PROTECT_HOSTNAME_PRIVATE] = "private",
 };
 
 DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(protect_hostname, ProtectHostname, PROTECT_HOSTNAME_YES);
index 8df91e3bdf90600c4881cfb6894a2bcf40dddab4..96f62be30a2690b033d60f1d617b63ef363c1631 100644 (file)
@@ -31,6 +31,7 @@ typedef enum ProtectHome {
 typedef enum ProtectHostname {
         PROTECT_HOSTNAME_NO,
         PROTECT_HOSTNAME_YES,
+        PROTECT_HOSTNAME_PRIVATE,
         _PROTECT_HOSTNAME_MAX,
         _PROTECT_HOSTNAME_INVALID = -EINVAL,
 } ProtectHostname;
diff --git a/test/units/TEST-07-PID1.protect-hostname.sh b/test/units/TEST-07-PID1.protect-hostname.sh
new file mode 100755 (executable)
index 0000000..c2ede39
--- /dev/null
@@ -0,0 +1,44 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# shellcheck disable=SC2016
+set -eux
+set -o pipefail
+
+# shellcheck source=test/units/test-control.sh
+. "$(dirname "$0")"/test-control.sh
+# shellcheck source=test/units/util.sh
+. "$(dirname "$0")"/util.sh
+
+LEGACY_HOSTNAME="$(hostname)"
+HOSTNAME_FROM_SYSTEMD="$(hostnamectl hostname)"
+
+testcase_yes() {
+    # hostnamectl calls SetHostname method via dbus socket which executes in homenamed
+    # in the init namespace. So hostnamectl is not affected by ProtectHostname=yes or
+    # private since sethostname() system call is executed in the init namespace.
+    #
+    # hostnamed does authentication based on UID via polkit so this guarantees admins
+    # can only set hostname.
+    (! systemd-run --wait -p ProtectHostname=yes hostname foo)
+
+    systemd-run --wait -p ProtectHostname=yes -p PrivateMounts=yes \
+        findmnt --mountpoint /proc/sys/kernel/hostname
+}
+
+testcase_private() {
+    systemd-run --wait -p ProtectHostnameEx=private \
+        -P bash -xec '
+            hostname foo
+            test "$(hostname)" = "foo"
+        '
+
+    # Verify host hostname is unchanged.
+    test "$(hostname)" = "$LEGACY_HOSTNAME"
+    test "$(hostnamectl hostname)" = "$HOSTNAME_FROM_SYSTEMD"
+
+    # Verify /proc/sys/kernel/hostname is not bind mounted from host read-only.
+    (! systemd-run --wait -p ProtectHostnameEx=private -p PrivateMounts=yes \
+        findmnt --mountpoint /proc/sys/kernel/hostname)
+}
+
+run_testcases