# Non-secret request packet for KEY-WP-0039: temporary probe registrations whose # tokens the OpenBao role secrets-engine-openbao (auth/keycape-services) must # refuse, for CCR-2026-0038 (railiance-platform RPF-WP-0035-T02). # # This is not executable authorization. Each probe is a copy of the live # secrets-engine-openbao registration (config/service-clients.example.yaml) with # exactly one property changed. `audience` is pinned to secrets-engine-openbao # wherever aud is not the property under test: a service token's aud is the # clientId unless audience is set (token.go, accessAudience), so without the pin # every probe would also fail on aud and prove nothing about its own claim. # # The secrets-engine-openbao registration itself is not changed. reference: clientId: secrets-engine-openbao aud: secrets-engine-openbao sub: service:secrets-engine tenant: tenant:platform roles: [secrets-engine] scope: openbao:login principal_type: service common: grantTypes: ["client_credentials"] clientType: confidential tokenLifetime: "1m" # the minimum KeyCape accepts # Each secretRef names an env var that exists only for the attended session. # railiance-platform's condition (a), 2026-10-10: no OpenBao custody. secretRefPattern: "env:KEYCAPE__CLIENT_SECRET" probes: - case: aud clientId: openbao-probe-aud differs: {aud: openbao-probe-aud} # no audience pin expect: refuse expected_class: claim_mismatch - case: sub clientId: openbao-probe-sub audience: secrets-engine-openbao differs: {serviceSubject: "service:openbao-probe"} expect: refuse expected_class: claim_mismatch - case: tenant clientId: openbao-probe-tenant audience: secrets-engine-openbao differs: {tenant: "tenant:openbao-probe"} expect: refuse expected_class: claim_mismatch - case: roles clientId: openbao-probe-roles audience: secrets-engine-openbao differs: {roles: ["openbao-probe"]} expect: refuse expected_class: claim_mismatch - case: scope clientId: openbao-probe-scope audience: secrets-engine-openbao differs: {allowedScopes: ["openbao:probe"]} expect: refuse expected_class: claim_mismatch - case: expired clientId: openbao-probe-expiry audience: secrets-engine-openbao # Identical to the reference in every claim, so expiry is the only defect. # This makes it a second credential that can log in as secrets-engine until # it is removed. That exposure is bounded by the session and is the reason the # probe clients go in the same session. A token from one of the other probes # cannot stand in: it already fails a claim, so its refusal would not show # that expiry was checked. differs: {} present_after: "exp + OpenBao expiration_leeway (default 150s) + 10s" expect: refuse expected_class: expired existing_tokens: # These differ from the reference in several claims, so they prove refusal but # not which bound claim refused. The receipt records whatever class OpenBao reports. - case: principal_type source: a human access token from an attended `keycape login` expect: refuse - case: coding_agent source: codex-railiance-platform (role coding-agent), if registered live expect: refuse session: # One attended operator session, run by the owner of the live KeyCape client # registry (net-kingdom). Order: # 1. Generate the six probe secrets in the session. Write them to a transient # Kubernetes Secret bound to Deployment sso/keycape, never to OpenBao. # 2. Add the probe registrations; KeyCape restarts. # 3. Run scripts/openbao_negative_probe.py with the secrets on stdin. # 4. Remove the registrations and the transient Secret; KeyCape restarts. # Verify that client_credentials for each probe returns HTTP 401 invalid_profile_usage with feature client_id. # Each restart drops in-flight logins, so pick a quiet window. # The transient Secret is the only place the values exist outside process # memory. KeyCape resolves secretRef from its environment at startup, so a # copy that lives only in memory cannot reach it. receipt: docs/evidence/-openbao-negative-probe.json