import json import os from pathlib import Path import subprocess import tempfile from types import SimpleNamespace import unittest from unittest.mock import Mock, patch from engine import overlay def action(name): return overlay.load_module(str(Path(__file__).resolve().parents[1] / f"actions/{name}.py"), "test_manual_" + name.replace("/", "_")) class HostSetup(unittest.TestCase): def test_packages_preserve_pve_firewall(self): packages = action("config/packages") ctx = SimpleNamespace(sys=Mock()) with (patch.object(Path, "is_file", return_value=True), patch.object(Path, "exists", return_value=True), patch.object(packages, "_install_missing") as install): packages.run(ctx, None) requested = install.call_args.args[1] self.assertIn("sudo", requested) self.assertNotIn("ufw", requested) def test_failed_effective_policy_restores_original_configuration(self): hardening = action("config/hardening") system, host = Mock(), Mock() system.sudo.return_value.stdout = "Port 22\n" with (patch.object(hardening, "_operator_access", return_value="verified"), patch.object(hardening, "_effective", return_value={"authenticationmethods": "any"})): with self.assertRaisesRegex(RuntimeError, "effective SSH"): hardening.run(SimpleNamespace(sys=system, host=host), None) writes = host.write_file.call_args_list self.assertTrue(writes[0].args[1].startswith("Port 22\n")) self.assertEqual(writes[-1].args[1], "Port 22\n") system.reload.assert_called_once_with("ssh") def test_current_endpoint_must_have_authenticated_with_a_key(self): hardening = action("config/hardening") ctx = SimpleNamespace(sys=Mock()) sockets = SimpleNamespace(stdout='0 0 192.0.2.9:22 192.0.2.8:3002 users:(("sshd",pid=60,fd=4))') accepted = {"_PID": "60", "__MONOTONIC_TIMESTAMP": "100", "MESSAGE": "Accepted publickey for adam from 192.0.2.8 port 3002 ssh2: ED25519 SHA256:example"} with (patch.dict(os.environ, {"SSH_CONNECTION": "192.0.2.100 4321 192.0.2.9 1234", "SSH_USER_AUTH": "/arbitrary/file"}), patch.object(hardening, "_ancestors", return_value={60: 50}), patch.object(hardening.pwd, "getpwuid", return_value=SimpleNamespace(pw_name="adam"))): for changes in ({"_PID": "42"}, {"__MONOTONIC_TIMESTAMP": "49"}, {"MESSAGE": accepted["MESSAGE"].replace("3002", "4002")}): with self.subTest(changes=changes): ctx.sys.sudo.side_effect = [sockets, SimpleNamespace( returncode=0, stdout=json.dumps({**accepted, **changes}))] with self.assertRaisesRegex(RuntimeError, "Cannot verify"): hardening._connection(ctx) ctx.sys.sudo.side_effect = [sockets, SimpleNamespace(returncode=0, stdout=json.dumps(accepted))] self.assertEqual(hardening._connection(ctx), "user=adam,host=192.0.2.8,addr=192.0.2.8,laddr=192.0.2.9,lport=22") def test_journald_restart_failure_restores_previous_config(self): base = action("config/base") with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) target = root / "99-apex.conf" legacy = root / "maxuse.conf" target.write_text("previous\n") legacy.write_text("legacy\n") backup = root / "legacy-backup" host = Mock() host.backup.side_effect = lambda path, **kwargs: (backup.write_bytes(Path(path).read_bytes()) and str(backup)) host.write_file.side_effect = lambda path, content, **kwargs: Path(path).write_text(content) def sudo(command): if command[0] == "systemctl": raise RuntimeError("restart failed") if command[0] == "cp": Path(command[-1]).write_bytes(Path(command[-2]).read_bytes()) else: Path(command[-1]).unlink() ctx = SimpleNamespace(sys=SimpleNamespace(sudo=sudo), host=host) with self.assertRaisesRegex(RuntimeError, "restart failed"): base._configure(ctx, target, legacy, "updated\n", restart=True) self.assertEqual(target.read_text(), "previous\n") self.assertEqual(legacy.read_text(), "legacy\n") def test_unverified_session_makes_no_host_changes(self): hardening = action("config/hardening") ctx = SimpleNamespace(sys=Mock(), host=Mock()) with (patch.object(hardening.pwd, "getpwnam", return_value=SimpleNamespace( pw_uid=1001, pw_dir="/home/adam")), patch.object(hardening.pwd, "getpwuid", return_value=SimpleNamespace(pw_name="root")), patch.object(Path, "read_text", return_value="1001")): with self.assertRaisesRegex(RuntimeError, "independent administrator"): hardening.run(ctx, None) self.assertFalse(ctx.host.mock_calls) self.assertFalse(ctx.sys.mock_calls) def test_independent_administrator_can_harden_only_with_an_operator_key(self): hardening = action("config/hardening") account = SimpleNamespace(pw_uid=1001, pw_dir="/home/adam") system = Mock() def privileged(command, **kwargs): output = "" if "stat" in command: output = "1001 600 regular file" if command[-1].endswith("authorized_keys") else "1001 700 directory" elif "cat" in command: output = "ssh-ed25519 fixture" elif "-T" in command: output = "authorizedkeysfile .ssh/authorized_keys" return SimpleNamespace(stdout=output, returncode=0) system.sudo.side_effect = privileged ctx = SimpleNamespace(sys=system) with (patch.object(hardening.pwd, "getpwnam", return_value=account), patch.object(hardening.pwd, "getpwuid", return_value=SimpleNamespace(pw_name="root")), patch.object(hardening.os, "getuid", return_value=0), patch.object(Path, "read_text", return_value="0")): system.run.return_value.returncode = 1 with self.assertRaisesRegex(RuntimeError, "valid unrestricted operator public key"): hardening._operator_access(ctx) system.run.return_value.returncode = 0 self.assertEqual(hardening._operator_access(ctx), "user=adam,host=localhost,addr=127.0.0.1") def test_chown_limits_changes_to_checkout_and_skips_mounts_and_hardlinks(self): chown = action("repository/chown") with tempfile.TemporaryDirectory() as temporary: root = Path(temporary).resolve() for name in (".git", "configs", "volumes"): (root / name).mkdir() for name in ("configs/node", ".git/HEAD", "private", "volumes/data"): (root / name).write_text("fixture") os.link(root / "private", root / "configs/key") (root / "configs/other").symlink_to(root / "volumes", target_is_directory=True) with patch("os.chown") as ownership, patch("os.fchown"): chown._repair(str(root), {"configs/node", "configs/key", "configs/other", "volumes/data"}, {str(root / "volumes")}, os.getuid(), os.getgid()) self.assertEqual({call.args[0] for call in ownership.call_args_list}, {"HEAD", "node", "other"}) self.assertTrue(all(not call.kwargs["follow_symlinks"] for call in ownership.call_args_list)) def test_operator_provisioning_creates_account_then_preserves_keys_on_rerun(self): adam = action("config/adam") with tempfile.TemporaryDirectory() as temporary: root = Path(temporary).resolve() home = root / "adam" account = SimpleNamespace(pw_dir=str(home), pw_uid=1001) key = root / "operator" subprocess.run(["ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", str(key)], check=True) public = key.with_suffix(".pub") ctx = SimpleNamespace(sys=Mock(), host=Mock()) ctx.sys.run.side_effect = lambda cmd, **kw: subprocess.run(cmd, input=kw["input"], text=True, capture_output=True, check=True) def privileged(command, **kwargs): if command[0] == "useradd": home.mkdir() if command[:6] == ["runuser", "-u", "adam", "--", "sh", "-ec"]: subprocess.run(command[4:], text=True, input=kwargs["input"], check=True) ctx.sys.sudo.side_effect = privileged actual_stat = Path.stat def metadata(path, *args, **kwargs): stat = actual_stat(path, *args, **kwargs) if path == home: fields = list(stat) fields[4] = account.pw_uid return os.stat_result(fields) return stat with (patch.object(adam, "HOME", home), patch.object(Path, "stat", metadata), patch.object(adam.grp, "getgrnam", side_effect=KeyError), patch.object(adam.pwd, "getpwnam", side_effect=[KeyError, account, account])): args = SimpleNamespace(public_key_file=str(public)) adam.run(ctx, args) authorized = home / ".ssh/authorized_keys" with authorized.open("a") as stream: stream.write("existing operator entry\n") adam.run(ctx, args) self.assertEqual(authorized.read_text().splitlines().count(public.read_text().strip()), 1) self.assertIn("existing operator entry", authorized.read_text()) commands = [call.args[0] for call in ctx.sys.sudo.call_args_list] self.assertEqual(sum(cmd[0] == "useradd" for cmd in commands), 1) self.assertIn(["runuser", "-u", "adam", "--", "sudo", "-n", "true"], commands) def test_existing_system_account_cannot_be_promoted(self): adam = action("config/adam") ctx = SimpleNamespace(sys=Mock()) account = SimpleNamespace(pw_dir="/home/adam", pw_uid=999) with patch.object(adam.pwd, "getpwnam", return_value=account): with self.assertRaisesRegex(RuntimeError, "incompatible UID"): adam.run(ctx, SimpleNamespace(public_key_file=None)) self.assertFalse(ctx.sys.mock_calls) def test_invalid_operator_key_is_rejected_before_provisioning(self): adam = action("config/adam") with tempfile.NamedTemporaryFile(mode="w") as key: key.write("private key fixture") key.flush() ctx = SimpleNamespace(sys=Mock()) with self.assertRaisesRegex(ValueError, "OpenSSH public key"): adam.run(ctx, SimpleNamespace(public_key_file=key.name)) self.assertFalse(ctx.sys.mock_calls)