Compare commits

...

2 commits

Author SHA1 Message Date
97dfa5ea8f Refuse login-only protected execution before consuming approval
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
2026-10-11 14:19:30 +02:00
4378ec1fc2 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
2026-10-11 14:11:59 +02:00
11 changed files with 977 additions and 18 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
the provider name and an accessor fingerprint only.
Until railiance-platform publishes that file, `--auth auto` keeps the named
bootstrap-file and `BAO_TOKEN` providers. `--auth service-jwt` fail-closes.
railiance-platform published the exact consumer file on 2026-10-10 at
`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`.

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
`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`,
`BAO_TOKEN`, or AppRole. Until the contract is published, `--auth auto` keeps
those named providers and `--auth service-jwt` fail-closes. See
`BAO_TOKEN`, or AppRole. The platform contract was published 2026-10-10. Configured `--auth auto`
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).
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,248 @@
# Task-scoped protected OpenBao execution — owner design
Design ID: `SECRETS-EXEC-20261011-v1`. Status: proposed, source preparation only.
Owning draft: `SECRETS-WP-0013`; adoption remains `SECRETS-WP-0008-T06`.
No native entitlement, owner assent or founder go is established here.
## The concrete gap and immediate correction
`cli._require_lane_approval` consumes the approved action before `_open_backend`.
`engine_auth.select_engine_auth` selects service-jwt for auto whenever its login
contract is configured. CCR-2026-0038 grants a 300-second, eight-use token with
exactly `secrets-engine-login-self`. It explicitly denies custody data/metadata,
policy/auth writes and child-token creation. Thus a valid approval can be
consumed before an ordinary apply/provision/exec fails for lack of backend power.
The staged correction checks **local execution readiness before claim/PDP/CAS**
for every caller about to access protected OpenBao work. Until a real reviewed
execution backend ships, service-jwt is rejected for those callers. Explicit
bootstrap/env providers retain their existing approval/stance gates; no fallback
or default changes. Service-auth login/self probes remain available. Local-only
compromise/reactivate records retain their existing path. A file/boolean claiming
entitlement cannot make the guard pass. Tests exercise the actual apply handler,
asserting zero claim/PDP/consume/login/apply calls for configured auto and missing
explicit service-jwt entitlement. This correction fixes early consumption; it
does **not** implement task-scoped protected work or complete T06.
## One initial execution surface
Implement `secrets-engine execute-ccr --catalog <id> --stage prod --request <file>`
as an explicit, metadata-only action. It admits one owner-reviewed Platform
`workload-kv-read` CCR with already-existing KV and auth mounts, exact generated
policy and exact Kubernetes or existing netkingdom OIDC role. It does not call
generic `apply_plan`: that function can manage AppRoles/mounts, which differs
from Platform's typed CCR applier. Provision, exec delivery, rotation, KV reads,
SecretIDs, token creation by the caller, mounting and arbitrary CLI commands
remain outside this first entitlement. Their backend obligations require their
own later exact action contracts, not reuse of this metadata allowance.
CCR38 and its role/policy stay byte-for-byte unchanged. The engine identifies
itself through that login-only service session; authentication does not create
authority for the metadata action. A new separately reviewed Platform
`task-execution-grant` CCR admits the founder-owned execution mediator and its
temporary exact two-path task token. Existing applied CCRs, including CCR38,
cannot be replayed to manufacture new native work. A real unexecuted, separately
approved metadata CCR and native success/negative/cleanup receipts are needed
for the first proof; none is selected or approved by this design.
## Exact request binding
Use the existing `authorization.build_action_request(entry, 'apply', ...)` shape
and canonical digest rules. Add a reviewed `context.execution_contract` object
before observing the claim or calling Check. Its fields are:
| Field | Binding |
| --- | --- |
| contract_version | `SECRETS-EXEC-20261011-v1` |
| operation_id | Owner-issued UUID, part of the request digest |
| operation | `metadata-apply-ccr`, closed enum |
| execution_actor | Verified `service:secrets-engine`, separate from initiating subject |
| ccr_id / ccr_immutable_sha256 | Exact unapplied owner CCR ID and immutable content digest |
| execution_grant_ccr_id / grant_immutable_sha256 | Exact reviewed task-token materialization and cleanup contract |
| catalog_sha256 | Exact non-secret approved catalog target specification |
| source_bundle | Absolute owner-resolved files and immutable SHA256 hashes for engine dispatcher, Platform mediator, typed CCR module and policy/auth source |
| backend_origin / cluster_uid | Exact reviewed estate OpenBao origin and railiance01 identity |
| operations | Ordered exact HTTP method, normalized path and canonical JSON/text payload hash for each read/write/readback; no user supplied body |
| operation_deadline | Upper bound no later than approval, PDP or task-token expiry |
| backend_budget | TTL at most 120s, at most twelve uses, no renew/child/wrap/unlimited uses |
| expected_before / expected_after | Owner-authenticated revision/state digests, never secret contents |
The action subject/type/purpose/tenant are the actual initiating principal's
verified identity and existing catalog purpose, not inferred from engine service
identity or arbitrary environment fields. Founder-attended invocation names that
principal and the representation explicitly. The mediator refuses absent owner
identity correspondence; a caller-authored subject string is not evidence.
The signed/served approval must bind the same approval-free execution context;
the PDP must return the exact canonical request and matching approval digest.
Publish and pin the updated access-engine policy package/version that evaluates
this context; an older package accepting only a catalog ID cannot authorize it.
No package default or implicit semantic acceptance is assumed. Approval identity,
decision ID and canonical request digest travel together in a non-secret request
descriptor. They are provenance until freshly validated and CAS-consumed.
CCR immutable digests exclude only explicitly owner-defined append-only review/
evidence fields; policy bytes, role bytes, typed mutation body, target coordinates,
grant/deadline and source revisions are included. Approval IDs added after
issuance do not change their own binding. The mediator independently reconstructs
all operation bodies from owner sources and compares the resulting digest list.
It accepts no caller ACL template, path override, shell argv or raw JSON body.
## Execution mediation and consume ordering
Use the existing attended Platform front door, invoked by the founder, to run a
new fixed Platform owner mediator. No daemon, broad new service or new bearer
delivery to agents is required. The caller engine prepares a non-secret request
and receives a non-secret receipt. The founder-held admin credential exists only
inside the reviewed Platform child, through the existing attended route. It is
never exported to the caller. Neither local receipt files nor user-authored
`approved` labels authorize the child.
The Platform child runs the reviewed Secrets Engine authorization/consume code
at the last responsible execution boundary; the CLI preparation path does not
consume early and then hand an untrusted receipt to a more privileged process.
The backend mediation contract must preserve engine ownership of lifecycle
validation while Platform owns its OpenBao credential and typed native primitive.
1. Load pinned sources and exact non-secret request; check typed CCR/grant owner
approval, catalog/subject/operation correspondence, source/state expectations,
owner-only execution state directory, supported method/body allowlist and
local clock/deadline. No protected OpenBao call occurs yet.
2. Atomically reserve the operation in a dedicated engine execution ledger,
uniquely keyed by `approval_id + request_digest`; the operation UUID is inside
the digest. Bind the authenticated caller, sources and deadline. This is
execution state, not the audit outbox or a local evidence-presence gate.
Multiworker locking uses SQLite `BEGIN IMMEDIATE` and uniqueness constraints;
only the reservation creator may proceed.
3. Use `authorize_action` / approval-claim validation / `check_decision` / exact
envelope validation, with pinned policy package and trusted application time.
Recheck freshness after network calls. Require served human control for the
production grant; missing claims, unreachable PDP or mismatched binding refuse.
4. Call `require_production_consume` once for that exact binding. Require
confirmed matching consumption. Persist `consume-confirmed` before any
protected OpenBao login, custody read, capability check or mutation. A consume
timeout is `unknown`; never treat it as absence or automatically retry.
A consumed/idempotent response without the same live local admission is not
permission to execute again. Retrying a completed operation returns only its
existing receipt; an incomplete/unknown operation requires owner reconciliation
and a fresh exact approval for any additional backend action.
5. After consume, perform the existing attended in-memory client-secret source
and CCR38 service login without provider fallback. Verify its exact login-self
policy/TTL/uses and service identity; keep this session self-only. Failure
consumes the trial without metadata effects; no implicit re-login or retry.
6. In the Platform child, use the separately authorized founder-held identity
only to materialize the reviewed exact ephemeral task policy/token. OpenBao's
token parent remains platform-owned; the CCR38 token cannot mint it. The grant
binds every policy/token create/revoke/delete step to this operation and its
cleanup. Do not give a standing `sys/policies/acl/*`, `auth/*/role/*` or
`auth/token/create` grant to the engine service.
7. Recheck decision, approval and operation deadlines before **each** protected
request. The task token is nonrenewable, no default/identity policies and
grants only read/create/update on the one approved ACL policy path and the
one approved workload auth-role path, plus revoke-self. No child token, mount
administration, KV data/metadata, SecretID or sibling role is admitted.
OpenBao ACL narrows endpoint access; it cannot restrict the HCL/role payload.
Only the mediator's independently reconstructed body equality enforces that.
8. Use an injected restricted backend adapter for the exact ordered native
calls. Compare current expected state; write only the approved generated body,
read it back, and compare it. No `OpenBaoClient._run_ok` raw failure reaches
receipts: capture/discard backend output and use closed reason codes. A generic
403/timeout or “already exists” is not successful verification.
9. Finally self-revoke the task token and login-self token, then retire only the
exact per-operation grant policy under founder-owned cleanup authority.
Record confirmed revocation/absence separately from native apply status.
Policy retirement alone does not prove issued-token revocation.
Task-token use budget is declared before issuance: two before-state reads, two
writes, two readbacks and one self-revoke (seven uses), plus at most four explicit
metadata capability checks and one reserved cleanup use (twelve total). Failed
backend calls count; never retry to evade limits. Task TTL is
`min(120s, approval_remaining, decision_remaining, operation_remaining)` minus
the declared clock uncertainty/cleanup reserve. Reject if fewer than 30s remain
for planned requests. No renewal or remint extends the operation. CCR38 keeps its
own 300s/eight-use limit; login-self checks and revoke fit its separate budget.
## Failure, replay and recovery
Ledger states: `reserved → consume-confirmed → authenticating → grant-issued →
applying → verified`, followed by a separate `cleanup-confirmed` or
`cleanup-unconfirmed` result. Every external call is durably recorded as about
to execute before the call, then recorded complete afterward. Crash between
those steps is `unknown` rather than presumed failure. Concurrent duplicate
requests never both reach consume or OpenBao.
An operation can end refused before consumption, consumed-without-backend-work,
partially-applied, backend-outcome-unknown, verified-cleanup-unconfirmed or
verified-cleanup-confirmed. No incomplete state permits blind reapplication.
Cleanup continues when an action deadline expires, under the original explicit
cleanup obligation, without new business writes. Read/repair of changed target
state requires a fresh exact owner-reviewed recovery approval; no old snapshot
overwrites concurrent owner edits. Hard interruption uses the Platform owner's
bounded revocation/cleanup procedure with protected accessor custody. Ledger
and evidence carry only fingerprints/metadata; the raw accessor/token never
leaves owner memory or the separately admitted owner-private recovery channel.
Revocation failure blocks owner acceptance and T06 closure, even if TTL will
eventually expire. Emit non-secret atomic local lifecycle events/outbox records
at the ledger boundaries; audit-core outage does not suppress cleanup. Do not
claim an empty archive proves no effects, or that local ledger state is founder
approval. Owner acceptance requires native before/after equality, denied sibling
actions and verified cleanup under the same exact trial.
## Implementation hooks and closure tests
- `engine_auth.py`: staged readiness guard now. Later separate identity-session
and execution-adapter objects; generic service-jwt client remains self-only.
- `cli.py`: staged guard passed through backend-action approval callers now.
New explicit execute-ccr preparation/mediated dispatch later; ordinary apply
does not acquire new rights merely because the login contract is configured.
- `authorization.py`, `approval_consume.py`: execution context participates in
both approval-free binding and full PDP digest; owner child returns the actual
`ConsumedApproval`, not only an `AuthorizedAction` ID.
- New `execution_contract.py`, `execution_ledger.py`, `mediated_backend.py`:
strict closed schema, deterministic compiler, separate admission state,
pinned owner-native adapter and non-secret outcome types. Ordinary subprocess
command execution is not an implementation of this adapter.
- Platform owns task-execution-grant CCR/typed validator/token materializer/
fixed execution child/recovery hooks. Approval Engine/PDP owners acknowledge
the request+claim/package correspondence; secrets-engine cannot assent for them.
Counterexamples required before shipping: login-only auto provider consumes
nothing; stale/forged grant file changes nothing; same name with changed HCL or
role body refuses; wrong method/sibling policy/role/KV/SecretID/child-token/mount
write refuses; changed source/caller/tenant/catalog/deadline alters the bound
request and refuses; consume conflict/unavailability/timeout produces zero
backend calls; idempotent approval consumption never causes a second execution;
concurrent calls admit one worker; postconsume login failure never falls back;
expiry during a multi-call operation stops new mutations but permits scoped
cleanup; readback mismatch/transport failure is incomplete; revoke failure
never passes; partial apply and crash after write require explicit recovery.
Current staged readiness regression suite: eight new checks plus 28 existing
auth, consume and privileged-evidence checks pass (36 total). No native action
was performed. Full entitlement implementation, policy-package publication,
independent Platform/Secrets Engine/identity/approval/PDP acknowledgement and
exact founder-scoped native proof remain required. A successful metadata trial
alone would not cover value provision or exec delivery.
## Existing commands and the next contract they actually need
| Existing operation | Current login-only outcome | Contract required after the metadata pilot |
| --- | --- | --- |
| apply / apply_plan | Readiness refusal before consume; no backend | A separate exact policy/AppRole/mount structure compiler and grant for each admitted PlanAction. The workload-kv-read execute-ccr pilot does not implement these generic AppRole/mount calls. |
| provision / rotate | Readiness refusal before consume; no KV writes | Exact KV v2 data path, declared field set, expected CAS version, no sibling changes, owner-held opaque input handle, confirmed new version and separately admitted revoke/cleanup. Never expose secret plaintext or guessable secret-value hashes in request/evidence. |
| verify positive/negative | Readiness refusal before consume; no custody reads | Metadata capability probe plus, only when required, exact declared field-presence read through a restricted owner adapter. Return booleans; malformed/denied/transport error is inconclusive, not absence. Independent negative identity and exact deny paths are separate bindings. |
| exec / wrap | Readiness refusal before consume; no read/injection/wrap | Exact custody path/field, signed executable/cwd/destination/exec-owner binding and duration, no user arbitrary shell, owner-mediated read and approved process-local delivery, or exact one-use wrap creation/unwrap/expiry; self revoke and injected material cleanup. |
| handoff | Readiness refusal before consume; no role-id/SecretID mint | Exact existing AppRole and single-use SecretID creation, bounded TTL/uses and owner-admitted delivery; native role-id lookup/SecretID mint/login/revoke budget and no caller standing credential. |
| revoke / native suspend/deactivate | Readiness refusal before consume; no backend revocation/deletion | Exact policy/role/lease/accessor target and expected state, admitted lifecycle effect, rollback/custody preservation and denial readback. Token/lease handles remain owner-private; receipt fingerprints cannot authorize another handle. |
| session revoke | Readiness refusal before its backend; legacy stance remains | Exact already-held protected handle, typed token/lease revocation and independent subject/authority binding; no arbitrary accessor or lease-prefix wildcard. |
| destroy | Existing live destroy refusal remains | Exact destructive dual-control contract and custody/backup/version proof; metadata pilot supplies neither. |
| compromise / reactivate local state | Existing local state gate remains | No new OpenBao entitlement; these local records do not establish actual native revocation/reactivation. |
| service-auth probe-openbao | Existing explicit self-only identity trial | CCR38 positive/negative/lifetime/revoke proof, with no implied entitlement to any catalog lane. |
Each subsequent contract needs its own owner-reviewed compiler, request/PDP
package binding, method/body enforcement, consume admission, TTL/use budget,
failure/recovery rules and native counterexamples. No success in the initial
metadata pilot unblocks these rows by default. Full T06 adoption requires the
actual selected engine production workflow to run through the corresponding
reviewed entitlement, with native effect, denial/no-fallback and cleanup proof.

View file

@ -43,7 +43,11 @@ from secrets_engine.config import Config, repo_root
from secrets_engine.decisions import require_approved, resolve_decision
from secrets_engine.errors import BackendError, DecisionError, SecretsEngineError
from secrets_engine.evidence import EvidenceWriter, PrivilegedActionEvidence
from secrets_engine.engine_auth import login_service_jwt, select_engine_auth
from secrets_engine.engine_auth import (
login_service_jwt,
select_engine_auth,
require_backend_execution_ready,
)
from secrets_engine.lane_state import (
load_lane_state,
operation_state,
@ -111,6 +115,7 @@ def _require_lane_approval(
evidence: PrivilegedActionEvidence | None = None,
*,
fields: tuple[str, ...] = (),
auth_args: object | None = None,
):
"""Apply published PEP stance, resolve lane approval, then CAS-consume.
@ -125,6 +130,8 @@ def _require_lane_approval(
# Steps 1 and 2 first: a validated claim and decision are what make the
# unreachable-engine residue inapplicable. Absent configuration returns
# None and production stays closed exactly as before.
if auth_args is not None:
require_backend_execution_ready(cfg, auth_args, action or "unknown")
authorization = authorize_action(
cfg, entry, action or "unknown", None, fields=fields,
policy_targets=(entry.policy_name,) if entry.has_delivery_auth else (),
@ -303,7 +310,7 @@ def cmd_apply(cfg: Config, args) -> int:
return 0
with _privileged_evidence(cfg, entry, "apply") as evidence:
decision = _require_lane_approval(cfg, entry, "apply", evidence)
decision = _require_lane_approval(cfg, entry, "apply", evidence, auth_args=args)
evidence.mark_approved(decision)
plan = build_plan(
entry, args.stage, decision_id=decision.id if decision else ""
@ -331,7 +338,8 @@ def cmd_provision(cfg: Config, args) -> int:
f"lane '{entry.id}' is stage '{entry.stage}', not '{args.stage}'"
)
decision = _require_lane_approval(
cfg, entry, "provision", evidence, fields=(field,) if field else ()
cfg, entry, "provision", evidence, fields=(field,) if field else (),
auth_args=args,
)
evidence.mark_approved(decision)
require_provision_state(cfg.evidence_dir, entry.id)
@ -368,7 +376,8 @@ def cmd_rotate(cfg: Config, args) -> int:
f"lane '{entry.id}' is stage '{entry.stage}', not '{args.stage}'"
)
decision = _require_lane_approval(
cfg, entry, "rotate", evidence, fields=(field,) if field else ()
cfg, entry, "rotate", evidence, fields=(field,) if field else (),
auth_args=args,
)
evidence.mark_approved(decision)
with _open_backend(cfg, args, evidence) as client:
@ -410,7 +419,8 @@ def cmd_verify(cfg: Config, args) -> int:
},
) as evidence:
decision = _require_lane_approval(
cfg, entry, "verify", evidence, fields=tuple(fields)
cfg, entry, "verify", evidence, fields=tuple(fields),
auth_args=args,
)
evidence.mark_approved(decision)
with _open_backend(cfg, args, evidence) as client:
@ -482,7 +492,7 @@ def cmd_handoff(cfg: Config, args) -> int:
raise ProvisioningError(
f"lane '{entry.id}' is {entry.kind}; handoff needs auth-capability"
)
decision = _require_lane_approval(cfg, entry, "handoff", evidence)
decision = _require_lane_approval(cfg, entry, "handoff", evidence, auth_args=args)
evidence.mark_approved(decision)
require_delivery_state(cfg.evidence_dir, entry.id, "handoff")
with _open_backend(cfg, args, evidence) as client:
@ -522,7 +532,7 @@ def cmd_wrap(cfg: Config, args) -> int:
with _privileged_evidence(
cfg, entry, "wrap", detail={"ttl": args.ttl, "out_file": args.out}
) as evidence:
decision = _require_lane_approval(cfg, entry, "wrap", evidence)
decision = _require_lane_approval(cfg, entry, "wrap", evidence, auth_args=args)
evidence.mark_approved(decision)
require_delivery_state(cfg.evidence_dir, entry.id, "wrap")
with _open_backend(cfg, args, evidence) as client:
@ -576,7 +586,8 @@ def cmd_exec(cfg: Config, args) -> int:
))
# require approval + readiness before running.
decision = _require_lane_approval(
cfg, entry, "exec", evidence, fields=(field,) if field else ()
cfg, entry, "exec", evidence, fields=(field,) if field else (),
auth_args=args,
)
evidence.mark_approved(decision)
require_delivery_state(cfg.evidence_dir, entry.id, "exec")
@ -598,7 +609,8 @@ def cmd_exec(cfg: Config, args) -> int:
},
))
lane_decision = _require_lane_approval(
cfg, lane, "exec", lane_evidence, fields=(lane_field,)
cfg, lane, "exec", lane_evidence, fields=(lane_field,),
auth_args=args,
)
lane_evidence.mark_approved(lane_decision)
require_delivery_state(cfg.evidence_dir, lane.id, "exec")
@ -646,6 +658,7 @@ def cmd_session_revoke(cfg: Config, args) -> int:
detail={"handle_kind": kind},
)
with evidence:
require_backend_execution_ready(cfg, args, "session-revoke")
stance = apply_unreachable_engine_stance(cfg, stance_entry, "session-revoke")
evidence.mark_stance(stance)
handle = read_strict_token_file(
@ -726,7 +739,7 @@ def cmd_revoke(cfg: Config, args) -> int:
with _privileged_evidence(
cfg, entry, "revoke", detail={"operation": plan.operation}
) as evidence:
decision = _require_lane_approval(cfg, entry, "deactivate", evidence)
decision = _require_lane_approval(cfg, entry, "deactivate", evidence, auth_args=args)
evidence.mark_approved(decision)
with _open_backend(cfg, args, evidence) as client:
result = apply_lifecycle_plan(client, plan)
@ -831,7 +844,7 @@ def cmd_lifecycle(cfg: Config, args) -> int:
"live destroy is disabled until an exact-action destruction "
"approval contract is available; use --dry-run to inspect targets"
)
decision = _require_lane_approval(cfg, entry, args.operation, evidence)
decision = _require_lane_approval(cfg, entry, args.operation, evidence, auth_args=args)
evidence.mark_approved(decision)
with _open_backend(cfg, args, evidence) as client:
result = apply_lifecycle_plan(client, plan)

View file

@ -156,3 +156,21 @@ def login_service_jwt(cfg: Any) -> ScopedTokenSession:
finally:
# Drop the KeyCape JWT from this frame; OpenBao verifies the signature.
del service_jwt
def require_backend_execution_ready(cfg: Any, args: Any, action: str) -> AuthSelection:
"""Reject a login-only provider before approval consume or backend work.
CCR-2026-0038 authenticates identity; it grants no protected lane action.
A configured login contract is not an execution entitlement. No mediated
execution backend is shipped, so no local file/flag may declare it ready.
Named bootstrap/env providers retain their existing separate gates.
"""
selection = select_engine_auth(cfg, args)
if selection.provider == "service-jwt":
raise BackendError(
"service-jwt protected execution is not ready: CCR-2026-0038 is "
"login-only; no reviewed task-scoped execution backend is shipped. "
"Approval was not consumed and no OpenBao call was made"
)
return selection

View file

@ -0,0 +1,71 @@
from types import SimpleNamespace
import pytest
from secrets_engine import cli
from secrets_engine.engine_auth import require_backend_execution_ready
from secrets_engine.errors import BackendError
def cfg(contract=None):
return SimpleNamespace(openbao_jwt_login_file=contract)
@pytest.mark.parametrize('provider,contract', [('service-jwt', None), ('auto', '/non-secret/login.yaml')])
def test_login_only_refused_before_claim_consume_or_backend(provider, contract, monkeypatch):
events = []
for name in ['authorize_action', 'require_production_consume', 'login_service_jwt']:
monkeypatch.setattr(cli, name, lambda *a, _name=name, **k: events.append(_name))
monkeypatch.setattr(cli.OpenBaoClient, 'resolve', lambda *a, **k: events.append('backend'))
args = SimpleNamespace(auth=provider, bootstrap_token_file=None)
with pytest.raises(BackendError, match='Approval was not consumed'):
cli._require_lane_approval(cfg(contract), SimpleNamespace(stage='prod'), 'apply', auth_args=args)
assert events == []
@pytest.mark.parametrize('provider,bootstrap', [('env', None), ('bootstrap', '/private/explicit-token')])
def test_named_legacy_provider_retained(provider, bootstrap):
args = SimpleNamespace(auth=provider, bootstrap_token_file=bootstrap)
result = require_backend_execution_ready(cfg('/published/login.yaml'), args, 'apply')
assert result.provider == provider
assert result.break_glass is (provider == 'bootstrap')
def test_auto_with_contract_and_bootstrap_never_falls_back():
with pytest.raises(BackendError, match='no fallback'):
require_backend_execution_ready(cfg('/published/login.yaml'),
SimpleNamespace(auth='auto', bootstrap_token_file='/private/file'), 'apply')
def test_readiness_cannot_be_self_asserted():
c = cfg('/published/login.yaml')
c.execution_entitlement_file = '/pretend/approved.json'
c.service_jwt_execution_ready = True
with pytest.raises(BackendError, match='no reviewed task-scoped'):
require_backend_execution_ready(c, SimpleNamespace(auth='auto', bootstrap_token_file=None), 'apply')
def test_self_probe_auth_selection_remains_service_jwt():
from secrets_engine.engine_auth import select_engine_auth
assert select_engine_auth(cfg('/published/login.yaml'),
SimpleNamespace(auth='auto', bootstrap_token_file=None)).provider == 'service-jwt'
def test_real_apply_handler_refuses_before_consumption(tmp_path, monkeypatch):
import copy
from secrets_engine.catalog import validate_entry
from secrets_engine.config import Config
from tests.test_catalog import VALID
entry = validate_entry(copy.deepcopy(VALID))
configuration = Config(catalog_dir=tmp_path, policy_dir=tmp_path,
evidence_dir=tmp_path / 'evidence', hub_url='', bao_addr='http://127.0.0.1:8200',
topic_id='fixture', openbao_jwt_login_file=tmp_path / 'login-contract.yaml')
monkeypatch.setattr(cli, '_resolve_lane_and_decision', lambda *a: entry)
calls = []
for name in ['authorize_action', 'require_production_consume', 'login_service_jwt', 'apply_plan']:
monkeypatch.setattr(cli, name, lambda *a, _name=name, **k: calls.append(_name))
args = SimpleNamespace(ref=entry.id, stage=entry.stage, dry_run=False,
auth='auto', bootstrap_token_file=None)
with pytest.raises(BackendError, match='Approval was not consumed'):
cli.cmd_apply(configuration, args)
assert calls == []

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
status: wait
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
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
break-glass provider with `auth_break_glass` evidence.
The platform-owned OpenBao JWT mount/role is still unpublished, so auto keeps
bootstrap/env and this task remains `wait`.
Historical 2026-09-02 state, superseded 2026-10-10: the platform-owned
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,
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
login, denial and revocation evidence. Source tenant corrections are complete.
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.

View file

@ -0,0 +1,65 @@
---
id: SECRETS-WP-0013
type: workplan
title: "Design and prove task-scoped protected OpenBao execution"
domain: infotech
repo: secrets-engine
status: proposed
flavor: planning
owner: codex
topic_slug: netkingdom
created: "2026-10-11"
updated: "2026-10-11"
depends_on: []
---
Source-staged draft only; no owner acceptance or native authority inferred.
Existing SECRETS-WP-0008-T06 remains open for actual adoption. CCR38 is an
unchanged identity-only login and is not a protected execution entitlement.
## Define the exact mediated execution contract
```task
id: SECRETS-WP-0013-T01
status: progress
priority: high
```
Design SECRETS-EXEC-20261011-v1 in docs/task-scoped-openbao-execution-contract.md
binds action, target, payload/source digests, initiating identity, exact PDP/CAS,
owner-held ephemeral two-path token, bounded TTL/uses, durable admission and
cleanup. Independent Platform/Secrets Engine/identity/approval/PDP owner returns
are required. No foreign review is written by this draft.
## Prevent consumption without execution readiness
```task
id: SECRETS-WP-0013-T02
status: progress
priority: high
```
Staged local fail-closed guard rejects service-jwt protected actions before
approval consumption because no reviewed task-execution backend exists. Named
bootstrap/env paths and login-self probes remain distinct. Eight new readiness
tests plus 28 existing auth/consume/evidence checks pass (36 total). Owner review,
publication and native checkout verification remain pending; not marked done.
## Implement and prove one native metadata action
```task
id: SECRETS-WP-0013-T03
status: wait
depends_on: [SECRETS-WP-0013-T01, RPF-WP-0035-T12]
wait_reason: "Wait for independent execution-contract acceptance and Platform task-scoped entitlement design/materialization, then exact founder native go."
priority: high
```
After T01 independent owner acceptance, implement the narrow execute-ccr surface,
Platform task-grant mediator, bound policy package and admission ledger. Select
an actual unexecuted approved metadata CCR, exact founder go and attended owner
custody. Prove real protected effect, sibling/body/refusal/replay counterexamples
and confirmed revocation/cleanup. No applied CCR38 replay or broad default flip.
Native metadata proof does not certify provision/exec delivery or all T06 scope.
2026-10-11 source revision v2: qualified T03 dependencies point to the owning design and Platform execution-entitlement task. WP13 owns the missing task-scoped execution seam; WP8-T06 retains production adoption and its full workflow proof. Source publication and fixture checks do not imply native acceptance. Original v1 stage remains unchanged.