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