Overview
A virtual key is a live, governed resource, not a line of YAML. With the key_management: block enabled, you mint, revoke, and rotate inbound keys at runtime through an admin API. Each change takes effect on the next request without a reload, because every request resolves its key through a cache and then a store. Inbound keys are hashed at rest; upstream provider credentials can be encrypted at rest.
This is the runtime layer on top of the static credentials: block. The static block still works and lowers into the same store as config-sourced records.
The block
proxy:
key_management:
enabled: true
store:
backend: embedded # embedded | redis | secrets_manager
path: /var/lib/sbproxy/keystore.redb
cache:
ttl_secs: 60
tier: none # none | redis | mesh
crypto:
pepper: env:SBPROXY_KEY_PEPPER # HMAC key for inbound hashing
master_key: env:SBPROXY_KEY_MASTER # envelope key for upstream creds
failure_mode_allow: false # fail closed when the store is down
The security model
Two kinds of secret, two treatments. Inbound virtual keys are hashed, never stored in a form you can read back. The at-rest verifier is HMAC-SHA256(secret, pepper); the server pepper means a stolen store is useless without it. A minted token looks like sk-<key_id>-<secret>: the key_id is a public prefix, the secret is shown once and never stored. Verification is constant-time.
Upstream provider credentials are encrypted, because the proxy has to present them to the provider. Use a vault reference (vault://, awssm://, gcpsm://) resolved at use, or an AES-256-GCM envelope: a per-record data key encrypts the secret, then is wrapped under a key derived from the master_key, so you can rotate the master without re-encrypting every payload.
By default the plane fails closed. If the store cannot be reached, a request carrying a virtual key is denied.
The admin API
Mounted on the existing admin server, under the same bind and basic auth. The plaintext token comes back exactly once on create and rotate.
| Method and path | Effect |
|---|---|
POST /admin/keys | Mint a key (token shown once) |
GET /admin/keys | List keys (no secrets) |
PATCH /admin/keys/{id} | Update limits, budget, models, attribution |
POST /admin/keys/{id}/revoke | Mark revoked (terminal, instant) |
POST /admin/keys/{id}/rotate | Rotate with a grace window |
POST /admin/credentials | Create an upstream credential |
Revoke is instant: the next request with that key is denied. Rotation mints a fresh secret for the same key_id and keeps the prior secret valid for a grace window, so a client fleet can pick up the new token before the old one stops working.
OIDC and JWT
If your callers authenticate with an OIDC or JWT identity instead of a bearer key, set oidc_claim_map.claim_field to the claim whose value names a key record. After the token is verified, the claim value resolves the record and its policy applies, so a bearer key and an OIDC identity converge on the same record and the same limits.
Across a fleet
For multiple replicas, set cache.tier: redis or cache.tier: mesh. The mesh tier keeps the key plane coherent across the cluster with no external Redis in the path; see Mesh Clustering.