Running the relay behind a reverse proxy
A reverse proxy is a web server of your own that takes the connection from the
browser and hands it to the relay. You need one when you want HTTPS for a relay
on your LAN or on a server, when the relay should live under a sub-path of a
site you already run (https://), or when a tunnel's edge
(for example Cloudflare) sits between the browser and the relay. A proxy on
the relay's own machine works without any setting. A proxy on another machine
needs two or three, shown below. Five config. keys tell
the relay about the proxy, and each one fixes something specific; they are
explained after that.
What the proxy must do #
The relay's page, its API and its live connection all use one port (3333
unless you changed port). The proxy must:
- Pass WebSocket upgrades. The app keeps a Socket.IO connection open to
the relay, on the path
/(with a prefix,socket. io/ <prefix>/). If upgrades do not get through, the page loads but never shows live updates.socket. io/ - Not buffer or time out long-lived connections. That socket stays open for as long as the page is open, and turns stream to it as they happen. Use long read and idle timeouts, and do not buffer responses.
- Pass the
Hostheader, or sendX-. The relay builds the address of a shared link from it, and Added in 0.9.11 accepts the live connection only from a page on that address (seeForwarded- Host allowed).Origins X-andForwarded- Host X-count only from a proxy the relay trusts.Forwarded- Proto - Send
X-when the proxy ends TLS, so the relay knows the browser used HTTPS (see Cookies and HTTPS), andForwarded- Proto X-, so it knows the client's address.Forwarded- For - Keep the path as it is. Do not rewrite it. If you serve the relay under
a prefix, the relay removes the prefix itself (see
remote).Path
You do not need a cache rule. The relay sends Cache- for its
page, its service worker, its manifest and its API, and also
CDN- and Cloudflare-.
Do not add a rule that caches /: shared views poll for new
content, and a cached answer would show viewers an old snapshot.
A minimal Caddy example for a relay on the same machine. Caddy passes
WebSocket upgrades on its own and adds X-:
relay.example.com {
reverse_proxy 127.0.0.1:3333
}
For nginx or any other proxy, check its own documentation for how to pass upgrades and set timeouts, against the list above. For tunnels, see SSH tunnel and Cloudflare Tunnel; for the access token, see Access.
A proxy on another machine Added in 0.9.11 #
The built-in tunnels and a proxy on the relay's own machine reach the relay
over 127., and the default trust believes their forwarded
headers. A proxy on another host, or in another container or VM, is a
different peer. Until you trust it, the relay ignores its X-,
X- and X-, so:
- every visitor has the proxy's address: one sign-in lockout for all of them, and one address in every access log entry;
- new share links start with
http://and the host the proxy sent; - the sign-in cookie is not marked
Secure.
Set three keys in the config file and restart the relay:
{
"localhostOnly": false,
"trustProxy": "192.0.2.10",
"allowedOrigins": ["https://relay.example.com"]
}
localhostlets the relay listen beyond loopback. Keep the port closed to everything but the proxy.Only: false trustis the proxy's address, its subnet, or the number of proxies in front of the relay (Proxy 1for one). See below.allowedis needed only when the proxy rewrites theOrigins Hostheader and sends noX-. See below.Forwarded- Host
A proxy on the relay's own machine needs neither trust nor
localhost, and allowed only if it rewrites Host. Under
a URL prefix, set remote too.
The keys that tell the relay about the proxy #
All five are in the table on config.json. Restart the relay after changing them.
trustProxy #
Default "loopback". It decides whose X- headers the relay
believes. With the default, only a proxy that connects from the same machine
is believed, which is right for Caddy, nginx or cloudflared running next to
the relay.
What the relay takes from the headers of a trusted proxy:
- whether the request came over HTTPS, which decides if the sign-in cookie
carries the
Secureflag, and the scheme of a new share link; - the host the browser addressed (
X-), for new share links and for the origin check of the live connection;Forwarded- Host - the client's address, for the sign-in lockout, the presence limit of
shared views and the access log of shared links. The country in that
log (
cf-) is also read only from a trusted proxy.ipcountry
If your proxy runs on another machine, the relay sees a non-local peer and
ignores those headers. Give trust what Express's trust proxy takes:
a hop count (1), an address or subnet ("10.), a list of them, or
"loopback".
Only with a proxy in front
Set trust wider than "loopback" only when every connection to the
relay really comes through your proxy. Otherwise anyone who can reach the port
can send a forged X-.
Added in 0.9.11 Every use of a forwarded header follows trust. In 0.9.10
the presence limit of shared views and the address of a new share link read
them from any client.
allowedOrigins Added in 0.9.11 #
Default []. The live connection (Socket.IO) accepts a browser's handshake
only from the relay's own origin: the Origin the browser sends must have the
host the browser addressed, which is X- when a trusted proxy
sends one and Host otherwise. A page on another site cannot use your sign-in
cookie to open a live connection. Clients that send no Origin, such as
scripts, are not affected.
A proxy that rewrites the Host header to the relay's internal address and
sends no X- breaks that comparison. List the public origin
then, exactly as the browser shows it, with scheme, host and any port other
than the default:
"allowedOrigins": ["https://relay.example.com"]
Without it the page loads and you can sign in, but nothing updates live, and the relay logs once per origin:
[socket] Refused a handshake from origin https://relay.example.com (host 127.0.0.1:3333); list it in allowedOrigins if a proxy rewrites the Host header
remotePath #
Default "". Set it to the prefix when the proxy serves the relay under a
sub-path, for example "/. A leading slash is added and trailing slashes
are removed.
With it set, the relay:
- takes the prefix off every incoming request, so the proxy passes the path with the prefix and does not strip it;
- puts the prefix into the sign-in cookie's path, the socket path
(
/), and the addresses of files and shared links.oar/ socket. io/
Without it, a relay under a prefix cannot find its own files and its socket,
because the page asks for / and / at the root of the
site. Open the relay with a trailing slash (https://).
An installed app belongs to the address it was installed from, so a relay under a prefix is its own app. See Install as an app.
publicHostnames #
Default []. List the host names that reach the relay itself, for example
["relay.. The relay uses them in one place: the preview lane
refuses a previews. on one of these names (or on localhost
or 127.), so a previewed app never shares a login cookie with the
relay. If you put the relay behind a proxy on a new name and use
previews, add that name here.
tunnelMarkerHeaders #
Default []. The names of headers that your proxy or tunnel's edge adds to
every request it forwards. The relay uses them for the
session-worker path guard below. cf-, which Cloudflare adds,
is always on the list. The value can be an array or a comma-separated string.
The environment variable COPILOT_ (comma separated)
adds more names; Added in 0.9.11 the two lists are merged, so
tunnel no longer switches the variable off. See
Environment variables.
The status dot
tunnel does not turn the status dot amber. The dot is amber
while the relay's own managed Cloudflare tunnel is connected (see
the conversation menu). A proxy you run yourself
never turns it amber.
The session-worker path guard #
Each agent session runs in a worker on the machine of the relay, and the worker
talks to the relay over a WebSocket on / (with a prefix,
<prefix>/). A proxy or tunnel forwards every path of its
host name to the relay, so without a guard that path would be reachable from
the internet. Only local workers should use it.
Added in 0.9.11 The guard lets a request to that path through only when it comes from the relay's own machine and nothing forwarded it:
- the connecting peer is loopback (
127.or0. 0. 1 ::);1 - it carries no marker header:
cf-, or one inray tunnel;Marker Headers - it carries no
X-,Forwarded- For ForwardedorX-.Real- IP
Anything else is refused before sign-in is checked:
- a plain request gets
403with{"error":"Session worker endpoints are reachable from this machine only"}; - a WebSocket upgrade gets
403 Forbiddenand the connection is closed.
Each refusal is logged as [tunnel-, naming what gave the request away. Local workers connect to
127. directly, without any of these headers, and are not affected.
Shared links keep working through the tunnel: the guard keys on the worker
path only, not on a request being anonymous.
Cloudflare adds cf- and Caddy adds X- on its own, so the
guard works with them without a setting; with nginx, send X-
with proxy_. A forwarder that
adds nothing at all, such as an SSH reverse tunnel or a bare TCP proxy, looks
like a local process; the path is then protected only by the access token. With
such a forwarder, make it add a header with a fixed non-empty value, for
example X-, and list its name:
"tunnel. Or refuse the path in front of the
relay.
Endpoints only the relay's machine may use #
Added in 0.9.11 Two things answer only requests from the relay's own machine that no proxy forwarded, by the same rule as the path guard:
- the session-worker socket, above;
- running an image operation (
POST /), which the relay's own workers call. Anything else getsapi/ image- operations/ …/ execute 403Image operation execution is reachable from this machine only.
Everything else needs only the access token, from wherever it comes. That includes 🌄 Restart web relay, withdrawing a queued restart (the Cancel button on its banner) and 🚮 Empty queue: any signed-in browser can use them, also over the LAN and through any proxy or tunnel.
Up to 0.9.10 these three were meant for the relay's machine only, but the
check let everything through a tunnel or a proxy on the same machine, and
refused a direct LAN connection while the page reported success. If remote
users must not restart the relay or empty its queue, have the proxy refuse
POST /, POST / and
POST /. oar restart on the relay machine always works.
Cookies and HTTPS #
The sign-in cookie is Http and Same, lasts 30 days, and has the
path / (or the remote prefix). It carries Secure when the relay
considers the request secure.
- Proxy ends TLS on the same machine. The proxy sends
X-, the defaultForwarded- Proto: https trustbelieves it, and the cookie isProxy Secure. - Proxy ends TLS on another machine. Set
trustas above. Until you do, the cookie has noProxy Secureflag, though it still works. - Mixed setups. If you also open the relay directly over plain
httpon the LAN, that request is not secure, so its cookie has noSecureflag; the proxied HTTPS requests get the flag. Browsers keep cookies per address.
Check the setup #
- Open the proxied address in a browser. The sign-in or the conversation list appears.
- Look at the status dot: it turns green and the page shows live updates when you start a turn. An endless reconnecting state means the WebSocket does not get through.
- Under a prefix, open the address with the trailing slash. The page, its styles and the live connection load, and the cookie's path is the prefix (look in the browser's developer tools).
- Create a shared link (Sharing). Its address
uses the public host name and
https, and the prefix if you set one. - Test the guard from outside: request
https://. You should seerelay. example. com/ api/ session- worker/ ws 403and a[tunnel-line in the relay's log. If you get another answer, your proxy adds neither a marker header norguard] X-.Forwarded- For - Behind a proxy on another machine: sign in from two devices, and check in
Settings → Sharing that a shared link's access log shows the
viewers' addresses, not the proxy's. If it shows the proxy's,
trustdoes not match the proxy.Proxy
When it does not work #
| What you see | Likely cause |
|---|---|
| The page loads but reconnects forever, or never updates | The proxy does not pass WebSocket upgrades on /, or closes idle connections. |
The page loads but nothing updates live, and the log says Refused a handshake from origin |
The proxy rewrites the Host header. List the public origin in allowed, or have the proxy pass Host on. |
| Sign-in says Too many failed sign-in attempts for everyone at once | The relay does not trust a proxy on another machine, so all visitors share its address and its lockout. Set trust. |
| Under a prefix, the page is blank or files are missing | remote is not set, or the proxy strips the prefix. The proxy must pass it. |
| Under a prefix, you are asked to sign in again and again | The cookie's path is the remote prefix, so the browser sends it only under that prefix. Open the relay under that prefix, and with the trailing slash. |
| Shared links or file links have the wrong address or no prefix | remote is unset, or the proxy sends no Host or X- / X-, or the relay does not trust the proxy. |
Share links start with http://, or the cookie has no Secure flag, behind a proxy on another machine |
trust is still "loopback". |
The worker path answers without a 403 through your proxy |
The proxy adds neither a marker header nor X-. See the path guard. |
| The dot never turns amber | Expected: only the relay's own managed Cloudflare tunnel does that. |
| Previews refuse to start on the new name | Add the relay's host name to public. |
For the first sign-in and the access token, see First run.