# Upstream credentials

> How your API key is stored, injected, and why nothing can read it back.

Source: https://route402.dev/docs/credentials

---

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_auth` answers 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 <value>` |
| `header` | `<param_name>: <value>` |
| `query` | `?<param_name>=<value>` 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://` and `https://` 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
  `localhost` and `.internal` names.
- 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.
