Files
apex.ermnvldmr.com/tests/test_manual_host_setup.py
T

203 lines
11 KiB
Python

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)