key-cape/docs/openbao-negative-probe-clients.yaml

98 lines
4.1 KiB
YAML
Raw Normal View History

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