Split host setup into explicit entity actions

This commit is contained in:
deytenit committed 2026-11-08 12:39:49 +03:00
1 parent a297e7c71e
commit ba9ef92051
51 files changed
+1023 -275

No files matched your search

+202
View File
@@ -0,0 +1,202 @@
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)