Prepare reviewed service JWT boundary verification and retain execution adoption gates

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
This commit is contained in:
tegwick 2026-10-11 14:11:59 +02:00
parent 443c2698c3
commit 4378ec1fc2
6 changed files with 551 additions and 7 deletions

View file

@ -18,6 +18,12 @@ through a temporary JSON file, never argv. The resulting OpenBao token is
revoked at the end of the command (`token revoke -self`). Evidence records revoked at the end of the command (`token revoke -self`). Evidence records
the provider name and an accessor fingerprint only. the provider name and an accessor fingerprint only.
Until railiance-platform publishes that file, `--auth auto` keeps the named railiance-platform published the exact consumer file on 2026-10-10 at
bootstrap-file and `BAO_TOKEN` providers. `--auth service-jwt` fail-closes. `docs/contracts/secrets-engine-openbao-jwt-login.yaml` (mount `keycape-services`,
role `secrets-engine-openbao`, issuer `https://kc.coulomb.social`). When configured,
`--auth auto` selects service-jwt; without a contract it keeps named bootstrap/env
providers. Explicit service-jwt fails closed when configuration is missing.
The current service token is login-self-only and cannot perform protected lane
operations. T06 also requires a reviewed task-scoped execution entitlement and
functional steady-state adoption; see [final acceptance](service-jwt-final-acceptance.md).
A service-jwt selection never reads `--bootstrap-token-file` or `BAO_TOKEN`. A service-jwt selection never reads `--bootstrap-token-file` or `BAO_TOKEN`.

View file

@ -36,8 +36,10 @@ policy, TTL/use limits, and cryptographic verification.
When `SECRETS_ENGINE_OPENBAO_JWT_LOGIN` names that contract, the CLI selects When `SECRETS_ENGINE_OPENBAO_JWT_LOGIN` names that contract, the CLI selects
`service-jwt` and logs in for one command, then self-revokes the OpenBao `service-jwt` and logs in for one command, then self-revokes the OpenBao
token. Failure of that path never falls back to `--bootstrap-token-file`, token. Failure of that path never falls back to `--bootstrap-token-file`,
`BAO_TOKEN`, or AppRole. Until the contract is published, `--auth auto` keeps `BAO_TOKEN`, or AppRole. The platform contract was published 2026-10-10. Configured `--auth auto`
those named providers and `--auth service-jwt` fail-closes. See selects service-jwt; without that configuration it keeps bootstrap/env. The
current login-self-only token cannot perform protected lane operations;
functional adoption and task-scoped execution entitlement remain T06. See
[openbao-jwt-login.md](openbao-jwt-login.md). [openbao-jwt-login.md](openbao-jwt-login.md).
Canonical provider contract: Canonical provider contract:

View file

@ -0,0 +1,111 @@
# Service-JWT final acceptance — SECRETS-WP-0008-T06
Prepared 2026-10-11; source preparation only. T06 remains wait. This packet
neither grants live authority nor provides owner acknowledgment.
## Delivered producer and consumer work
SECRETS-WP-0012 is finished: protected in-memory client-secret acquisition,
operator cleanup, KeyCape exchange, service JWT login and service cleanup passed
in `docs/evidence/2026-10-10-keycape-openbao-probe.json`. The platform applied
CCR-2026-0038 on October 8 and published
`railiance-platform/docs/contracts/secrets-engine-openbao-jwt-login.yaml` on
October 10. Mount `keycape-services`, role `secrets-engine-openbao`, issuer
`https://kc.coulomb.social` are published. An unpublished-mount or
unprovisioned-registration wait is obsolete.
The consumer no-fallback receipt
`docs/evidence/2026-10-10-service-jwt-no-fallback.json` proves synthetic selection
and real Bao CLI header behavior against a loopback listener. It is not native
claim rejection evidence. KEY-WP-0039-T01 is source acceptance done; T02 owns the
attended actual negative-token session. RPF-WP-0035-T02 retains native claims,
effective policy/capability denials, and MASON-WP-0009-T05 return.
WP0007 is six of seven complete with native per-lane adoption outstanding.
WP0009 credential apply/verify/delivery/revocation passed in the September 27
attended receipts, but its task cannot finish: held EUR 10 reservation still
needs accounting reconciliation, replacement runtime admission and separate
retry authority. These are existing live tasks; this packet closes neither.
## Executable missing identity-boundary check
`tools/verify_service_jwt_boundary.py` defaults to backend-free preparation.
It checks exact applied CCR, published mount/role and issuer, client-secret-file
refusal and exact guarded paths. Preparation cannot produce a passing native
receipt. After explicit authorization for the attended metadata check, add
`--execute` to the same command:
```bash
env -u BAO_TOKEN -u VAULT_TOKEN -u SECRETS_ENGINE_KEYCAPE_CLIENT_SECRET_FILE \
BAO_ADDR=http://127.0.0.1:18200 \
SECRETS_ENGINE_KEYCAPE_TOKEN_URL=https://kc.coulomb.social/token \
SECRETS_ENGINE_KEYCAPE_ISSUER=https://kc.coulomb.social \
SECRETS_ENGINE_OPENBAO_JWT_LOGIN=/home/worsch/railiance-platform/docs/contracts/secrets-engine-openbao-jwt-login.yaml \
uv run python tools/verify_service_jwt_boundary.py \
--ccr /home/worsch/railiance-platform/credential-change-requests/CCR-2026-0038-secrets-engine-service-jwt-login.yaml
```
Live mode uses existing attended custody and performs two service-token metadata
requests: lookup-self and one batch capabilities-self. The batch checks both
client-secret guarded paths, `sys/policies/acl/*`, `auth/*/role/*` and
`auth/token/create`; it never reads their values or attempts writes. Cleanup
runs even on failed verification. Two metadata calls reserve room for cleanup
within the service token's eight-use budget. Passing requires exactly the
self policy, no identity-policy additions, deny on every requested path and
both operator and service revocation success. Output contains hashes, capability
booleans and session fingerprints. No backend response/error body is printed.
Transport fixes bind each request to the configured OpenBao origin, disable
environment proxies and refuse all redirects including relative redirects.
Only HTTP 200 application/json with a bounded 64 KiB body and object data is
accepted. Errors and response bodies are suppressed. Session evidence is
restricted to the contract whitelist with strict boolean types, the fixed
source name, and twelve-hex session handles. Native acceptance requires every
contract field (established, source, both fingerprints and attempted/succeeded
cleanup flags), a normally returned cleanup call, and no verification failure.
Stale true evidence cannot override a cleanup exception. HTTP JSON with an
errors member is refused even when a data member also exists. Synthetic/local-loopback tests
cover redirects, proxy environment, size/status/content/shape failures, absent
responses, unknown exceptions and unknown evidence fields with cleanup.
This does not prove expiry/use exhaustion, native claim negatives or adoption.
Platform retains those native test obligations and owner review of this tool.
## Protected-operation adoption is a distinct missing contract
INTENT requires task-bounded authority and a functional Lifecycle API. The
platform design explicitly says this login policy is insufficient for native
lanes; future execution entitlement requires a separately reviewed CCR type and
backend-enforced scope. No standing wildcard policy is accepted.
Actual CLI flow (`cli._require_lane_approval`, `_open_backend`, `cmd_apply`) is
claim → validated PDP Check → approval CAS consume → select provider → OpenBao
operation. `_open_backend` supplies the login-only service token directly.
There is no per-action delegated grant or scoped backend exchange in that path.
Consequently a service-jwt default flip would consume a real approval then fail
the protected operation. Configured `auto` chooses service-jwt; unconfigured
`auto` still chooses bootstrap/env. Neither selection proves steady-state
adoption. Keep existing service policy and provider defaults until the execution
contract exists; failed JWT never switches provider.
Founder/owner choice needed: retain login-self-only and implement a separately
owned task-scoped delegation contract (recommended), or explicitly redesign
backend execution mediation with platform enforcement. Both must preserve
claim/PDP/CAS binding, exact action/lane/target/expiry/principal scope, failure
closed, revocation and audit evidence; neither means granting broad standing
ACLs. Retaining attended bootstrap/env as a transition leaves T06 open.
Concrete missing owner obligation: secrets-engine and railiance-platform must
agree the execution entitlement's owner and CCR schema, implementation endpoint,
backend policy enforcement, exact request binding, TTL/use budget and revocation,
then wire it after successful consume. KeyCape supplies identity, not authority.
Existing native AppRole delivery sessions are short-lived workload read sessions,
not an engine administrative entitlement. Existing native lane cutover requires
scoped attended platform authority but does not implement this exchange.
T06 closes only after native KEY39/CCR negative/expiry/use/cleanup evidence and
MASON return, a reviewed implemented execution entitlement, and a selected real
protected action through service identity plus delegated authority: exact bound
PDP/CAS evidence, intended backend result, unauthorized target/action/expired or
replayed request refusal, cleanup on success and failure, no standing engine
credential, and steady-state invocation/default migration evidence. Do not use
`apply` or any other protected live command to discover the missing entitlement.
All of this remains in T06 and owner tasks; no residual is silently finished.

View file

@ -0,0 +1,212 @@
import importlib.util
from pathlib import Path
from types import SimpleNamespace
import pytest
from secrets_engine.errors import BackendError
spec = importlib.util.spec_from_file_location("boundary", Path(__file__).parents[1] / "tools/verify_service_jwt_boundary.py")
boundary = importlib.util.module_from_spec(spec)
spec.loader.exec_module(boundary)
class Session:
client = SimpleNamespace()
closed = False
def close(self):
self.closed = True
def evidence(self):
return {"secret_source": "openbao-oidc-in-memory", "established": True,
"operator_session_handle": "0123456789ab", "session_handle": "abcdef012345",
"operator_revocation_attempted": True, "operator_revocation_succeeded": True,
"revocation_attempted": self.closed, "revocation_succeeded": self.closed}
@pytest.mark.parametrize("extra_identity", [False, True])
def test_batch_denial_checks_identity_policy_and_always_cleans(extra_identity):
session = Session()
paths = list(boundary.EXPECTED_GUARDS + boundary.ADMIN_DENIES)
calls = []
def request(client, path, payload=None):
calls.append((path, payload))
if path == "auth/token/lookup-self":
return {"policies": ["secrets-engine-login-self"], "identity_policies": ["admin"] if extra_identity else []}
return {p: ["deny"] for p in paths}
result = boundary.verify_session(session, {"expected_policy": "secrets-engine-login-self", "denied_paths": paths}, request)
assert result["native_verified"] is (not extra_identity)
assert session.closed
assert len(calls) == 2
assert calls[1][1] == {"paths": paths}
assert not result["protected_operation_adoption_proved"]
@pytest.mark.parametrize("failure", ["request", "missing-deny", "revoke"])
def test_failed_or_partial_acceptance_never_passes(failure):
session = Session()
paths = list(boundary.EXPECTED_GUARDS + boundary.ADMIN_DENIES)
def request(client, path, payload=None):
if failure == "request":
raise BackendError("sensitive backend error")
if path == "auth/token/lookup-self":
return {"policies": ["secrets-engine-login-self"]}
return {p: ["deny"] for p in paths[:-1] if failure == "missing-deny"} if failure == "missing-deny" else {p: ["deny"] for p in paths}
if failure == "revoke":
def close():
raise BackendError("sensitive backend error")
session.close = close
result = boundary.verify_session(session, {"expected_policy": "secrets-engine-login-self", "denied_paths": paths}, request)
assert not result["native_verified"]
assert "sensitive backend error" not in str(result)
if failure != "revoke":
assert session.closed
@pytest.mark.parametrize("fault", ["oversize-header", "oversize-body", "malformed", "wrong-shape", "wrong-status", "wrong-content", "no-response", "unknown-error"])
def test_transport_failures_suppress_body_and_cleanup(monkeypatch, fault):
from email.message import Message
headers = Message()
headers["Content-Type"] = "text/plain" if fault == "wrong-content" else "application/json"
if fault == "oversize-header":
headers["Content-Length"] = str(boundary.MAX_RESPONSE_BYTES + 1)
response = SimpleNamespace(status=503 if fault == "wrong-status" else 200, headers=headers)
raw = b'{"data": {}}'
if fault == "oversize-body":
raw = b'x' * (boundary.MAX_RESPONSE_BYTES + 1)
if fault == "malformed":
raw = b'private-body'
if fault == "wrong-shape":
raw = b'{"data": []}'
class Response:
status = response.status
headers = response.headers
def __enter__(self): return self
def __exit__(self, *args): pass
def read(self, count):
assert count == boundary.MAX_RESPONSE_BYTES + 1
return raw
def opening(req, timeout):
if fault in ("no-response", "unknown-error"):
raise RuntimeError("private-body")
return Response()
monkeypatch.setattr(boundary, "build_opener", lambda *args: SimpleNamespace(open=opening))
session = Session()
session.client = SimpleNamespace(addr="http://127.0.0.1:18200", token="sentinel")
plan = {"expected_policy": "secrets-engine-login-self", "denied_paths": list(boundary.EXPECTED_GUARDS + boundary.ADMIN_DENIES)}
result = boundary.verify_session(session, plan, lambda c,p,d=None: boundary.request_json(c,p,d,expected_addr=c.addr))
assert not result["native_verified"]
assert session.closed
assert "private-body" not in str(result)
@pytest.mark.parametrize("location", ["http://127.0.0.1:9/private", "/relative"])
def test_real_redirect_refused_and_proxy_environment_ignored(monkeypatch, location):
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
observed = []
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
observed.append(self.path)
self.send_response(302)
self.send_header("Location", location)
self.end_headers()
def log_message(self, *args): pass
server = HTTPServer(("127.0.0.1", 0), Handler)
worker = threading.Thread(target=server.serve_forever, daemon=True)
worker.start()
# Invalid proxy would prevent the first request if environment proxies were used.
monkeypatch.setenv("http_proxy", "http://127.0.0.1:1")
monkeypatch.setenv("HTTP_PROXY", "http://127.0.0.1:1")
monkeypatch.setenv("no_proxy", "")
monkeypatch.setenv("NO_PROXY", "")
addr = f"http://127.0.0.1:{server.server_port}"
session = Session()
session.client = SimpleNamespace(addr=addr, token="synthetic-token-only")
try:
result = boundary.verify_session(session, {"expected_policy":"secrets-engine-login-self", "denied_paths": list(boundary.EXPECTED_GUARDS + boundary.ADMIN_DENIES)},
lambda c,p,d=None: boundary.request_json(c,p,d,expected_addr=addr))
finally:
server.shutdown()
server.server_close()
worker.join()
assert observed == ["/v1/auth/token/lookup-self"]
assert session.closed
assert not result["native_verified"]
def test_origin_mismatch_refused_before_request(monkeypatch):
monkeypatch.setattr(boundary, "build_opener", lambda *args: pytest.fail("must not send token"))
client = SimpleNamespace(addr="http://evil.invalid", token="sentinel")
with pytest.raises(BackendError, match="response suppressed"):
boundary.request_json(client, "auth/token/lookup-self", expected_addr="http://127.0.0.1:18200")
def test_unknown_evidence_and_private_failure_not_published():
session = Session()
session.evidence = lambda: {"private_token": "private-body", "session_handle": "private-body", "secret_source": "private-body", "operator_revocation_succeeded": True, "revocation_succeeded": True}
def fail(*args): raise RuntimeError("private-body")
result = boundary.verify_session(session, {"expected_policy":"secrets-engine-login-self", "denied_paths": list(boundary.EXPECTED_GUARDS)}, fail)
assert session.closed
assert not result["native_verified"]
assert "private-body" not in str(result)
assert "private_token" not in result
@pytest.mark.parametrize("missing", list(boundary.SAFE_EVIDENCE))
def test_every_contract_evidence_field_required(missing):
session = Session()
original = session.evidence
def evidence():
result = original()
del result[missing]
return result
session.evidence = evidence
paths = list(boundary.EXPECTED_GUARDS + boundary.ADMIN_DENIES)
def request(client, path, payload=None):
return {"policies": ["secrets-engine-login-self"]} if path == "auth/token/lookup-self" else {p:["deny"] for p in paths}
result = boundary.verify_session(session, {"expected_policy":"secrets-engine-login-self", "denied_paths": paths}, request)
assert session.closed
assert not result["native_verified"]
def test_revoke_exception_cannot_be_overridden_by_stale_evidence():
session = Session()
session.closed = True
def close(): raise RuntimeError("private failure")
session.close = close
paths = list(boundary.EXPECTED_GUARDS + boundary.ADMIN_DENIES)
def request(client, path, payload=None):
return {"policies": ["secrets-engine-login-self"]} if path == "auth/token/lookup-self" else {p:["deny"] for p in paths}
result = boundary.verify_session(session, {"expected_policy":"secrets-engine-login-self", "denied_paths":paths}, request)
assert result["revocation_succeeded"] is True
assert not result["native_verified"]
assert "private failure" not in str(result)
@pytest.mark.parametrize("errors", [[], ["private backend failure"], None])
def test_errors_and_data_response_never_accepted(monkeypatch, errors):
import json
from email.message import Message
headers = Message()
headers["Content-Type"] = "application/json"
class Response:
status = 200
def __enter__(self): return self
def __exit__(self, *args): pass
def read(self, count): return json.dumps({"data": {}, "errors": errors}).encode()
response = Response()
response.headers = headers
monkeypatch.setattr(boundary, "build_opener", lambda *args: SimpleNamespace(open=lambda *a,**k: response))
client = SimpleNamespace(addr="http://127.0.0.1:18200", token="synthetic-only")
with pytest.raises(BackendError, match="response suppressed"):
boundary.request_json(client, "auth/token/lookup-self", expected_addr=client.addr)

View file

@ -0,0 +1,194 @@
"""Prepare or attend CCR-2026-0038 self-policy/capability acceptance.
Default is backend-free. --execute invokes existing attended in-memory custody;
never print backend response bodies, secret values, tokens or full accessors.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import re
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.parse import urlsplit
from urllib.request import HTTPRedirectHandler, ProxyHandler, Request, build_opener
import yaml
from secrets_engine.config import Config
from secrets_engine.engine_auth import keycape_config, load_jwt_login_contract, login_service_jwt
from secrets_engine.errors import BackendError
ADMIN_DENIES = ("sys/policies/acl/*", "auth/*/role/*", "auth/token/create")
EXPECTED_GUARDS = (
"platform/data/workloads/keycape/secrets-engine-openbao",
"platform/metadata/workloads/keycape/secrets-engine-openbao",
)
def prepare(cfg, ccr_path):
raw = ccr_path.read_bytes()
ccr = yaml.safe_load(raw)
if not isinstance(ccr, dict) or ccr.get("id") != "CCR-2026-0038" or ccr.get("status") != "applied":
raise BackendError("expected applied CCR-2026-0038")
bao = ccr.get("openbao", {})
auth = bao.get("auth", {})
contract = load_jwt_login_contract(cfg)
kcfg = keycape_config(cfg)
if (contract.mount, contract.role, contract.bound_issuer) != (
auth.get("mount"), auth.get("role"), kcfg.issuer
):
raise BackendError("login contract and issuer must match applied CCR")
if bao.get("guarded_paths") != list(EXPECTED_GUARDS) or bao.get("policy_name") != "secrets-engine-login-self":
raise BackendError("unexpected CCR guarded paths or self policy")
return {
"ccr_sha256": hashlib.sha256(raw).hexdigest(),
"contract_sha256": hashlib.sha256(contract.path.read_bytes()).hexdigest(),
"denied_paths": list(EXPECTED_GUARDS + ADMIN_DENIES),
"expected_policy": "secrets-engine-login-self",
}
MAX_RESPONSE_BYTES = 65536
SAFE_EVIDENCE = {
"secret_source": str,
"operator_session_handle": str,
"session_handle": str,
"established": bool,
"operator_revocation_attempted": bool,
"operator_revocation_succeeded": bool,
"revocation_attempted": bool,
"revocation_succeeded": bool,
}
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
raise BackendError("redirect refused")
def origin(url):
parsed = urlsplit(url)
if parsed.scheme not in ("http", "https") or not parsed.hostname or parsed.username or parsed.password or parsed.query or parsed.fragment or parsed.path not in ("", "/"):
raise BackendError("invalid configured OpenBao origin")
return (parsed.scheme, parsed.hostname, parsed.port or (443 if parsed.scheme == "https" else 80))
def request_json(client, path, payload=None, *, expected_addr=None):
# expected_addr is captured from Config.load(), never inferred from a response.
try:
if expected_addr is None or origin(client.addr) != origin(expected_addr):
raise BackendError("unexpected OpenBao origin")
if path not in ("auth/token/lookup-self", "sys/capabilities-self"):
raise BackendError("unexpected metadata endpoint")
data = None if payload is None else json.dumps(payload).encode()
req = Request(expected_addr.rstrip("/") + "/v1/" + path, data=data,
headers={"X-Vault-Token": client.token, "Content-Type": "application/json"})
# Disable environment proxies and all redirect following before supplying token.
opener = build_opener(ProxyHandler({}), NoRedirect())
with opener.open(req, timeout=30) as response:
if response.status != 200 or response.headers.get_content_type() != "application/json":
raise BackendError("unexpected metadata response")
length = response.headers.get("Content-Length")
if length is not None and (not length.isdecimal() or int(length) > MAX_RESPONSE_BYTES):
raise BackendError("oversize metadata response")
raw = response.read(MAX_RESPONSE_BYTES + 1)
if len(raw) > MAX_RESPONSE_BYTES:
raise BackendError("oversize metadata response")
result = json.loads(raw)
if not isinstance(result, dict) or not isinstance(result.get("data"), dict) or "errors" in result:
raise BackendError("malformed metadata response")
return result["data"]
except Exception:
raise BackendError("boundary metadata request failed; response suppressed") from None
def safe_evidence(session):
result = {}
try:
evidence = session.evidence()
if not isinstance(evidence, dict):
return result
for key, kind in SAFE_EVIDENCE.items():
value = evidence.get(key)
if type(value) is not kind:
continue
if kind is str:
if key == "secret_source" and value != "openbao-oidc-in-memory":
continue
if key.endswith("handle") and not re.fullmatch(r"[0-9a-f]{12}", value):
continue
result[key] = value
except Exception:
pass
return result
def verify_session(session, plan, request=request_json):
"""Two metadata requests, then unconditional cleanup, within 8-use budget."""
result = {**plan, "auth_provider": "service-jwt", "native_verified": False}
cleanup_returned = False
try:
lookup = request(session.client, "auth/token/lookup-self")
# Effective identity policies matter even when default policy is absent.
policies = lookup.get("policies")
identities = lookup.get("identity_policies", [])
result["exact_self_policy"] = policies == [plan["expected_policy"]] and identities == []
caps = request(session.client, "sys/capabilities-self", {"paths": plan["denied_paths"]})
result["capability_denials"] = {path: caps.get(path) == ["deny"] for path in plan["denied_paths"]}
except Exception:
result["failure"] = "metadata verification failed; response suppressed"
finally:
try:
session.close()
cleanup_returned = True
except Exception:
result["failure"] = "service revocation failed"
result.update(safe_evidence(session))
evidence_complete = (
result.get("secret_source") == "openbao-oidc-in-memory"
and result.get("established") is True
and result.get("operator_revocation_attempted") is True
and result.get("operator_revocation_succeeded") is True
and result.get("revocation_attempted") is True
and result.get("revocation_succeeded") is True
and all(isinstance(result.get(key), str) and re.fullmatch(r"[0-9a-f]{12}", result[key])
for key in ("operator_session_handle", "session_handle"))
)
result["native_verified"] = bool(
cleanup_returned and "failure" not in result and evidence_complete
and result.get("exact_self_policy") and all(result.get("capability_denials", {}).values())
and len(result.get("capability_denials", {})) == len(plan["denied_paths"])
and result.get("operator_revocation_succeeded") and result.get("revocation_succeeded")
)
# These are deliberately separate from boundary acceptance.
result["claim_negatives_proved"] = False
result["protected_operation_adoption_proved"] = False
return result
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--ccr", type=Path, required=True)
parser.add_argument("--execute", action="store_true", help="attended live self metadata check; requires scoped authorization")
args = parser.parse_args()
try:
cfg = Config.load()
plan = prepare(cfg, args.ccr)
result = verify_session(
login_service_jwt(cfg), plan,
lambda client, path, payload=None: request_json(client, path, payload, expected_addr=cfg.bao_addr),
) if args.execute else {
**plan, "mode": "backend-free-preparation", "native_verified": False,
"claim_negatives_proved": False, "protected_operation_adoption_proved": False,
}
except Exception:
print(json.dumps({"native_verified": False, "failure": "preflight/login failed; details suppressed"}))
return 1
print(json.dumps(result, sort_keys=True))
return 0 if not args.execute or result["native_verified"] else 1
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -332,7 +332,7 @@ Acceptance:
id: SECRETS-WP-0008-T06 id: SECRETS-WP-0008-T06
status: wait status: wait
depends_on: [RPF-WP-0035-T02] depends_on: [RPF-WP-0035-T02]
blocking_reason: "Positive service-JWT login proof passed 2026-10-10 (SECRETS-WP-0012-T03, docs/evidence/2026-10-10-keycape-openbao-probe.json). Waits on railiance-platform closing RPF-WP-0035-T02 with the CCR-2026-0038 negative checks, then on routing live engine actions through service-jwt instead of bootstrap/env." blocking_reason: "Published JWT contract and positive login/revocation are delivered. Native KEY-WP-0039-T02 claim negatives, CCR-2026-0038 effective-policy/capability/expiry/use checks and MASON-WP-0009-T05 remain. Login-self-only grants no protected-operation authority: agree and implement separately reviewed task-scoped backend execution entitlement, then prove functional steady-state service-identity adoption without bootstrap/env fallback."
priority: medium priority: medium
state_hub_task_id: "d7bc8bdc-a0f8-5058-a640-374ef9859148" state_hub_task_id: "d7bc8bdc-a0f8-5058-a640-374ef9859148"
``` ```
@ -352,8 +352,10 @@ argv), then self-revokes. A JWT selection refuses `--bootstrap-token-file` and
does not read `BAO_TOKEN` on failure. `--bootstrap-token-file` is a named does not read `BAO_TOKEN` on failure. `--bootstrap-token-file` is a named
break-glass provider with `auth_break_glass` evidence. break-glass provider with `auth_break_glass` evidence.
The platform-owned OpenBao JWT mount/role is still unpublished, so auto keeps Historical 2026-09-02 state, superseded 2026-10-10: the platform-owned
bootstrap/env and this task remains `wait`. OpenBao JWT mount/role was then unpublished. The consumer contract is now
published; native negative/capability checks and functional task-scoped backend
execution adoption remain open, so T06 remains `wait`.
Inbox triage 2026-09-21. railiance-platform `29cccf8a` (2026-09-09): status, Inbox triage 2026-09-21. railiance-platform `29cccf8a` (2026-09-09): status,
not a contract. Their side is RPF-WP-0035-T02 (design not a contract. Their side is RPF-WP-0035-T02 (design
@ -549,3 +551,20 @@ T06 still depends on RPF-WP-0035-T02, whose current file remains wait for live
platform service registration/custody and scoped JWT provisioning plus native platform service registration/custody and scoped JWT provisioning plus native
login, denial and revocation evidence. Source tenant corrections are complete. login, denial and revocation evidence. Source tenant corrections are complete.
The workplan is now blocked; no new task or workplan was created. The workplan is now blocked; no new task or workplan was created.
### 2026-10-11 final gate preparation
Completed preparation record: `docs/service-jwt-final-acceptance.md` and
`tools/verify_service_jwt_boundary.py`; synthetic acceptance tests cover added
identity policy, omitted deny, metadata failure, failed cleanup, redirects,
environment proxies, bounded transport, complete typed evidence, stale cleanup
claims and mutually exclusive backend error/data responses. The tool
default is backend-free and cannot claim native success. It prepares the exact
applied CCR capability batch and preserves the eight-use cleanup budget.
The actual CLI consumes approval before supplying its selected backend token.
The current service token grants self endpoints only, and no delegated backend
entitlement is implemented. Configuring auto now would not prove functional
protected-action adoption. T06 stays wait; no provider default, policy, secret,
live registration or owner acknowledgment changed. Existing live owner tasks
retain all remaining acceptance and execution entitlement work.