From 1eabb839d306887264d2aeb401e35286de624ce8 Mon Sep 17 00:00:00 2001 From: Jos Rothman Date: Wed, 4 Nov 2026 22:31:52 +0100 Subject: [PATCH] Let the manager run the helper through sudo ProtectKernelTunables, ProtectKernelModules and LockPersonality imply NoNewPrivileges for a non-root unit, so sudo refused to start the helper (hostname, reboot, network changes all failed). Guarded by a test. Co-Authored-By: Claude Opus 5.5 --- systemd/voxip-kiosk-manager.service | 6 +++--- tests/test_units.py | 30 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 tests/test_units.py diff --git a/systemd/voxip-kiosk-manager.service b/systemd/voxip-kiosk-manager.service index 53ee65b..e310b81 100644 --- a/systemd/voxip-kiosk-manager.service +++ b/systemd/voxip-kiosk-manager.service @@ -22,10 +22,10 @@ SyslogIdentifier=voxip-kiosk-manager # "sudo -n voxip-kiosk-helper" inherits this unit's mount namespace and must # be able to write /etc (hostname, NetworkManager profiles). The web process # itself runs unprivileged as user voxip and can only write its own dirs. -ProtectKernelTunables=yes -ProtectKernelModules=yes +# Also no ProtectKernelTunables/ProtectKernelModules/LockPersonality, +# RestrictSUIDSGID, SystemCall*, ...: for a non-root unit those imply +# NoNewPrivileges, and sudo then refuses to run (tests/test_units.py). ProtectControlGroups=yes -LockPersonality=yes [Install] WantedBy=multi-user.target diff --git a/tests/test_units.py b/tests/test_units.py new file mode 100644 index 0000000..4168935 --- /dev/null +++ b/tests/test_units.py @@ -0,0 +1,30 @@ +from pathlib import Path + +UNITS = Path(__file__).resolve().parent.parent / "systemd" + +# Settings that make systemd set no_new_privs for a unit that does not run as root +# (systemd.exec(5)). sudo cannot gain root under that flag. +IMPLY_NO_NEW_PRIVILEGES = ( + "NoNewPrivileges", "SystemCallFilter", "SystemCallArchitectures", "RestrictAddressFamilies", + "RestrictNamespaces", "PrivateDevices", "ProtectKernelTunables", "ProtectKernelModules", + "ProtectKernelLogs", "ProtectClock", "MemoryDenyWriteExecute", "RestrictRealtime", + "RestrictSUIDSGID", "DynamicUser", "LockPersonality", +) + + +def settings(unit): + result = {} + for line in (UNITS / unit).read_text().splitlines(): + key, sep, value = line.partition("=") + if sep and not line.startswith("#"): + result[key.strip()] = value.strip() + return result + + +def test_manager_can_use_sudo_for_the_helper(): + unit = settings("voxip-kiosk-manager.service") + assert unit["User"] == "voxip" + for key in IMPLY_NO_NEW_PRIVILEGES: + assert unit.get(key, "no") in ("no", "false", ""), key + for key in ("ProtectSystem", "ProtectHome", "PrivateTmp"): + assert key not in unit, key