Skip to main content

Browser access to the Gateway

This page tells an operator which web pages may call the Gateway API from a browser. The Gateway identifies a caller by its WireGuard address. So without this check, any page open in a browser on a WireGuard peer would act with that peer’s authority.

Allowed callers

The check covers /api/*, /mcp, and /mcp/*. It runs before CORS, routing, and the WireGuard identity check. The queue dashboard at /horizon and /horizon/* has its own rule: Laravel’s request forgery check admits a request that changes something only when the browser sends Sec-Fetch-Site: same-origin. A request without Origin passes only when its Sec-Fetch-Site is absent, same-origin, or none. So a cross-site request without an Origin, such as a link opened from another site, is refused. A request whose origin is null is refused. A CORS preflight from a refused page is refused too. The Gateway never answers CORS with * and never allows credentials. Public Routes, custom proxy Routes, analytics tracking hosts, and Routes that are not active grant no browser access.

Annotation package

The annotation package’s Orbit mode calls the Gateway from the page it annotates. It works on an Instance with an active private Route. On any other page, the refusal carries no CORS headers, so the browser hides it. The availability check then reports Cannot reach Orbit. Check the connection.

Web app development server

vp dev in apps/web proxies /api to the Gateway and /grafana to Grafana. For both, the proxy removes the browser’s Origin header, because it serves the page on its own origin. The browser’s Sec-Fetch-Site header still reaches the Gateway, so a cross-site request through the development server is refused too. For disposable UI work, run the web app against a topology with bin/e2e-topology web ISSUE. Its proxy runs inside the topology’s operator container as that topology’s WireGuard peer, with a Gateway access grant. It pins the selected Gateway URL and CA for Gateway, realtime, and metrics traffic. It does not fall back to the caller’s live profile. Loopback publication and SSH forwarding expose the development page, not a new Gateway origin or an exception to the browser-origin policy.

Refusal response

A refused request receives this JSON body with HTTP 403.

Why it works this way

These reasons explain the design. Check them before you propose a change.

Route records as the trust list

The Gateway has no login, so a browser on a peer carries the peer’s authority. CORS alone does not help, because a cross-site form POST reaches the server without a preflight. The annotation package must keep working on development applications, so the Gateway origin alone is too narrow. A fixed list of TLDs such as *.test is rejected, because an unrelated site can use those names. The Route list names exactly the applications that Orbit serves. A page on one of them keeps API access, because those applications belong to the operator.

No browser login

A login or token for browsers is rejected, because it would change the identity model of every client. The origin check closes the exposure without it.