Viewer guide
HTTPS and a reverse proxy
The Viewer speaks plain HTTP. Put a proxy that terminates TLS in front of it, and set the two values that keep cookies and rate limiting correct behind it.
This assumes the container from Deploy the Viewer is up on port 3100.
The Viewer speaks plain HTTP and terminates nothing. In production you put a TLS-terminating proxy in front of it. There is one non-obvious rule.
Warning Set
VIEWER_PUBLIC_URLto the https URL. The Viewer does not readX-Forwarded-Protofor this: it decides whether to mark the session cookieSecurepurely by checking whetherVIEWER_PUBLIC_URLstarts withhttps://. Leave it on anhttp://value behind an HTTPS proxy and your session cookies ship withoutSecure, meaning a browser will send them over plain HTTP too.Also set
VIEWER_TRUST_PROXY(for one proxy:1) whenever a reverse proxy terminates TLS in front of the Viewer. It feeds Express'strust proxy, which is what lets the built-in rate limiter see real client addresses instead of the proxy's. The literal valuetrueis refused at boot because it would trust a client-supplied header.
The session cookie is HttpOnly and SameSite=Lax regardless; Secure is the only part that depends on this.
An nginx server block:
server {
listen 443 ssl;
server_name proto.internal.example.com;
ssl_certificate /etc/ssl/certs/proto.crt;
ssl_certificate_key /etc/ssl/private/proto.key;
# Tell browsers never to try this host over plain http again. The Viewer
# cannot set this itself: it speaks http and terminates no TLS, so it does
# not know the connection was secure. Without it, the very first request a
# browser makes to a bare hostname is http, and it is interceptable even
# though every later one is not.
add_header Strict-Transport-Security "max-age=63072000" always;
location / {
proxy_pass http://127.0.0.1:3100;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
# Required by VIEWER_TRUST_PROXY. See the warning below.
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Build logs and comment updates are server-sent events.
proxy_buffering off;
proxy_read_timeout 15m;
}
}
alwaysis not optional on thatadd_headerline. Without it nginx omits the header on error responses, which is most of what an attacker can provoke. Note also that nginx'sadd_headerdoes not merge: alocationblock that adds any header of its own drops every header inherited from the server block, so if you add per-location headers later you must repeat this line there too. Warningproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;is not optional once you setVIEWER_TRUST_PROXY.X-Forwarded-Foris the headertrust proxyreads, and nginx does not set it on its own.Leave it out and you get worse than the shared-bucket problem
VIEWER_TRUST_PROXYexists to fix. nginx passes a client's own headers through to the upstream, so a caller can send their ownX-Forwarded-For, and the Viewer, told to trust one proxy, will believe it. Rotating that header then gives a fresh rate-limit bucket per request on the sign-in and comment-write lanes.
$proxy_add_x_forwarded_forappends the real peer address to whatever the client sent, which is what makes the value trustworthy at the hop count you configured.
The last two lines matter. Build logs and comment updates stream over server-sent events, so a buffering proxy holds the log until the build finishes and shows the user a frozen panel. And a single build step is allowed up to ten minutes, so a default 60-second read timeout will cut the stream mid-build on any real project.
If you use VIEWER_SERVE_DOMAIN to serve each prototype on its own subdomain, the proxy needs a wildcard certificate and a wildcard server_name for that domain, and it must pass the original Host header through: the Viewer routes on it. See serving.