net-kingdom/bounded-executor/synthetic_journey_only.py
tegwick abed4360d4 NK-WP-0056-T10: journey-only executor mode; fail-closed proof on the T08 stack
cli.py --journey-only: read-only, no admission; journey-read role only
(CCR-2026-0059), no Kubernetes/directory client, no approval or audit call;
exit 0 pass / 1 fail closed / 4 refused. Shared operations.journey_from_kv
with the admission check. Synthetic proof: healthy pass, broken svc-authelia
bind fails closed at authelia_first_factor, restored pass, journey token
cannot read the bind; all_met, no values. 45 executor tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 5502@bnt-lap001
Assistant-Session: 68b3b02f-5d0f-4e71-8421-796f62e50196
2026-10-31 19:56:52 +01:00

105 lines
4.6 KiB
Python

#!/usr/bin/env python3
# /// script
# dependencies = ["PyYAML>=6,<7", "jsonschema>=4,<5"]
# ///
"""Journey-only run on the T08 synthetic stack: passes when healthy, fails closed on a broken bind (NK-WP-0056-T10).
The same code path the Job runs with `cli.py --journey-only` (`operations.journey_from_kv`). The difference is the
login: here a dev-OpenBao token that holds only the journey-read policy, instead of Kubernetes auth. Scenarios:
healthy the journey passes
broken bind svc-authelia's password is changed in LLDAP while Authelia keeps the old one (a half-done
rotation). The journey must fail closed at authelia_first_factor.
restored the password is set back and the journey passes again
lane boundary the journey token cannot read the svc-authelia bind
Run: uv run bounded-executor/synthetic_journey_only.py [--report FILE]. Exit 0 when every expectation is met.
"""
from __future__ import annotations
import argparse
import json
import pathlib
import secrets
import sys
import tempfile
HERE = pathlib.Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
import synthetic_pilot as P # noqa: E402 (loads executor, operations and the T08 stack)
OPS, stack_mod = P.OPS, P.stack_mod
def summary(receipt: dict) -> dict:
return {k: receipt.get(k) for k in ("result", "failed_step", "reason")}
def main(argv=None) -> int:
parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
parser.add_argument("--report")
args = parser.parse_args(argv)
report = {"schema": "netkingdom.bounded-executor.synthetic-journey-only/v1",
"started_at": OPS.now_utc().isoformat(), "images": {"openbao": P.OPENBAO_IMAGE},
"code_path": "operations.journey_from_kv (cli.py --journey-only)", "results": []}
ok = False
with tempfile.TemporaryDirectory(prefix="nk56-journey-only-") as tmp:
stack = stack_mod.Stack(pathlib.Path(tmp), pi_cache_timeout=120)
bao = None
secrets_in_report = []
try:
stack.up()
bao = P.OpenBaoDev(stack.network, stack.tag)
write_token, journey_token = bao.setup(stack.pw["svc-authelia"], stack.pw[stack_mod.USER], stack.seed)
secrets_in_report = [stack.pw["svc-authelia"], stack.pw[stack_mod.USER], write_token, journey_token, bao.root]
journey_bao = OPS.OpenBao(bao.addr, token=journey_token)
state: dict = {}
def run():
return OPS.journey_from_kv(journey_bao, P.JOURNEY_KV, stack.journey_config(), state)
healthy = run()
report["results"].append({"scenario": "healthy", "expected": "pass", "journey": summary(healthy),
"met": healthy["result"] == "pass"})
stack.set_password(stack.lldap_token(), "svc-authelia", secrets.token_urlsafe(24))
broken = run()
report["results"].append({"scenario": "broken bind (svc-authelia)",
"expected": "fail at authelia_first_factor", "journey": summary(broken),
"met": broken["result"] == "fail"
and broken["failed_step"] == "authelia_first_factor"})
stack.set_password(stack.lldap_token(), "svc-authelia", stack.pw["svc-authelia"])
restored = run()
report["results"].append({"scenario": "restored", "expected": "pass", "journey": summary(restored),
"met": restored["result"] == "pass"})
try:
journey_bao.read(P.BIND_KV)
denied = False
except OPS.OperationError:
denied = True
report["results"].append({"scenario": "lane boundary", "expected": "journey token cannot read the bind",
"met": denied})
journey_bao.revoke_self()
ok = all(r["met"] for r in report["results"])
except Exception as error: # report the type only: messages could carry context
report["error"] = type(error).__name__
finally:
if bao:
bao.remove()
stack.down()
text = json.dumps(report, sort_keys=True)
report["no_values_in_report"] = not any(s and s in text for s in secrets_in_report)
report["all_met"] = ok and report["no_values_in_report"]
report["finished_at"] = OPS.now_utc().isoformat()
out = json.dumps(report, indent=2, sort_keys=True)
if args.report:
pathlib.Path(args.report).write_text(out + "\n")
print(out)
return 0 if report["all_met"] else 1
if __name__ == "__main__":
sys.exit(main())