atlas-iac/testing/tests/test_hermes_agent_security.py

236 lines
8.3 KiB
Python

"""Agent authentication, networking, and Flux health contracts."""
from __future__ import annotations
from testing.tests.test_hermes_cli_support import (
FLUX_HERMES,
HERMES,
KEYCLOAK,
_agent_deployment,
yaml,
)
def test_agent_image_runs_execution_safety_patch_and_regressions():
dockerfiles = HERMES.parents[1] / "dockerfiles"
dockerfile = (dockerfiles / "Dockerfile.hermes-agent").read_text(encoding="utf-8")
dockerignore = (dockerfiles / "Dockerfile.hermes-agent.dockerignore").read_text(
encoding="utf-8"
)
for name in (
"patch-hermes-execution-safety.py",
"hermes-execution-safety-regression.py",
"hermes_execution_patch_support.py",
"patch_hermes_run_safety.py",
"patch_hermes_decomposition_safety.py",
"hermes_execution_regression_support.py",
"hermes_run_safety_regression.py",
"hermes_decomposition_safety_regression.py",
):
assert f"COPY dockerfiles/{name}" in dockerfile
assert f"!dockerfiles/{name}" in dockerignore
assert "/opt/hermes/.venv/bin/python /tmp/patch-hermes-execution-safety.py" in (
dockerfile
)
assert "/opt/hermes/.venv/bin/python /tmp/hermes-execution-safety-regression.py" in (
dockerfile
)
assert "COPY services/hermes/scripts/cli_lane_*.py" in dockerfile
assert "!services/hermes/scripts/cli_lane_*.py" in dockerignore
assert "HERMES_CLI_LANE_SOURCE=/tmp/hermes-lane-regression" in dockerfile
def test_agent_refreshes_routes_after_restoring_cli_logins():
deployment = _agent_deployment()
init_containers = {
item["name"]: item
for item in deployment["spec"]["template"]["spec"]["initContainers"]
}
configure = init_containers["configure-agent-clients"]
command = configure["command"][-1]
assert "configure_agent_clients.py" in command
assert command.index("configure_agent_clients.py") < command.index(
"hermes_coordinator.py --once"
)
env = {item["name"]: item["value"] for item in configure["env"]}
assert env["HERMES_AUTH_FILE"] == "/runtime-access/hermes-auth.json"
assert env["PYTHONPATH"] == "/opt/hermes"
for name in ("bootstrap-coordinator", "configure-agent-clients"):
route_env = {
item["name"]: item["value"]
for item in init_containers[name]["env"]
}
assert route_env["CODEX_HOME"] == "/runtime-access/codex"
assert route_env["CLAUDE_CONFIG_DIR"] == "/runtime-access/claude"
assert "/opt/data/tools/bin" in route_env["PATH"]
assert "patch-web-session-activity" in init_containers
web_patch = init_containers["patch-web-session-activity"]
assert "/opt/coordinator/patch_web_session_activity.py" in web_patch["command"]
containers = {
item["name"]: item
for item in deployment["spec"]["template"]["spec"]["containers"]
}
steward_env = {
item["name"]: item["value"]
for item in containers["model-steward"]["env"]
}
assert steward_env["CODEX_HOME"] == "/runtime-access/codex"
assert steward_env["CLAUDE_CONFIG_DIR"] == "/runtime-access/claude"
assert "/opt/data/tools/bin" in steward_env["PATH"]
hermes_mounts = {
(item["name"], item["mountPath"], item.get("subPath"))
for item in containers["hermes"]["volumeMounts"]
}
assert (
"web-server-patch",
"/opt/hermes/hermes_cli/web_server.py",
"web_server.py",
) in hermes_mounts
def test_flux_health_checks_follow_the_owner_oauth_sidecar():
flux = yaml.safe_load(FLUX_HERMES.read_text())
checks = {
(item["kind"], item["name"])
for item in flux["spec"]["healthChecks"]
}
assert ("Deployment", "hermes-agent") in checks
assert ("DaemonSet", "hermes-node-ssh-access") in checks
assert ("Deployment", "oauth2-proxy-hermes-agent") not in checks
def test_agent_auth_is_bstein_group_and_email_bounded():
deployment = _agent_deployment()
oauth = next(
item for item in deployment["spec"]["template"]["spec"]["containers"]
if item["name"] == "oauth2-proxy"
)
args = oauth["args"]
assert "--user-id-claim=sub" in args
assert "--oidc-groups-claim=groups" in args
assert "--allowed-group=/hermes-owner" in args
assert "--authenticated-emails-file=/etc/oauth2-proxy/allowed-emails" in args
script = (KEYCLOAK / "scripts/hermes_access_oidc_ensure.sh").read_text()
assert 'group_name="hermes-owner"' in script
assert "username=bstein&exact=true" in script
assert '"full.path":"true"' in script
def test_agent_network_boundary_allows_only_authenticated_and_metrics_surfaces():
documents = [
item
for item in yaml.safe_load_all((HERMES / "networkpolicy.yaml").read_text())
if item
]
isolation = next(item for item in documents if item.get("metadata", {}).get("name") == "hermes-agent-isolation")
assert isolation["spec"]["ingress"] == [
{
"from": [
{
"namespaceSelector": {
"matchLabels": {
"kubernetes.io/metadata.name": "traefik"
}
},
"podSelector": {
"matchLabels": {"app.kubernetes.io/name": "traefik"}
},
}
],
"ports": [{"protocol": "TCP", "port": 4180}],
},
{
"from": [
{
"podSelector": {
"matchLabels": {"app": "hermes-chat-tenant"}
}
}
],
"ports": [
{"protocol": "TCP", "port": 9002},
{"protocol": "TCP", "port": 9003},
],
},
{
"from": [
{
"podSelector": {
"matchLabels": {"app": "hermes-switchyard"}
}
}
],
"ports": [
{"protocol": "TCP", "port": 9003},
{"protocol": "TCP", "port": 9006},
],
},
{
"from": [
{
"namespaceSelector": {
"matchLabels": {
"kubernetes.io/metadata.name": "monitoring"
}
},
"podSelector": {"matchLabels": {"app": "server"}},
}
],
"ports": [{"protocol": "TCP", "port": 9010}],
},
]
egress = isolation["spec"]["egress"]
assert {} not in egress
broker = next(
rule
for rule in egress
if rule.get("ports") == [{"protocol": "TCP", "port": 9081}]
)
assert broker["to"] == [
{
"namespaceSelector": {
"matchLabels": {"kubernetes.io/metadata.name": "hermes-scm"}
},
"podSelector": {"matchLabels": {"app": "hermes-scm-broker"}},
}
]
namespace_rule = next(
rule
for rule in egress
if rule.get("to", [{}])[0].get("namespaceSelector", {}).get(
"matchExpressions"
)
)
assert namespace_rule["to"][0]["namespaceSelector"]["matchExpressions"] == [
{
"key": "kubernetes.io/metadata.name",
"operator": "NotIn",
"values": ["gitea", "hermes-scm"],
}
]
public = next(
rule
for rule in egress
if rule.get("to", [{}])[0].get("ipBlock", {}).get("cidr") == "0.0.0.0/0"
)
assert "192.168.0.0/16" in public["to"][0]["ipBlock"]["except"]
def test_owner_agent_has_observer_kubernetes_context_without_cluster_admin():
config = yaml.safe_load((HERMES / "agent-kubeconfig.yaml").read_text())
assert config["current-context"] == "atlas-observer"
assert config["contexts"][0]["context"]["namespace"] == "default"
rbac_path = HERMES / "rbac.yaml"
documents = [item for item in yaml.safe_load_all(rbac_path.read_text()) if item]
bindings = [item for item in documents if item["kind"] == "ClusterRoleBinding"]
assert bindings
assert all(binding["roleRef"]["name"] != "cluster-admin" for binding in bindings)
assert all(
subject["name"] != "hermes-agent"
for binding in bindings
for subject in binding["subjects"]
)