RavensPort

Calling the proxy

Every request — routes and funnels alike — must present the proxy key of the endpoint it is calling. There is no key for the proxy as a whole: each route carries its own, each funnel carries its own, and a key opens nothing but the endpoint it was issued for.

Copy a route's key from its row on the Routes tab (select the route to open its editor), and a funnel's from the panel under the MCP Funnel tab.

curl -H "X-Proxy-Key: <this-route's-key>" http://127.0.0.1:5559/app/my-service/foo

The header is the only place the key is read from. A ?proxy_key= query parameter used to be accepted as well, for clients that cannot set headers (browser EventSource, some MCP SSE transports). It is now ignored: a URL ends up in the upstream's access log, in every intermediary's, in browser history, and in the Referer of anything the response loads, and this key is the whole of the proxy's authorization. A request carrying only the parameter gets 403, and the activity log says which parameter it saw so the cause is not a guess. A client that cannot set a header cannot use RavensPort.

The key is stripped before forwarding — the header, and the proxy_key parameter if a caller still sends one — so it never reaches the upstream's access log or this app's activity log. Your own headers and parameters pass through untouched.

Anything without a valid key gets 403: a wrong key, another endpoint's key, an expired key, and a path belonging to no route or funnel all answer the same way, so the reply cannot be used to map which endpoints exist.

The key can be backed by a client certificate as well — see Client certificates (mTLS).

Key validity#

Each key is generated when its route or funnel is created and is valid until you replace it unless you say otherwise. Valid for on the row sets a lifetime — 1 or 4 hours, or 1, 7, 30, 90, or 360 days — always measured from the moment the key was last generated, never from when you picked it. Changing the setting therefore re-describes how long this secret was ever meant to live rather than granting it more time: dropping a month-old key to "1 hour" ends it now. Once it lapses the endpoint answers 403; the row says so in red, and so does the log.

Regenerate issues a new key for that one endpoint, immediately, and is the only thing that restarts the clock — at whatever lifetime is currently selected. Clients still holding the old key get 403; every other endpoint is untouched. It is also the way back from an expired key.

Upgrading from a build with a single proxy-wide key: that key is no longer read. Every existing route and funnel is issued its own on first launch, so each client has to be given the key of the endpoint it calls.

Use Regenerate if a key is ever exposed; clients using the old key start getting 403 immediately.

Why the key exists, and why there is one per endpoint#

Binding to 127.0.0.1 keeps other machines out, but it is not an authorization boundary: every process on your computer, under any account, can reach loopback. Since the proxy attaches your live OAuth token to whatever it forwards, an unguarded listener would hand your Google or Nextcloud session to any local program that knew the port.

One key for the whole proxy made every client that held it a client of every route: an agent given the key so it could reach a calendar endpoint could equally spend the grant attached to a mail endpoint, and revoking one client meant re-keying all of them. Per-endpoint keys make the blast radius of a leaked key exactly the endpoint it was issued for, and revocation a one-row operation. It is also what makes a funnel meaningful — an agent handed a funnel's key sees the tools that funnel exposes and cannot reach the routes underneath it directly.

The key also blocks DNS rebinding, where a page on an attacker's domain re-resolves that name to 127.0.0.1 so the browser treats proxied responses as same-origin and lets its JavaScript read them.

Alongside the key, the proxy refuses requests whose Host is not loopback, refuses requests carrying an Origin header (only browsers send one), and strips Access-Control-* headers from upstream responses so a permissive upstream cannot reopen the same hole.


Edit this page on the wiki Request support · Report vulnerabilities privately