route402 holds third-party API keys. That makes the credential vault the most security-sensitive thing here, so the rules are strict and few.
Write-only, everywhere
Once stored, a credential cannot be read back:
- No MCP tool returns it.
set_upstream_authanswers with a keyed digest and nothing else. - The console does not display it, not even masked. There is no "reveal" button because there is no read path to put behind one.
- No API endpoint returns it. The single internal endpoint that hands the
wrapped blob to the gateway is authenticated with a secret derived from
VAULT_MASTER_KEY, which only the gateway holds.
An agent that can read back a customer's API key is a breach waiting to happen. The safest way to build that is to never build the read path.
If you lose the credential, you cannot recover it from here. Get a new one from your upstream and save it again.
How it is stored
Envelope encryption. Each credential gets a fresh 256-bit data key; the data
key is itself encrypted under a key derived from VAULT_MASTER_KEY.
credential --AES-256-GCM--> ciphertext
data key --AES-256-GCM--> wrapped data key (under the master key)
Rotating the master key re-wraps data keys without touching the ciphertexts. Losing it loses every stored credential — there is no backdoor.
How it is injected
At call time the gateway decrypts in memory, adds the credential to the outbound request, and discards it. It is never logged, never included in an error message, and never returned in a response.
Three placements are supported:
| Placement | What the upstream receives |
|---|---|
bearer |
Authorization: Bearer |
header |
|
query |
? appended to the URL |
Egress control
A publisher supplies an arbitrary upstream URL and we then make requests to it.
Without a guard, "wrap my API" becomes "fetch http://169.254.169.254/ for
me".
So every upstream is checked at creation and again at call time, because DNS can change in between:
http://andhttps://only.- The hostname must resolve, and every address it resolves to must be public. A record that returns one public and one private address is still a pivot.
- Private, loopback, link-local, CGNAT and multicast ranges are refused, as are
localhostand.internalnames. - Credentials embedded in the URL are refused — store them properly instead.
- Redirects are not followed, because a redirect can leave the checked host.
EGRESS_BLOCK_PRIVATE_RANGES=false disables the resolution checks. It exists
for local development against a service on your own machine. Do not set it in
production.
Audit
Every write, rotation and removal is recorded with the actor that performed it — a console session or a specific MCP client id.