--- 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.