---
title: "Credential brokering"
description: "End-to-end: a virtual key arrives, policy decides, the vault unwraps on the outbound hop, and no agent, worker, or dashboard ever holds a vendor secret."
---
Credential brokering is the whole of ADR-0004 in one sentence:
**The credential that hits Obol is never the credential that hits a vendor.**
Obol mints its own keys and brokers the customer's vendor credentials under
strict rules. It does not resell pooled vendor accounts, and it does not act as
a merchant of record.
## The path of one tool call
`Authorization: Bearer ob_live_…` on `/v1/*` or `/mcp`. The gateway resolves it
by SHA-256 hash from the Redis key cache, falling back to control. Revoked,
expired, environment-mismatched, and out-of-scope keys are rejected here; a
store failure is `NotReady`, never a pass. See
[/security/virtual-keys](/security/virtual-keys).
CEL decides visibility — `tools/list` never shows a tool the key cannot call.
Cedar decides permission, default-deny, with argument conditions. A destructive
tool in prod requires an approval unless policy explicitly grants it. All of
this is compiled data evaluated in-process, with no network hop per decision.
See [/policy/overview](/policy/overview).
The resolved upstream URL — an `mcp_remote` origin or an `openapi` base URL — is
checked before the outbound hop. A blocked upstream terminates the invocation
`upstream_blocked` and is never dispatched. See
[/security/egress-and-ssrf](/security/egress-and-ssrf).
`Vault::unwrap_checked` decrypts the connection's `EncryptedCredential` inside
the gateway process, refusing an expired credential before touching the KEK.
An unwrap failure terminates the invocation `credential_unavailable`; it never
falls through to an uncredentialed call. See [/security/vault](/security/vault).
`OutboundCredential::apply` writes the vendor credential onto the outbound
request — `Authorization: Bearer`, `Authorization: Basic`, a named header with an
optional prefix, or a query parameter — with the header value marked
`sensitive`. This is the only place header or query mutation happens.
Every tool call carries an Obol-minted idempotency key (`idt_` UUIDv7) and
produces a receipt naming the policy id, upstream id, vendor status, and the
evidence class of the route. See [/receipts/overview](/receipts/overview).
The plaintext exists between step 4 and step 5 and is dropped immediately after.
## What each party can see
| Party | Sees the Obol key | Sees the vendor secret |
|---|---|---|
| The agent or IDE | Yes — it presents it | No |
| The gateway process | Yes, as a hash | Yes, briefly, in memory only |
| Control plane | Only as a hash and `last4` | No — ciphertext envelopes only |
| The dashboard (`apps/web`) | No plaintext after mint | No |
| A connector worker | No | See the worker hand-off below |
| A catalog broker | No | Holds its own; Obol never imports or exports one |
| Logs and traces | `key_id`, hashes | `last4` only |
## Evidence is tier-dependent; the guarantee is not
Everything above — authentication, scoping, CEL visibility, Cedar permission,
approvals, budgets, environment separation, revocation at the Obol hop,
idempotency — happens upstream of every connector tier and is therefore
tier-independent.
What Obol can *prove afterwards* is not. A native route can be
`gateway_observed`; a federated catalog route cannot, because the socket the
gateway observed terminated at the broker rather than at the vendor. The receipt
always names the class. See [/receipts/evidence](/receipts/evidence).
## The worker hand-off, and its boundaries
`apps/connectors` ships nothing — one README, no package, no Dockerfile, no
image — and no deployment target defines a connector-worker service or sets
`OBOL_CONNECTORS_URL`. Every native connector with a reviewed pack compiles to
the in-process OpenAPI executor instead, which is what makes its transport
evidence `gateway_observed`. The `worker://` target and its hand-off are
described here because the code path exists in the gateway, not because
anything reaches it.
A `ToolTarget::Worker` call is an MCP `tools/call` to
`{OBOL_CONNECTORS_URL}/mcp/{worker}` over a per-call streamable-HTTP session
(`apps/gateway/crates/obol-mcp/src/worker/`). Two credentials cross that hop and
they do different jobs:
- `Authorization: Bearer ` — HS256, audience `obol-connectors`,
60-second TTL, minted per call. This authenticates *the gateway*, not the
customer and not the vendor.
- `x-obol-upstream-authorization` — the rendered vendor credential, present only
when the connection has one. It is produced by running
`OutboundCredential::apply` into a scratch header map, marked `sensitive`, and
passed as a custom header. The worker copies it onto its vendor request and
never persists it.
The body is the frozen `WorkerCallRequest` contract:
`{connection_id, workspace_id, tool, arguments, idempotency_key}`. ADR-0014
chose a header over the body deliberately — bodies get logged, buffered, and
snapshotted by frameworks far more often than headers, and MCP handlers see
bodies but not headers.
Hard boundaries on that header:
- It is never logged. The only log line for a worker call carries the worker,
connection, tool, idempotency key, and `last4`. A gateway test asserts the
string never appears in log output, and another asserts the header name is
mentioned nowhere in the tree outside `obol-mcp/src/worker`.
- It never appears on a remote MCP relay call — a relay test asserts its
absence.
- A worker holds no key material at rest and has no KEK or KMS dependency.
- Retries are bounded by transport certainty and the tool's declared vendor
idempotency binding, and reuse the same idempotency key. A post-send transport
failure is never retried, because the worker may have executed.
## Why workers are meant to be credential-blind
ADR-0024 decided the destination state and is accepted-not-implemented (parked
by ADR-0058). It states the rule plainly:
**ADR-0024's rule — not yet the code.** ADR-0024 requires that a worker never
see a vendor credential and never call a vendor, making trusted workers request
planners and response normalizers while the gateway performs all credentialed
vendor HTTP. **That protocol is not implemented.** The shipped gateway still
runs ADR-0014's hand-off and would pass the credential to the worker. Nothing
crosses that hop today, because no worker exists to receive it, but do not cite
this rule as a property the system currently has.
The reasoning is that redaction and no-at-rest rules reduce accidental leakage
but do not stop a compromised worker dependency from exfiltrating each live
credential. Under ADR-0024 a worker would return a `RequestPlan` — a declared
operation id, method, relative path, selected safe headers, and a bounded body,
never an absolute destination and never a credential — which the gateway would
validate against the pinned connector bundle before injecting the vaulted
credential and performing the HTTP itself. Worker-derived semantic fields would
stay `connector_attested`; HTTP status, digests, and gateway-selected response
fields would stay `gateway_observed`. A worker cannot assign trust, evaluate
Cedar, mint a receipt, or choose a verification conclusion.
ADR-0024 supersedes ADR-0014's credential hand-off and retry mechanics on paper.
The shipped gateway path is still ADR-0014's
`x-obol-upstream-authorization` header, so treat that header as legacy surface:
do not extend it, and do not add a worker that depends on it. A vendor whose
request signing Obol cannot express as an auth profile is a reviewed exception
requiring a new ADR, not a secret hand-off.
See [/connectors/trusted-workers](/connectors/trusted-workers) for the tier
decision and what a worker is for.
## What is prohibited outright
- Pooled vendor accounts resold through Obol's API.
- Storing a customer's full `sk_live_`. Control rejects any sealable secret
beginning with `sk_live_`, and the gateway rejects a Stripe API-key seal
beginning with `sk_live_` again on its own side.
- A shared broker OAuth application across tenants. A federated connection uses
the customer's own OAuth client, and activation fails if one broker account
identifier appears under more than one `workspace_id` (ADR-0030).
- Exposing a vendor secret to an agent, a worker config, a log, or the web app.
- Pass-through, where the agent holds the vendor key and Obol only logs.
See [/concepts/invariants](/concepts/invariants) for the full list.