Reveal

Plaintext for a named operator — its own scope, mandatory attribution, audited before the response.

Operators editing credentials through the Internal Admin API see only masks — which is right until the day a key might be wrong and the only cure would be a blind overwrite. POST /internal/v1/admin/credentials/{credential_id}/reveal is the deliberate exception: decrypt one record and show it to a human.

curl -s -X POST "$CREDS_BASE/internal/v1/admin/credentials/$CRED_ID/reveal" \
  -H "Authorization: Bearer $REVEAL_TOKEN" \
  -H "X-Admin-Actor: panel:alice"

The response carries the decrypted credentials, the open configuration, the owner coordinates, and revealed_at.

The four deliberate restrictions

  1. Its own scope. provider-credentials.reveal — not implied by admin (edit ≠ see) nor by invoke (machines resolve, humans reveal). The auth-server registry grants it separately, to the panel only; see Service Plane Tokens.
  2. A named operator, mandatorily. Unlike other admin calls, a missing X-Admin-Actor is a 400. A reveal leaves nothing behind except its audit event; anonymous, that event would say only "some client read a secret" — worthless. Anonymous plaintext disclosure is rejected by construction.
  3. Audited before answered. The credential.revealed event (field names only, never values) is committed in its own transaction before the response is sent — the trace exists even if the response never arrives. The event is also exempt from audit retention purge: reveals are rare, human, and worth keeping.
  4. Its own scarce budget. Default 10/min per operator (Rate Limits) — far below the admin 60/min, so a stolen panel session cannot walk the whole store at pagination speed.

Semantics worth knowing

  • Works on records of any level and any status including disabled — a suspected-broken key is inspected exactly when it is off. Soft-deleted records are a 404: deletion means gone.
  • It is a POST, not a GET: a reveal is an audited action, not a cacheable resource. The id stays in the path; values travel only in the body, so no secret ever lands in an access log.
  • A ciphertext that will not decrypt is a 500 CREDENTIAL_DECRYPTION_FAILED, itself audited as credential.decryption_failed — that response is a fire alarm about the keyring, not a retry candidate.

Worked example, including the audit event it leaves: cookbook scenario 08.