Reverse proxy and TLS

HTTPS reaches an esrun service one of two ways: a reverse proxy terminates TLS and forwards plain HTTP, or esrun terminates TLS itself. A proxy is the usual choice — it obtains and renews certificates, and serves several services on one address.

Caddy

Caddy obtains a certificate for the site's name and forwards to the service. h2c:// makes it speak HTTP/2 to esrun over the internal connection, which esrun serves without configuration — see HTTP/2:

TEXT
example.com {
	reverse_proxy h2c://app:8080
}

Here app is the service's name on a Compose network; on a single server it is 127.0.0.1:8080.

nginx

NGINX
server {
    listen 80;
    location / {
        proxy_pass http://app:8080;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Add TLS with nginx's own listen 443 ssl and certificate directives.

The client's address

Behind a proxy, the connection esrun sees is the proxy's: the handler's info.remoteAddr is the proxy, and the client arrives in X-Forwarded-For, which both configurations above set. esrun never reads that header on its own — see Identity, limits, and deployment for why and how to read it.

request.url also starts with http://, because the connection to esrun is plain HTTP. The scheme the client used is in X-Forwarded-Proto.

Do not publish the service's own port

Only the proxy should be reachable. Bind the service to 127.0.0.1, or in Compose, leave its port out of ports:; otherwise a client can bypass the proxy and send any X-Forwarded-For it likes.

TLS in esrun

serve() terminates TLS itself with secureTransport: "on" and a PEM certificate and key. Read them from files the service is granted, here Compose secrets under /run/secrets:

TypeScript
import { serve } from "runtime:http";
import { file } from "runtime:fs";

serve({
  port: 8443,
  secureTransport: "on",
  cert: await file("/run/secrets/cert.pem").text(),
  key: await file("/run/secrets/key.pem").text(),
}, handler);
YAML
services:
  app:
    build: .
    command: ["--allow-listen=8443", "--allow-read=/run/secrets", "dist/server.js"]
    ports: ["443:8443"]
    secrets: [cert.pem, key.pem]

secrets:
  cert.pem: { file: ./cert.pem }
  key.pem: { file: ./key.pem }

Clients negotiate HTTP/2 or HTTP/1.1 by ALPN. The certificate is read when the service starts, so a renewed one takes effect on the next restart; esrun does not obtain or renew certificates.

Last updated on
Edit this page