Running behind a proxy
A load balancer or reverse proxy terminates TLS, so the server sees plain http from the proxy's address.
A load balancer or reverse proxy terminates TLS, so the server sees plain http from the proxy's address. Two things
are then wrong: the URL (http, the proxy's host name) and the client address (the proxy). They have two separate
settings, described in Hosts and proxies:
serve(handler, { trustProxy: true })reads the last value ofX-Forwarded-ProtoandX-Forwarded-Host: the value the nearest proxy appended, not the client's leftmost one (G-C6). Soevent.url,Securecookies, HSTS, the CSRF origin comparison andallowedHostssee the public URL. A proxy that appends must be the one you own.security.trustProxy(inentry-server.ts) derivesevent.clientAddressfromX-Forwarded-For, which rate limits and logs use.
Trust only the headers your proxy supplies. Configure it to overwrite forwarded host, protocol and client-address headers rather than passing values from the caller. For nginx:
upstream shadow_app {
server 127.0.0.1:3000;
keepalive 16;
}
server {
listen 443 ssl;
http2 on;
server_name example.com;
# ssl_certificate and ssl_certificate_key go here.
location / {
proxy_pass http://shadow_app;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
# Streamed pages must reach the browser as they are rendered.
proxy_buffering off;
# One place compresses: serve({ compress: false }) if nginx gzips.
gzip off;
}
location /assets/ {
proxy_pass http://shadow_app;
proxy_set_header Host $host;
add_header Strict-Transport-Security "max-age=63072000" always;
}
}
X-Forwarded-For $remote_addr replaces whatever the client sent; $proxy_add_x_forwarded_for would append to it, and
then hops in security.trustProxy must count the proxies you own. With one proxy and hops: 1 (the default) the
address is the last entry. Static files bypass the handler, so the HSTS header for /assets/ is set by the proxy;
security.headers.hsts covers everything the handler answers. Behind a CDN or a managed load balancer follow the same
rules: forward the original host and scheme, and list the public host names in allowedHosts.
allowedHosts should be set in production. The Host header is attacker input, and Dusk builds absolute URLs and host
conditions from it. When it is unset in production the server starts and serves with the defaults; the first production
request is answered after one warning that names security.allowedHosts, sent through onWarning (default
console.warn). The generated
template sets it from APP_URL. To accept any Host on purpose (a proxy that already checks it), write
allowedHosts: [] and also acknowledgeNoHostCheck: true; without the acknowledgment an empty list is refused in
production, because that choice is never made by accident.