Root cause: gnome-remote-desktop-setup.service ran as root but dropped privileges via `runuser -u gnome-remote-desktop`. Non-root grdctl --system attempts authorisation through pkexec. The Nix-store pkexec binary is not setuid, so every system-mode credential call silently failed while the script still printed "configured successfully". GNOME Remote Desktop then started without applied credentials, causing Remmina to loop at the login dialog. Fix: - Remove `runuser -u gnome-remote-desktop --` from grdctl_system helper; the root-run oneshot service can call grdctl --system directly. - Remove pkgs.polkit and pkgs.util-linux from the setup service path as neither polkit nor runuser is needed any more. - Update tests: rename test_setup_runs_grdctl_as_gnome_remote_desktop_user to test_setup_runs_grdctl_directly_as_root and assert that runuser, pkexec, and sudo are absent; add test_privilege_escalation_packages_absent_from_setup_path. All 21 app/tests pass.
98 lines
5.1 KiB
Python
98 lines
5.1 KiB
Python
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
RDP_NIX = Path(__file__).resolve().parents[2] / "modules" / "rdp.nix"
|
|
USERNAME_READ = "USERNAME=\"$(tr -d '\\n' < \"$USERNAME_FILE\")\""
|
|
USERNAME_LENGTH_GUARD = "if [ \"''${#USERNAME}\" -gt 32 ]; then"
|
|
SHORT_PASSWORD_GUARD = 'if [ "\'\'${#PASSWORD}" -lt 8 ]; then'
|
|
|
|
|
|
def _section(source: str, start: str, end: str) -> str:
|
|
start_idx = source.find(start)
|
|
if start_idx == -1:
|
|
raise AssertionError(f"Expected section start not found: {start!r}")
|
|
end_idx = source.find(end, start_idx)
|
|
if end_idx == -1:
|
|
raise AssertionError(f"Expected section end not found: {end!r}")
|
|
return source[start_idx:end_idx]
|
|
|
|
|
|
class RdpModuleBootSetupTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.source = RDP_NIX.read_text()
|
|
self.gnome_service = _section(
|
|
self.source,
|
|
"systemd.services.gnome-remote-desktop = {",
|
|
"systemd.tmpfiles.rules = [",
|
|
)
|
|
self.setup_service = _section(
|
|
self.source,
|
|
"systemd.services.gnome-remote-desktop-setup = {",
|
|
"};\n}",
|
|
)
|
|
|
|
def test_does_not_redeclare_gnome_remote_desktop_user(self):
|
|
self.assertNotIn("users.users.gnome-remote-desktop", self.source)
|
|
self.assertNotIn("createHome = true;", self.source)
|
|
|
|
def test_main_service_requires_setup_before_starting(self):
|
|
self.assertIn('wantedBy = [ "graphical.target" ];', self.gnome_service)
|
|
self.assertIn('after = [ "gnome-remote-desktop-setup.service" ];', self.gnome_service)
|
|
self.assertIn('requires = [ "gnome-remote-desktop-setup.service" ];', self.gnome_service)
|
|
|
|
def test_setup_waits_for_configuration_service_and_bounded_timeout(self):
|
|
self.assertIn('wantedBy = [ "graphical.target" ];', self.setup_service)
|
|
self.assertIn('before = [ "gnome-remote-desktop.service" ];', self.setup_service)
|
|
self.assertIn('"dbus.service"', self.setup_service)
|
|
self.assertIn('"gnome-remote-desktop-configuration.service"', self.setup_service)
|
|
self.assertNotIn("RemainAfterExit", self.setup_service)
|
|
self.assertIn('TimeoutStartSec = "2min";', self.setup_service)
|
|
self.assertIn('timeout --kill-after=5s 10s', self.setup_service)
|
|
self.assertIn('echo "grdctl command timed out: $*" >&2', self.setup_service)
|
|
self.assertIn('echo "grdctl command failed (exit $rc): $*" >&2', self.setup_service)
|
|
|
|
def test_setup_runs_grdctl_directly_as_root(self):
|
|
# The oneshot service already runs as root; system-mode grdctl must be
|
|
# invoked directly so it does not attempt pkexec authorisation.
|
|
self.assertIn('grdctl --system "$@"', self.setup_service)
|
|
self.assertNotIn("runuser", self.setup_service)
|
|
self.assertNotIn("pkexec", self.setup_service)
|
|
self.assertNotIn("sudo", self.setup_service)
|
|
|
|
def test_privilege_escalation_packages_absent_from_setup_path(self):
|
|
self.assertNotIn("pkgs.polkit", self.setup_service)
|
|
self.assertNotIn("pkgs.util-linux", self.setup_service)
|
|
|
|
def test_hub_files_are_the_source_of_truth_for_username_and_password(self):
|
|
self.assertIn('DEFAULT_USERNAME="sovran"', self.setup_service)
|
|
self.assertIn('if [ ! -f "$USERNAME_FILE" ]; then', self.setup_service)
|
|
self.assertIn(USERNAME_READ, self.setup_service)
|
|
self.assertIn(USERNAME_LENGTH_GUARD, self.setup_service)
|
|
self.assertIn('case "$USERNAME" in', self.setup_service)
|
|
self.assertIn('[A-Za-z_][A-Za-z0-9_-]*)', self.setup_service)
|
|
self.assertIn("RDP username is too long (''${#USERNAME} characters, maximum 32)", self.setup_service)
|
|
self.assertIn("RDP username must start with a letter or underscore and contain only letters, numbers, underscores, and hyphens", self.setup_service)
|
|
self.assertIn('if [ ! -f "$PASSWORD_FILE" ]; then', self.setup_service)
|
|
self.assertIn("tr -d '\\n'", self.setup_service)
|
|
self.assertIn('"$PASSWORD_FILE"', self.setup_service)
|
|
self.assertIn(SHORT_PASSWORD_GUARD, self.setup_service)
|
|
self.assertIn("RDP password is too short (''${#PASSWORD} characters, minimum 8)", self.setup_service)
|
|
self.assertIn('grdctl_system rdp set-credentials "$USERNAME" "$PASSWORD"', self.setup_service)
|
|
self.assertNotIn('grdctl --system rdp set-credentials sovran "$PASSWORD"', self.setup_service)
|
|
|
|
def test_secure_permissions_are_enforced_for_state_and_secret_files(self):
|
|
self.assertIn('"d /var/lib/gnome-remote-desktop/tls 0700', self.source)
|
|
self.assertIn("chmod 700", self.setup_service)
|
|
self.assertIn('chmod 600 "$USERNAME_FILE"', self.setup_service)
|
|
self.assertIn('chmod 600 "$PASSWORD_FILE"', self.setup_service)
|
|
self.assertIn('chmod 600 "$CRED_FILE"', self.setup_service)
|
|
self.assertIn('chmod 600 "$TLS_DIR/rdp-tls.key"', self.setup_service)
|
|
self.assertIn('chmod 644 "$TLS_DIR/rdp-tls.crt"', self.setup_service)
|
|
self.assertIn('LOCAL_IP="$(hostname -I | awk \'{print $1}\')"', self.setup_service)
|
|
self.assertIn('LOCAL_IP="127.0.0.1"', self.setup_service)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|