ops-warden/scripts/check_policy_caller_identity.py
tegwick e24d2d5bd0
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
docs: record live zone config migration
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0291a-1e87-7151-9934-fcbfe3f65eb1
2026-08-22 15:50:42 +02:00

201 lines
6.8 KiB
Python
Executable file

#!/usr/bin/env python3
"""Readiness gate for the zone-aware flex-auth caller identity.
flex-auth deployed ``flex-auth-ops-warden`` (FLEX-WP-0016) in ``callerAuth.mode:
warn``: it authenticates the caller with a Kubernetes TokenReview and binds
``resource.system: ops-warden`` to ``system:serviceaccount:ops-warden:ops-warden``,
but a caller that sends no ``Authorization`` header only produces a
``caller authentication warning`` and is still served. That pin cannot move to
``enforce`` until ops-warden's calling side actually presents a token. The
former repo-wide ``policy.enabled`` switch is retired by WARDEN-WP-0032.
This script asserts the calling side *without* flipping anything:
* warden.yaml loads and ``policy.caller_auth.mode`` is not ``none``,
* a caller token can actually be obtained (file / env / command),
* (optional, ``--url``) a live ``/v1/check`` against the warn pin returns a
decision **and** the response is reached with the header attached.
Exit 0 = ready to ask flex-auth to enforce, 1 = not ready, 2 = bad input.
The token is never printed, logged, or written anywhere — only its length and a
truncated SHA-256 fingerprint, which are safe to paste into a handoff message.
Usage:
python scripts/check_policy_caller_identity.py [--config ~/.config/warden/warden.yaml]
python scripts/check_policy_caller_identity.py --url http://127.0.0.1:19090
"""
from __future__ import annotations
import argparse
import hashlib
import sys
from pathlib import Path
from typing import List, Optional, Tuple
_SRC = Path(__file__).resolve().parent.parent / "src"
if _SRC.is_dir() and str(_SRC) not in sys.path:
sys.path.insert(0, str(_SRC))
from warden.caller_identity import ( # noqa: E402
CallerIdentityError,
resolve_caller_token,
)
from warden.config import ConfigError, load_config # noqa: E402
Check = Tuple[str, str, str]
def _fingerprint(token: str) -> str:
return "sha256:" + hashlib.sha256(token.encode()).hexdigest()[:12]
def run_checks(config_path: Optional[Path], url: Optional[str]) -> List[Check]:
checks: List[Check] = []
try:
cfg = load_config(config_path)
except ConfigError as e:
return [("fail", "warden.yaml", str(e))]
policy = cfg.policy
checks.append(("ok", "warden.yaml", "loaded; security-zones_v0.1 profile"))
mode = policy.caller_auth.mode
if mode == "none":
checks.append(
(
"fail",
"caller_auth.mode",
"none — no Authorization header is sent; the flex-auth pin stays in warn",
)
)
return checks
checks.append(("ok", "caller_auth.mode", mode))
try:
token = resolve_caller_token(policy.caller_auth)
except CallerIdentityError as e:
checks.append(("fail", "caller token", str(e)))
return checks
assert token is not None
checks.append(
("ok", "caller token", f"obtained, {len(token)} chars, {_fingerprint(token)}")
)
target = url or policy.flex_auth_url
if target is None:
checks.append(
(
"skip",
"live /v1/check",
"policy.flex_auth_url is absent; pass --url to run the live smoke",
)
)
return checks
import httpx # local import: the offline checks above must not need it
probe = {
"subject": {
"id": "agt-state-hub-bridge",
"type": "agt",
"tenant": policy.tenant,
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/agt-state-hub-bridge",
"type": "ssh-certificate",
"system": policy.system,
"tenant": policy.tenant,
},
"context": {
# A structurally complete context, so a deny means the policy said
# no — not that the probe was malformed. What is under test here is
# the caller identity, and that is answered by the HTTP status.
"actor_name": "agt-state-hub-bridge",
"actor_type": "agt",
"principals": ["agt-task-bridge"],
"ttl_hours": 24,
"pubkey_fingerprint": "sha256:" + "0" * 64,
"readiness_probe": True,
},
}
try:
response = httpx.post(
target.rstrip("/") + "/v1/check",
json=probe,
headers={"Authorization": f"Bearer {token}"},
timeout=10.0,
)
except httpx.RequestError as e:
checks.append(("fail", "live /v1/check", f"unreachable at {target}: {e}"))
return checks
if response.status_code == 401:
checks.append(
(
"fail",
"live /v1/check",
"401 — the token was sent but flex-auth did not accept it "
"(check the TokenReview audience and the ServiceAccount binding)",
)
)
elif response.status_code == 403:
checks.append(
(
"fail",
"live /v1/check",
f"403 — authenticated, but the principal may not represent "
f"system {policy.system!r}",
)
)
elif response.status_code >= 400:
checks.append(
("fail", "live /v1/check", f"HTTP {response.status_code} from {target}")
)
else:
try:
decision = response.json()
except ValueError:
checks.append(("fail", "live /v1/check", "non-JSON decision"))
return checks
effect = str(decision.get("effect", "?"))
decision_id = decision.get("id") or decision.get("request_id") or "?"
checks.append(
("ok", "live /v1/check", f"HTTP 200, effect={effect}, decision={decision_id}")
)
return checks
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--config", type=Path, default=None, help="path to warden.yaml")
parser.add_argument(
"--url",
default=None,
help="flex-auth base URL to smoke (e.g. a port-forward of the warn pin)",
)
args = parser.parse_args()
checks = run_checks(args.config, args.url)
glyph = {"ok": "", "fail": "", "skip": "·"}
print("flex-auth caller-identity readiness\n")
for status, label, detail in checks:
print(f" {glyph[status]} {label}: {detail}")
failed = [c for c in checks if c[0] == "fail"]
if failed:
print(
f"\nNOT READY — {len(failed)} check(s) failed. "
"Do not ask flex-auth to enforce caller authentication."
)
return 1
print(
"\nREADY — the calling side presents an identity. Verify "
"callerAuth.mode remains enforce on flex-auth-ops-warden after rollout. "
"Zone-specific PEP failure modes replace the retired global switches."
)
return 0
if __name__ == "__main__":
raise SystemExit(main())