[]previous and next page

Running behind a proxy

A load balancer or reverse proxy terminates TLS, so the server sees plain http from the proxy's address.

For agentsUpdated

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 of X-Forwarded-Proto and X-Forwarded-Host: the value the nearest proxy appended, not the client's leftmost one (G-C6). So event.url, Secure cookies, HSTS, the CSRF origin comparison and allowedHosts see the public URL. A proxy that appends must be the one you own.
  • security.trustProxy (in entry-server.ts) derives event.clientAddress from X-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:

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.