key-cape/docs/openbao-negative-probe-clients.yaml
tegwick bae9f85160 KEY-WP-0039: probe registration packet and attended negative-probe script
T01 done: railiance-platform agreed the case list. The packet adds an
otherwise-valid expiry client and pins audience on the claim probes so each
token fails only the claim under test. The script prints one refused boolean
and refusal class per case, revokes any accepted token, and never counts a
transport failure as a refusal.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 779409@bnt-lap001
Assistant-Session: c658ca06-19b5-468d-be64-2985dc20bfd3
2026-10-10 13:18:24 +02:00

97 lines
4.1 KiB
YAML

# 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_ID>_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 invalid_client.
# 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/<date>-openbao-negative-probe.json