generated from coulomb/repo-seed
feat(WARDEN-WP-0016): ops-bridge cert_command readiness gate + handoff
Close ops-warden's side of the last Partial INTENT criterion (ops-bridge integrates via a stable cert_command). The migration playbook and contract already existed; what was missing was an automated readiness gate before touching tunnel config. T1 — scripts/check_tunnel_cert_readiness.py: read-only preflight that asserts the cert_command path is ready without signing — config/backend, actor inventory + TTL within type max, pubkey exists/parses/not-private, principals present, and optional host-principal deployment (mirrors check_principals_drift). Exit 0/1/2. T2 — opt-in --sign-smoke: runs the cert_command against the local backend and validates identity/principals/TTL of the emitted cert; refuses a vault backend. Window measured from the cert's own valid_from->valid_before so it's timezone-robust (fixes a CEST off-by-2h artifact). integration-marked test + a vault-refusal unit test. T3 — playbook now leads with Step 0 readiness gate; ops-bridge handoff message sent. T4 — SCOPE INTENT row: Partial -> Pilot-ready; known-gaps + SSH-lane list updated. 9 unit + 1 integration test, 209 default passing, lint clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
128
tests/test_tunnel_cert_readiness.py
Normal file
128
tests/test_tunnel_cert_readiness.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""Tests for the ops-bridge cert_command readiness gate (WARDEN-WP-0016 T1/T2)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from warden.config import WardenConfig
|
||||
|
||||
_SCRIPT = Path(__file__).resolve().parent.parent / "scripts" / "check_tunnel_cert_readiness.py"
|
||||
_spec = importlib.util.spec_from_file_location("check_tunnel_cert_readiness", _SCRIPT)
|
||||
readiness = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(readiness)
|
||||
|
||||
PUBKEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFakeKeyMaterialForTestsOnly comment\n"
|
||||
|
||||
|
||||
def _status(checks, label):
|
||||
return next(s for s, lab, _ in checks if lab == label)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def setup(tmp_path):
|
||||
inv = tmp_path / "inventory.yaml"
|
||||
inv.write_text(
|
||||
"actors:\n"
|
||||
" agt-state-hub-bridge:\n"
|
||||
" type: agt\n"
|
||||
" principals: [agt-task-bridge]\n"
|
||||
" ttl_hours: 24\n"
|
||||
)
|
||||
pub = tmp_path / "agt.pub"
|
||||
pub.write_text(PUBKEY)
|
||||
cfg = WardenConfig(
|
||||
backend="local",
|
||||
ca_key=tmp_path / "ca",
|
||||
inventory_path=inv,
|
||||
state_dir=tmp_path / "state",
|
||||
)
|
||||
return cfg, pub, tmp_path
|
||||
|
||||
|
||||
def test_all_ready(setup):
|
||||
cfg, pub, _ = setup
|
||||
checks = readiness.run_checks(cfg, "agt-state-hub-bridge", pub, None)
|
||||
assert _status(checks, "inventory") == "ok"
|
||||
assert _status(checks, "public key") == "ok"
|
||||
assert _status(checks, "principals") == "ok"
|
||||
assert _status(checks, "infra principals") == "skip" # no --infra
|
||||
|
||||
|
||||
def test_unknown_actor_fails(setup):
|
||||
cfg, pub, _ = setup
|
||||
checks = readiness.run_checks(cfg, "agt-ghost", pub, None)
|
||||
assert _status(checks, "inventory") == "fail"
|
||||
|
||||
|
||||
def test_missing_pubkey_fails(setup):
|
||||
cfg, _, tmp = setup
|
||||
checks = readiness.run_checks(cfg, "agt-state-hub-bridge", tmp / "nope.pub", None)
|
||||
assert _status(checks, "public key") == "fail"
|
||||
|
||||
|
||||
def test_private_key_rejected(setup):
|
||||
cfg, _, tmp = setup
|
||||
priv = tmp / "id.pub"
|
||||
priv.write_text("-----BEGIN OPENSSH PRIVATE KEY-----\nxxx\n-----END OPENSSH PRIVATE KEY-----\n")
|
||||
checks = readiness.run_checks(cfg, "agt-state-hub-bridge", priv, None)
|
||||
assert _status(checks, "public key") == "fail"
|
||||
|
||||
|
||||
def test_infra_principal_missing(setup):
|
||||
cfg, pub, tmp = setup
|
||||
infra = tmp / "ssh_principals.yaml"
|
||||
infra.write_text("ssh_principals:\n host1:\n users:\n agt: [some-other-principal]\n")
|
||||
checks = readiness.run_checks(cfg, "agt-state-hub-bridge", pub, infra)
|
||||
assert _status(checks, "infra principals") == "fail"
|
||||
|
||||
|
||||
def test_infra_principal_present(setup):
|
||||
cfg, pub, tmp = setup
|
||||
infra = tmp / "ssh_principals.yaml"
|
||||
infra.write_text("ssh_principals:\n host1:\n users:\n agt: [agt-task-bridge]\n")
|
||||
checks = readiness.run_checks(cfg, "agt-state-hub-bridge", pub, infra)
|
||||
assert _status(checks, "infra principals") == "ok"
|
||||
|
||||
|
||||
def test_ttl_over_max_fails(tmp_path):
|
||||
inv = tmp_path / "inventory.yaml"
|
||||
# agt max TTL is 24h; load_inventory clamps? No — it preserves; the check flags it.
|
||||
inv.write_text("actors:\n agt-x:\n type: agt\n principals: [p]\n ttl_hours: 999\n")
|
||||
pub = tmp_path / "k.pub"
|
||||
pub.write_text(PUBKEY)
|
||||
cfg = WardenConfig(backend="local", ca_key=tmp_path / "ca", inventory_path=inv, state_dir=tmp_path)
|
||||
checks = readiness.run_checks(cfg, "agt-x", pub, None)
|
||||
assert _status(checks, "inventory") == "fail"
|
||||
|
||||
|
||||
def test_build_cert_command():
|
||||
cmd = readiness.build_cert_command("agt-state-hub-bridge", Path("/k.pub"))
|
||||
assert cmd == "warden sign agt-state-hub-bridge --pubkey /k.pub"
|
||||
|
||||
|
||||
def test_sign_smoke_rejects_vault_backend(tmp_path):
|
||||
cfg = WardenConfig(backend="vault", inventory_path=tmp_path / "i.yaml", state_dir=tmp_path)
|
||||
with pytest.raises(ValueError, match="local backend"):
|
||||
readiness.sign_smoke(cfg, "agt-x", tmp_path / "k.pub")
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_sign_smoke_validates_real_cert(setup):
|
||||
"""Opt-in: requires ssh-keygen. Issues a real local cert and validates it."""
|
||||
if shutil.which("ssh-keygen") is None:
|
||||
pytest.skip("ssh-keygen not available")
|
||||
cfg, _, tmp = setup
|
||||
# Generate a real CA key and a real actor pubkey.
|
||||
ca = tmp / "ca"
|
||||
subprocess.run(["ssh-keygen", "-t", "ed25519", "-f", str(ca), "-N", "", "-q"], check=True)
|
||||
actor_key = tmp / "actor"
|
||||
subprocess.run(["ssh-keygen", "-t", "ed25519", "-f", str(actor_key), "-N", "", "-q"], check=True)
|
||||
checks = readiness.sign_smoke(cfg, "agt-state-hub-bridge", actor_key.with_suffix(".pub"))
|
||||
statuses = {lab: s for s, lab, _ in checks}
|
||||
assert statuses.get("cert identity") == "ok"
|
||||
assert statuses.get("cert principals") == "ok"
|
||||
assert statuses.get("cert validity") == "ok"
|
||||
Reference in New Issue
Block a user