Three fenced worker Pods claim Hermes Kanban runs through a coordinator that owns every state transition, with per-ordinal HMAC authority, a mediated broker-only SCM path, and durable per-ordinal workspaces. Content is the reviewed head of PR #18 (689bcb6e) with PR 16's and PR 19's contributions removed: they were merged in only to validate co-existence and are not prerequisites, so this branch no longer carries them as ancestors. Only PR 14 and PR 15 remain, because the broker boundary and the cli_lane_* decomposition are load-bearing for two of the fixed P0 boundaries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
201 lines
6.8 KiB
Python
201 lines
6.8 KiB
Python
"""Host privilege-policy audits for the Hermes node SSH identity."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from testing.tests.test_hermes_node_account_support import _fixture
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"database,record",
|
|
[
|
|
("group", "disk:x:6:atlas,hermes-agent\n"),
|
|
("gshadow", "disk:!:hermes-agent:atlas\n"),
|
|
("gshadow", "disk:!::atlas,hermes-agent\n"),
|
|
],
|
|
)
|
|
def test_unexpected_group_or_gshadow_membership_fails_before_writes(
|
|
tmp_path: Path, monkeypatch, database: str, record: str
|
|
):
|
|
module, originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
|
path = module.HOST_ETC / database
|
|
path.write_text(
|
|
"\n".join(
|
|
line
|
|
for line in originals[database].splitlines()
|
|
if not line.startswith("disk:")
|
|
)
|
|
+ "\n"
|
|
+ record,
|
|
encoding="utf-8",
|
|
)
|
|
before = {name: (module.HOST_ETC / name).read_bytes() for name in originals}
|
|
|
|
with pytest.raises(module.HardeningError, match="group access"):
|
|
module.reconcile(public_key)
|
|
|
|
assert {name: (module.HOST_ETC / name).read_bytes() for name in originals} == before
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"relative,value",
|
|
[
|
|
("sudoers.d/hermes", "hermes-agent ALL=(ALL:ALL) NOPASSWD: ALL\n"),
|
|
("sudoers", "ALL ALL=(ALL:ALL) ALL\n"),
|
|
(
|
|
"polkit-1/rules.d/90-root.rules",
|
|
'if (subject.user == "hermes-agent") return polkit.Result.YES;\n',
|
|
),
|
|
(
|
|
"polkit-1/rules.d/90-root.rules",
|
|
"unix-user:* org.freedesktop.policykit.exec polkit.Result.YES\n",
|
|
),
|
|
],
|
|
)
|
|
def test_sudo_and_root_equivalent_polkit_authority_fail_closed(
|
|
tmp_path: Path, monkeypatch, relative: str, value: str
|
|
):
|
|
module, originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
|
policy = module.HOST_ETC / relative
|
|
policy.parent.mkdir(parents=True, exist_ok=True)
|
|
policy.write_text(value, encoding="utf-8")
|
|
|
|
with pytest.raises(module.HardeningError, match="sudo|polkit"):
|
|
module.reconcile(public_key)
|
|
|
|
assert all(
|
|
(module.HOST_ETC / name).read_text() == original
|
|
for name, original in originals.items()
|
|
)
|
|
|
|
|
|
def test_external_group_authority_source_is_rejected(tmp_path: Path, monkeypatch):
|
|
module, originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
|
(module.HOST_ETC / "nsswitch.conf").write_text("passwd: files\ngroup: files ldap\n")
|
|
with pytest.raises(module.HardeningError, match="external group"):
|
|
module.reconcile(public_key)
|
|
assert all(
|
|
(module.HOST_ETC / name).read_text() == original
|
|
for name, original in originals.items()
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("database", ["passwd", "group", "initgroups", "shadow"])
|
|
def test_external_identity_authority_sources_fail_closed(
|
|
tmp_path: Path, monkeypatch, database: str
|
|
):
|
|
module, originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
|
(module.HOST_ETC / "nsswitch.conf").write_text(
|
|
f"passwd: files\n{database}: files ldap\n", encoding="utf-8"
|
|
)
|
|
with pytest.raises(module.HardeningError, match="external group/account"):
|
|
module.reconcile(public_key)
|
|
assert all(
|
|
(module.HOST_ETC / name).read_text() == original
|
|
for name, original in originals.items()
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"value",
|
|
[
|
|
"#1200 ALL=(ALL:ALL) NOPASSWD: ALL\n",
|
|
"User_Alias HERMES_IDS = #1200\nHERMES_IDS ALL=(ALL:ALL) NOPASSWD: ALL\n",
|
|
"User_Alias HERMES_GROUP = %#1200\nHERMES_GROUP ALL=(ALL:ALL) NOPASSWD: ALL\n",
|
|
],
|
|
)
|
|
def test_visudo_valid_numeric_and_alias_grants_fail_closed(
|
|
tmp_path: Path, monkeypatch, value: str
|
|
):
|
|
module, originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
|
monkeypatch.setattr(module, "ACCOUNT_UID", 1200)
|
|
policy = module.HOST_ETC / "sudoers"
|
|
policy.write_text(value, encoding="utf-8")
|
|
visudo = shutil.which("visudo")
|
|
if visudo is None:
|
|
pytest.skip("visudo is not installed in this test environment")
|
|
validation = subprocess.run(
|
|
[visudo, "-c", "-f", str(policy)],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
assert validation.returncode == 0, validation.stderr
|
|
|
|
with pytest.raises(module.HardeningError, match="sudo authority"):
|
|
module.reconcile(public_key)
|
|
|
|
assert all(
|
|
(module.HOST_ETC / name).read_text() == original
|
|
for name, original in originals.items()
|
|
)
|
|
|
|
|
|
def test_sudo_include_is_bounded_to_the_audited_standard_directory(
|
|
tmp_path: Path, monkeypatch
|
|
):
|
|
module, originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
|
sudoers = module.HOST_ETC / "sudoers"
|
|
sudoers.write_text("#includedir /opt/external-sudoers\n", encoding="utf-8")
|
|
with pytest.raises(module.HardeningError, match="unaudited authority source"):
|
|
module.reconcile(public_key)
|
|
|
|
sudoers.write_text("#includedir /etc/sudoers.d\n", encoding="utf-8")
|
|
module.reconcile(public_key)
|
|
assert all(
|
|
(module.HOST_ETC / name).read_text().startswith(original)
|
|
for name, original in originals.items()
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"relative,value",
|
|
[
|
|
(
|
|
"polkit-1/rules.d/90-hermes.rules",
|
|
"polkit.addRule(function(action, subject) {\n"
|
|
" if (subject.uid == 1200) return polkit.Result.YES;\n"
|
|
"});\n",
|
|
),
|
|
(
|
|
"polkit-1/localauthority/50-local.d/hermes.pkla",
|
|
"[Hermes]\nIdentity=unix-user:1200\n"
|
|
"Action=org.freedesktop.policykit.exec\nResultActive=yes\n",
|
|
),
|
|
],
|
|
)
|
|
def test_numeric_polkit_grants_fail_closed(
|
|
tmp_path: Path, monkeypatch, relative: str, value: str
|
|
):
|
|
module, originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
|
monkeypatch.setattr(module, "ACCOUNT_UID", 1200)
|
|
policy = module.HOST_ETC / relative
|
|
policy.parent.mkdir(parents=True, exist_ok=True)
|
|
policy.write_text(value, encoding="utf-8")
|
|
|
|
with pytest.raises(module.HardeningError, match="polkit authority"):
|
|
module.reconcile(public_key)
|
|
|
|
assert all(
|
|
(module.HOST_ETC / name).read_text() == original
|
|
for name, original in originals.items()
|
|
)
|
|
|
|
|
|
def test_writable_privilege_policy_is_rejected(tmp_path: Path, monkeypatch):
|
|
module, originals, _key_value, _other, public_key = _fixture(tmp_path, monkeypatch)
|
|
sudoers = module.HOST_ETC / "sudoers"
|
|
sudoers.write_text("root ALL=(ALL:ALL) ALL\n")
|
|
sudoers.chmod(0o666)
|
|
with pytest.raises(module.HardeningError, match="unsafe mutation"):
|
|
module.reconcile(public_key)
|
|
assert all(
|
|
(module.HOST_ETC / name).read_text() == original
|
|
for name, original in originals.items()
|
|
)
|