NK-WP-0056-T10: one OpenBao login per lane; renew before the 15m lease ends
The executor logs in separately for the bind write (CCR-2026-0058) and the journey read (CCR-2026-0059) and refuses one role for both; every login is revoked at exit, also when setup fails. Clients log in again (re-reading the rotating projected JWT) before the lease ends, since a run's budget (20m) outlasts a 15m token. Synthetic pilot with per-lane tokens: completed and reverted, all_met. 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
This commit is contained in:
parent
74599a583d
commit
1cdc18f75f
6 changed files with 409 additions and 25 deletions
159
bounded-executor/tests/test_openbao_lanes.py
Normal file
159
bounded-executor/tests/test_openbao_lanes.py
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
"""One OpenBao login per lane, renewal before the lease ends, revocation on every path (NK-WP-0056-T10). Fakes only."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
HERE = pathlib.Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def load(name):
|
||||
spec = importlib.util.spec_from_file_location(name, HERE.parent / f"{name}.py")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
CLI = load("cli")
|
||||
OPS = sys.modules["operations"]
|
||||
|
||||
|
||||
class Bao:
|
||||
def __init__(self, lease=900):
|
||||
self.lease = lease
|
||||
self.logins, self.revoked, self.reads = [], [], []
|
||||
self.n = 0
|
||||
|
||||
|
||||
def serve(bao: Bao):
|
||||
class H(BaseHTTPRequestHandler):
|
||||
def _send(self, body):
|
||||
raw = json.dumps(body).encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(raw)))
|
||||
self.end_headers()
|
||||
self.wfile.write(raw)
|
||||
|
||||
def do_POST(self):
|
||||
body = json.loads(self.rfile.read(int(self.headers.get("Content-Length") or 0)) or b"{}")
|
||||
if self.path == "/v1/auth/kubernetes/login":
|
||||
bao.n += 1
|
||||
bao.logins.append((body["role"], body["jwt"]))
|
||||
return self._send({"auth": {"client_token": f"tok-{body['role']}-{bao.n}",
|
||||
"lease_duration": bao.lease}})
|
||||
if self.path == "/v1/auth/token/revoke-self":
|
||||
bao.revoked.append(self.headers.get("X-Vault-Token"))
|
||||
return self._send({})
|
||||
self._send({})
|
||||
|
||||
def do_GET(self):
|
||||
bao.reads.append((self.path, self.headers.get("X-Vault-Token")))
|
||||
self._send({"data": {"data": {"PASSWORD": "p", "TOTP_SEED": "GEZDGNBV"}, "metadata": {"version": 3}}})
|
||||
|
||||
def log_message(self, *a):
|
||||
return
|
||||
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), H)
|
||||
threading.Thread(target=server.serve_forever, kwargs={"poll_interval": 0.02}, daemon=True).start()
|
||||
return server
|
||||
|
||||
|
||||
class Base(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.bao = Bao()
|
||||
self.server = serve(self.bao)
|
||||
self.addCleanup(self.server.shutdown)
|
||||
tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(tmp.cleanup)
|
||||
self.root = pathlib.Path(tmp.name)
|
||||
(self.root / "jwt").write_text("synthetic-openbao-audience-jwt\n")
|
||||
self.addr = f"http://127.0.0.1:{self.server.server_port}"
|
||||
|
||||
|
||||
class CliLanes(Base):
|
||||
def run_cli(self, cfg):
|
||||
(self.root / "cfg.json").write_text(json.dumps(cfg))
|
||||
admission = CLI.X.REPO / "examples/bounded-execution-admission/bind-rotation-svc-authelia.yaml"
|
||||
with contextlib.redirect_stdout(io.StringIO()) as out:
|
||||
code = CLI.main(["--admission", str(admission), "--approval-id", "apr-1",
|
||||
"--config", str(self.root / "cfg.json")])
|
||||
return code, json.loads(out.getvalue())
|
||||
|
||||
def cfg(self, journey_role="journey-read"):
|
||||
# lldap is missing on purpose: setup fails after both logins
|
||||
return {"openbao": {"addr": self.addr, "k8s_role": "bind-write", "jwt_file": str(self.root / "jwt")},
|
||||
"journey": {"kv": "x", "k8s_role": journey_role, "config": {}}}
|
||||
|
||||
def test_two_lanes_two_logins_both_revoked_when_setup_fails(self):
|
||||
code, result = self.run_cli(self.cfg())
|
||||
self.assertEqual((code, result["reason"]), (4, "setup:KeyError"))
|
||||
self.assertEqual([role for role, _ in self.bao.logins], ["bind-write", "journey-read"])
|
||||
self.assertEqual(sorted(self.bao.revoked), ["tok-bind-write-1", "tok-journey-read-2"])
|
||||
|
||||
def test_one_role_for_both_lanes_is_refused_before_any_login(self):
|
||||
code, result = self.run_cli(self.cfg(journey_role="bind-write"))
|
||||
self.assertEqual((code, result["reason"]), (4, "setup:ValueError"))
|
||||
self.assertEqual(self.bao.logins, [])
|
||||
|
||||
|
||||
class Renewal(Base):
|
||||
def test_logs_in_again_before_the_lease_ends_and_revokes_the_old_token(self):
|
||||
self.bao.lease = 1 # renew_at = max(1 - margin, 0.5 s)
|
||||
client = OPS.OpenBao(self.addr, k8s_role="journey-read", jwt_file=str(self.root / "jwt"))
|
||||
client.read("platform/workloads/x")
|
||||
time.sleep(0.6)
|
||||
client.read("platform/workloads/x")
|
||||
self.assertEqual(len(self.bao.logins), 2)
|
||||
self.assertEqual(self.bao.revoked, ["tok-journey-read-1"])
|
||||
self.assertEqual([tok for _, tok in self.bao.reads], ["tok-journey-read-1", "tok-journey-read-2"])
|
||||
|
||||
def test_a_long_lease_is_not_renewed(self):
|
||||
client = OPS.OpenBao(self.addr, k8s_role="r", jwt_file=str(self.root / "jwt"))
|
||||
client.read("platform/workloads/x")
|
||||
client.read("platform/workloads/x")
|
||||
self.assertEqual(len(self.bao.logins), 1)
|
||||
|
||||
def test_a_given_token_never_logs_in(self):
|
||||
client = OPS.OpenBao(self.addr, token="static")
|
||||
client.read("platform/workloads/x")
|
||||
client.revoke_self()
|
||||
self.assertEqual((self.bao.logins, self.bao.revoked), ([], ["static"]))
|
||||
|
||||
|
||||
class JourneyUsesItsOwnLane(unittest.TestCase):
|
||||
def test_journey_check_reads_only_through_the_journey_client(self):
|
||||
class Recorder:
|
||||
def __init__(self, name):
|
||||
self.name, self.reads = name, []
|
||||
|
||||
def read(self, path):
|
||||
self.reads.append(path)
|
||||
return {"PASSWORD": "p", "TOTP_SEED": "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"}, 1
|
||||
|
||||
write, read = Recorder("write"), Recorder("read")
|
||||
_, checks = OPS.pilot_registry(bao=write, journey_bao=read, directory=None, delivery=None, restart=None,
|
||||
bind_kv="b", bind_user="svc-authelia", journey_kv="j", journey_config={})
|
||||
original = (OPS.journey.run_journey, OPS._load_config)
|
||||
OPS.journey.run_journey = lambda cfg, pw, seed, min_time_step=None: {"result": "pass"}
|
||||
OPS._load_config = lambda cfg: cfg
|
||||
try:
|
||||
self.assertTrue(checks["synthetic-journey"]({}, type("Ctx", (), {"data": {}})()))
|
||||
finally:
|
||||
OPS.journey.run_journey, OPS._load_config = original
|
||||
self.assertEqual((write.reads, read.reads), ([], ["j"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue