Add the backup reader boundary probe and record founder approval of MAS node execution.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 4541@bnt-lap001
Assistant-Session: 2099b3aa-d7d9-49a9-ab4e-fa8b7d0e39e8
This commit is contained in:
codex 2026-11-03 05:50:10 +01:00
parent c02c3977f9
commit d41c5ace0a
4 changed files with 417 additions and 3 deletions

View file

@ -237,7 +237,7 @@
| task | RPF-WP-0069-T08 | done | — | workplans/RPF-WP-0069-bounded-executor-lanes.md |
| task | RPF-WP-0069-T09 | wait | — | workplans/RPF-WP-0069-bounded-executor-lanes.md |
| task | RPF-WP-0070-T01 | done | — | workplans/RPF-WP-0070-mas-evidence-recovery.md |
| task | RPF-WP-0070-T02 | todo | — | workplans/RPF-WP-0070-mas-evidence-recovery.md |
| task | RPF-WP-0070-T02 | progress | — | workplans/RPF-WP-0070-mas-evidence-recovery.md |
| task | RPF-WP-0070-T03 | todo | — | workplans/RPF-WP-0070-mas-evidence-recovery.md |
| task | RPF-WP-0070-T04 | todo | — | workplans/RPF-WP-0070-mas-evidence-recovery.md |
| task | RPF-WP-0070-T05 | todo | — | workplans/RPF-WP-0070-mas-evidence-recovery.md |

View file

@ -0,0 +1,307 @@
#!/usr/bin/env python3
"""Prove one backup reader's OpenBao boundary on railiance01 before delivery admission.
Run as root on railiance01. Stdlib only and self-contained, so it can be piped:
ssh railiance01 'sudo python3 - --application mas --expected-kv-version 1' \
< scripts/backup_reader_boundary_probe.py
Positive: the dedicated ServiceAccount logs in with audience openbao, holds only
its one reader policy within the CCR bounds, and reads exactly the expected KV
version with exactly the two pair fields.
Negative: a wrong audience and a foreign role cannot log in; sibling data,
metadata, parent LIST, a KV write and an administration endpoint are denied.
The write probe sends CAS=0 against an existing path, so it can never replace
the pair even if a policy were wrong.
Prints one JSON receipt. Values, tokens and JWTs never leave process memory;
the receipt holds field names, HTTP statuses, versions and bounds only.
Exit 0 only when every check passes.
"""
from __future__ import annotations
import argparse
import base64
import datetime as dt
import ipaddress
import json
import re
import subprocess
import sys
import time
import urllib.error
import urllib.request
CLUSTER_UID = "a553c742-0115-43d4-99a4-a5ca56fe0786"
SA = "backup-primary-delivery"
FIELDS = ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"]
APPLICATIONS = ("coulomb-social", "email-connect", "mas")
KUBE_ENV = {
"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"KUBECONFIG": "/etc/rancher/k3s/k3s.yaml",
}
class ProbeError(Exception):
"""Fixed diagnostic codes only."""
def guard(condition, code):
if not condition:
raise ProbeError(code)
def policy(application):
return f"workload-kv-read-{application}-backup-primary"
def role(application):
return f"{application}-backup-primary-workload-kv-read"
def data_path(application):
return f"platform/data/workloads/{application}/backup-primary"
class Native:
"""kubectl and OpenBao access on the node. HTTP errors return their status."""
def __init__(self):
self.address = None
self.opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), _NoRedirect())
def kube(self, args):
try:
result = subprocess.run(
["kubectl", "--request-timeout=20s", *args],
capture_output=True, check=False, timeout=30, env=KUBE_ENV,
)
except (OSError, subprocess.SubprocessError):
raise ProbeError("kubernetes_command_failed") from None
guard(result.returncode == 0 and len(result.stdout) <= 1048576, "kubernetes_command_failed")
return result.stdout
def prepare(self, application):
ns = json.loads(self.kube(["get", "namespace", "kube-system", "-o", "json"]))
guard(ns.get("metadata", {}).get("uid") == CLUSTER_UID, "cluster_identity_mismatch")
svc = json.loads(self.kube(["-n", "openbao", "get", "service", "openbao", "-o", "json"]))
addr = ipaddress.ip_address(svc.get("spec", {}).get("clusterIP", ""))
guard(
addr in ipaddress.ip_network("10.43.0.0/16")
and any(p.get("port") == 8200 for p in svc["spec"].get("ports", [])),
"openbao_service_mismatch",
)
self.address = f"http://{addr}:8200/v1/"
sa = json.loads(self.kube(["-n", application, "get", "serviceaccount", SA, "-o", "json"]))
guard(sa.get("automountServiceAccountToken") is False, "non_mounting_service_account_required")
return sa.get("metadata", {}).get("uid")
def jwt(self, application, audience):
token = self.kube([
"-n", application, "create", "token", SA, f"--audience={audience}", "--duration=10m",
]).decode().strip()
guard(len(token.split(".")) == 3 and len(token) < 16384, "bounded_jwt_required")
return token
def http(self, method, path, payload=None, token=None):
headers = {"Content-Type": "application/json"}
if token is not None:
headers["X-Vault-Token"] = token
body = None if payload is None else json.dumps(payload).encode()
request = urllib.request.Request(self.address + path, data=body, headers=headers, method=method)
try:
with self.opener.open(request, timeout=20) as response:
raw = response.read(1048577)
guard(len(raw) <= 1048576, "openbao_response_too_large")
return response.status, ({} if not raw else json.loads(raw))
except urllib.error.HTTPError as exc:
return exc.code, {}
except (urllib.error.URLError, OSError):
raise ProbeError("openbao_request_failed") from None
class _NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, *args, **kwargs):
return None
def jwt_claims(token):
piece = token.split(".")[1]
return json.loads(base64.urlsafe_b64decode(piece + "=" * (-len(piece) % 4)))
def _login(native, application, login_role, audience="openbao"):
status, body = native.http(
"POST", "auth/kubernetes/login", {"role": login_role, "jwt": native.jwt(application, audience)}
)
auth = body.get("auth") if isinstance(body, dict) else None
token = auth.get("client_token") if isinstance(auth, dict) else None
return status, auth or {}, token if isinstance(token, str) and token else None
def _revoke(native, token):
status, _ = native.http("POST", "auth/token/revoke-self", {}, token=token)
return status in (200, 204)
def _denials(native, token, probes):
"""Run up to three denied probes on one four-use token, then revoke it."""
statuses = {}
try:
for name, method, path, payload in probes:
statuses[name], _ = native.http(method, path, payload, token=token)
finally:
revoked = _revoke(native, token)
return statuses, revoked
def probe(native, application, expected_version, sa_uid=None):
guard(application in APPLICATIONS, "application_refused")
guard(type(expected_version) is int and expected_version > 0, "expected_version_required")
expected_policy = policy(application)
receipt = {
"schema": "platform.backup-reader-boundary.v1",
"application": application,
"cluster_uid": CLUSTER_UID,
"service_account": f"{application}/{SA}",
"role": role(application),
"policy": expected_policy,
"kv_path": data_path(application),
"expected_kv_version": expected_version,
"values_recorded": False,
}
if sa_uid is None:
sa_uid = native.prepare(application)
claims = jwt_claims(native.jwt(application, "openbao"))
bound = claims.get("kubernetes.io", {}).get("serviceaccount", {})
receipt["jwt_binding"] = (
claims.get("sub") == f"system:serviceaccount:{application}:{SA}"
and claims.get("aud") == ["openbao"]
and bool(bound.get("uid"))
and bound.get("uid") == sa_uid
and 0 < claims.get("exp", 0) - time.time() <= 660
)
# Positive: one token for lookup, the read, and revoke.
status, auth, token = _login(native, application, role(application))
positive = {"login_status": status}
if token:
try:
positive["login_policies"] = auth.get("policies")
positive["lease_duration"] = auth.get("lease_duration")
_, info = native.http("GET", "auth/token/lookup-self", token=token)
data = info.get("data", {}) if isinstance(info, dict) else {}
positive["token"] = {
"policies": data.get("policies"),
"identity_policies": data.get("identity_policies") or [],
"ttl": data.get("ttl"),
"explicit_max_ttl": data.get("explicit_max_ttl"),
"period": data.get("period") or 0,
"num_uses": data.get("num_uses"),
}
read_status, record = native.http("GET", data_path(application), token=token)
payload = record.get("data", {}) if isinstance(record, dict) else {}
metadata = payload.get("metadata", {}) if isinstance(payload, dict) else {}
values = payload.get("data", {}) if isinstance(payload, dict) else {}
values = values if isinstance(values, dict) else {}
positive["read_status"] = read_status
positive["kv_version"] = metadata.get("version")
positive["kv_destroyed"] = metadata.get("destroyed")
positive["kv_deleted"] = bool(metadata.get("deletion_time"))
positive["field_names"] = sorted(values)
positive["encoding_ok"] = all(
isinstance(values.get(name), str)
and re.fullmatch(r"[A-Za-z0-9/+=_.-]{8,256}", values[name]) is not None
for name in FIELDS
)
positive["fields_distinct"] = values.get(FIELDS[0]) != values.get(FIELDS[1])
del values, payload, record
finally:
positive["token_revoked"] = _revoke(native, token)
token_view = positive.get("token", {})
positive["passed"] = (
status == 200
and positive.get("login_policies") == [expected_policy]
and 0 < (positive.get("lease_duration") or 0) <= 300
and token_view.get("policies") == [expected_policy]
and token_view.get("identity_policies") == []
and 0 < (token_view.get("ttl") or 0) <= 300
and 0 < (token_view.get("explicit_max_ttl") or 0) <= 300
and token_view.get("period") == 0
and 0 < (token_view.get("num_uses") or 0) <= 4
and positive.get("read_status") == 200
and positive.get("kv_version") == expected_version
and positive.get("kv_destroyed") is False
and positive.get("kv_deleted") is False
and positive.get("field_names") == FIELDS
and positive.get("encoding_ok") is True
and positive.get("fields_distinct") is True
and positive.get("token_revoked") is True
)
receipt["positive"] = positive
negative = {}
# Login boundary: a wrong audience, and this account against a sibling role.
status, _, stray = _login(native, application, role(application), audience="not-openbao")
negative["wrong_audience_login"] = status
if stray:
_revoke(native, stray)
sibling = next(item for item in APPLICATIONS if item != application)
status, _, stray = _login(native, application, role(sibling))
negative["foreign_role_login"] = status
if stray:
_revoke(native, stray)
others = [item for item in APPLICATIONS if item != application]
status, _, token = _login(native, application, role(application))
guard(status == 200 and token, "negative_login_failed")
first, revoked_first = _denials(native, token, [
("sibling_data_read", "GET", data_path(others[0]), None),
("metadata_read", "GET", f"platform/metadata/workloads/{application}/backup-primary", None),
("parent_list", "LIST", f"platform/metadata/workloads/{application}", None),
])
status, _, token = _login(native, application, role(application))
guard(status == 200 and token, "negative_login_failed")
second, revoked_second = _denials(native, token, [
# CAS=0 on an existing path cannot replace the pair even if allowed.
("kv_write", "POST", data_path(application), {"options": {"cas": 0}, "data": {"probe": "x"}}),
("second_sibling_data_read", "GET", data_path(others[1]), None),
("admin_policy_list", "LIST", "sys/policies/acl", None),
])
negative.update(first)
negative.update(second)
negative["tokens_revoked"] = revoked_first and revoked_second
login_denied = all(negative[name] in (400, 403) for name in ("wrong_audience_login", "foreign_role_login"))
api_denied = all(
negative[name] == 403
for name in (
"sibling_data_read", "metadata_read", "parent_list",
"kv_write", "second_sibling_data_read", "admin_policy_list",
)
)
negative["passed"] = login_denied and api_denied and negative["tokens_revoked"]
receipt["negative"] = negative
receipt["passed"] = receipt["jwt_binding"] and positive["passed"] and negative["passed"]
receipt["checked_at"] = dt.datetime.now(dt.timezone.utc).isoformat()
return receipt
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--application", choices=APPLICATIONS, required=True)
parser.add_argument("--expected-kv-version", type=int, required=True)
args = parser.parse_args(argv)
try:
receipt = probe(Native(), args.application, args.expected_kv_version)
except ProbeError as exc:
receipt = {"status": "failed", "code": str(exc), "values_recorded": False}
except Exception as exc: # noqa: BLE001 - never print backend bodies
receipt = {"status": "failed", "code": "contained_probe_failure",
"error_type": type(exc).__name__, "values_recorded": False}
print(json.dumps(receipt, indent=2, sort_keys=True))
return 0 if receipt.get("passed") is True else 1
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,95 @@
"""Reader boundary probe: passes only on an exact reader and never records values."""
import base64
import importlib.util
import json
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location("boundary", ROOT / "scripts/backup_reader_boundary_probe.py")
probe = importlib.util.module_from_spec(spec)
spec.loader.exec_module(probe)
SECRET = "supersecretvalue12345"
ACCESS = "SCWACCESSKEY12345678"
def _jwt(app, aud, uid="sa-uid"):
claims = {"sub": f"system:serviceaccount:{app}:{probe.SA}", "aud": [aud],
"exp": int(time.time()) + 600, "kubernetes.io": {"serviceaccount": {"uid": uid}}}
body = base64.urlsafe_b64encode(json.dumps(claims).encode()).decode().rstrip("=")
return f"h.{body}.s"
class FakeBao:
def __init__(self, app="mas", version=1, allow_write=False, extra_policy=False, allow_foreign_role=False):
self.app, self.version = app, version
self.allow_write, self.extra_policy, self.allow_foreign_role = allow_write, extra_policy, allow_foreign_role
self.tokens, self.writes = {}, []
def jwt(self, application, audience):
return _jwt(application, audience)
def http(self, method, path, payload=None, token=None):
if path == "auth/kubernetes/login":
claims = probe.jwt_claims(payload["jwt"])
ok_role = payload["role"] == probe.role(self.app) or self.allow_foreign_role
if claims["aud"] != ["openbao"] or not ok_role:
return 403, {}
tok = f"t{len(self.tokens)}"
policies = [probe.policy(self.app)] + (["default"] if self.extra_policy else [])
self.tokens[tok] = {"uses": 4, "policies": policies}
return 200, {"auth": {"client_token": tok, "policies": policies, "lease_duration": 300}}
state = self.tokens.get(token)
if not state or state["uses"] <= 0:
return 403, {}
state["uses"] -= 1
if path == "auth/token/revoke-self":
del self.tokens[token]
return 204, {}
if path == "auth/token/lookup-self":
return 200, {"data": {"policies": state["policies"], "identity_policies": None, "ttl": 299,
"explicit_max_ttl": 300, "period": 0, "num_uses": state["uses"]}}
if method == "GET" and path == probe.data_path(self.app):
return 200, {"data": {"data": {"AWS_ACCESS_KEY_ID": ACCESS, "AWS_SECRET_ACCESS_KEY": SECRET},
"metadata": {"version": self.version, "destroyed": False, "deletion_time": ""}}}
if method == "POST" and path == probe.data_path(self.app) and self.allow_write:
self.writes.append(payload)
return 400, {} # CAS mismatch: the path already exists
return 403, {}
def run(fake, version=1):
return probe.probe(fake, "mas", version, sa_uid="sa-uid")
def test_exact_reader_passes_and_records_no_values():
fake = FakeBao()
receipt = run(fake)
assert receipt["passed"] is True, receipt
assert receipt["positive"]["kv_version"] == 1
assert receipt["positive"]["field_names"] == probe.FIELDS
text = json.dumps(receipt)
assert SECRET not in text and ACCESS not in text and "t0" not in text
assert fake.tokens == {} # every issued token revoked
def test_version_mismatch_fails():
assert run(FakeBao(version=2))["passed"] is False
def test_extra_policy_fails():
assert run(FakeBao(extra_policy=True))["positive"]["passed"] is False
def test_write_not_denied_fails_and_cas0_payload_cannot_replace():
fake = FakeBao(allow_write=True)
receipt = run(fake)
assert receipt["negative"]["kv_write"] == 400 and receipt["passed"] is False
assert fake.writes == [{"options": {"cas": 0}, "data": {"probe": "x"}}]
def test_foreign_role_login_accepted_fails():
receipt = run(FakeBao(allow_foreign_role=True))
assert receipt["negative"]["foreign_role_login"] == 200 and receipt["passed"] is False

View file

@ -140,11 +140,11 @@ was not read or copied. Delivery admission stays false, and
```task
id: RPF-WP-0070-T02
status: todo
status: progress
priority: medium
needs_human: true
depends_on: [RPF-WP-0070-T01]
intervention_note: "Attended: set delivery admission for CCR-2026-0062 and run the node-file delivery after T01 records the pair."
intervention_note: "Founder approved node execution for CCR-2026-0062 on 2026-11-03. Next: run scripts/backup_reader_boundary_probe.py --application mas --expected-kv-version 1 as root on railiance01 and record the receipt. Then set the admission flags, export the CCR and pin its SHA256, and run the node delivery."
state_hub_task_id: "c8a9ba31-9fb6-5412-90b7-dea658943053"
```
@ -156,6 +156,18 @@ pair, through ServiceAccount `backup-primary-delivery`. Record a
metadata-only node check. Never copy the supplier key onto the workload
path, and give no TEM or SMTP credential to the node file.
2026-11-03: the founder approved node execution for CCR-2026-0062 in an
attended session. `scripts/backup_reader_boundary_probe.py` is the reader
boundary check. It is a self-contained stdlib script that runs as root on
railiance01. The positive check: the ServiceAccount logs in with audience
`openbao`, holds only the one reader policy within the CCR bounds, and
reads exactly the expected KV version with exactly the two fields. The
negative checks: login fails with a wrong audience and with a sibling role,
and these are refused with 403: sibling data reads, a metadata read, the
parent LIST, a KV write, and `sys/policies/acl`. The write probe uses CAS=0
on the existing path, so it cannot replace the pair. Every token is revoked,
and the receipt holds statuses and names only.
## Install the worker and publish the first offsite copy
```task