Send a caller identity to flex-auth so policy.enabled can flip
flex-auth's flex-auth-ops-warden pin (FLEX-WP-0016) TokenReviews the caller and binds resource.system: ops-warden to system:serviceaccount:ops-warden:ops-warden. policy.py posted /v1/check with no Authorization header, so the pin logs "caller authentication warning" and can only run callerAuth.mode: warn — which, under ADHOC-2026-08-17-T01, is exactly what blocks policy.enabled: true. - policy.caller_auth (none | file | env | command) + src/warden/caller_identity.py: token resolved per call, never cached, written, or logged (ADR-0002) - both check_sign_policy and check_fetch_policy attach the bearer header; an unobtainable token fails closed rather than retrying anonymously - scripts/check_policy_caller_identity.py: read-only gate, prints length and a truncated fingerprint only, distinguishes 401 (audience/binding) from 403 - example config: caller_auth block, and flex_auth_url corrected — it pointed at flex-auth.flex-auth.svc, a Service that does not exist - WARDEN-WP-0031, PolicyGatedSigning caller-identity section and flip sequence Default stays mode: none, so behaviour is unchanged until an operator opts in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
35aff380a3
commit
0a331413a2
9 changed files with 728 additions and 12 deletions
189
scripts/check_policy_caller_identity.py
Executable file
189
scripts/check_policy_caller_identity.py
Executable file
|
|
@ -0,0 +1,189 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Readiness gate for the flex-auth ``policy.enabled`` flip (WARDEN-WP-0031).
|
||||
|
||||
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`` — and therefore ``policy.enabled: true`` cannot be set — until
|
||||
ops-warden's calling side actually presents a token.
|
||||
|
||||
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", f"loaded; policy.enabled={str(policy.enabled).lower()}")
|
||||
)
|
||||
|
||||
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 url is None and not policy.enabled:
|
||||
checks.append(
|
||||
(
|
||||
"skip",
|
||||
"live /v1/check",
|
||||
f"policy.enabled=false; pass --url to smoke {target} anyway",
|
||||
)
|
||||
)
|
||||
return checks
|
||||
|
||||
import httpx # local import: the offline checks above must not need it
|
||||
|
||||
probe = {
|
||||
"subject": {"id": "readiness", "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": {"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, and do not set policy.enabled: true."
|
||||
)
|
||||
return 1
|
||||
print(
|
||||
"\nREADY — the calling side presents an identity. Next: tell flex-auth to set "
|
||||
"callerAuth.mode: enforce on flex-auth-ops-warden, re-run this check, then set "
|
||||
"policy.enabled: true with fail_closed: true."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue