Skip to main content
← Back to list
01Issue
BugClosedSwamp CLI
AssigneesNone

Relationships

#889 swamp serve: hardcoded WebSocket host allowlist makes it unusable behind a reverse proxy (no --trusted-hosts/--allowed-origins)

Opened by magistr · 6/29/2026

Summary

swamp serve's WebSocket upgrade handler enforces a hardcoded host allowlist with no configuration option, which makes a served instance unreachable through any reverse proxy (Traefik/Caddy/nginx) that forwards the real Host header. This contradicts serve's documented deployment model (TLS + token auth + --trust-proxy, and a Caddy reverse-proxy example in the manual).

Environment

  • swamp 20260629.145610.0-sha.ac2f277d (latest stable), linux-x86_64, running in Docker on a LAN host.
  • Datastore: @keeb/mongodb-datastore (remote Mongo). Vaults: @webframp/hashicorp-vault.
  • Reverse proxy: Traefik v3 terminating public TLS (Let's Encrypt) and proxying to the serve container over the container network (https backend, insecureSkipVerify).
  • Launch: swamp serve --host 0.0.0.0 --port 9090 --cert-file ... --key-file ... --auth-mode token --admins user:magistr --ws-idle-timeout 5m --trust-proxy

Repro

  1. Run swamp serve --host 0.0.0.0 --auth-mode token ... behind a reverse proxy that routes Host: serve.example.com to it.
  2. From a client: swamp model method run <model> <method> --server wss://serve.example.com --token <name>.<secret>
  3. Client fails with: Could not connect to wss://serve.example.com/
  4. Serve logs: WRN serve WebSocket upgrade rejected: "untrusted host: serve.example.com" from "<proxy-ip>"

A plain HTTPS GET / (health) succeeds through the same proxy (HTTP 200) — only the WebSocket upgrade is rejected, so routing/TLS/auth are all fine.

Root cause

The WS upgrade host check builds a fixed set and rejects anything else, with no flag/env to extend it:

const TRUSTED_HOSTS = new Set([
  "127.0.0.1",
  "localhost",
  "::1",
  bindHost.toLowerCase(),   // = the --host value, e.g. 0.0.0.0
]);
if (!TRUSTED_HOSTS.has(hostName)) {
  return { allowed: false, reason: `untrusted host: ${hostHeader}` };
}

(There is a parallel TRUSTED_ORIGINS check for the Origin header with the same limitation.)

Because the only trusted hosts are loopback and the bind address, a proxied request carrying a real hostname is always rejected. --trust-proxy does not affect this check.

Impact

  • swamp serve cannot be used behind a reverse proxy with a real domain — the standard production pattern, and the one the docs themselves illustrate (Caddy in front). Caddy/nginx would hit the same wall unless they are specifically configured to rewrite Host to localhost.
  • Note: Traefik v3 does not allow overriding the Host header via the headers.customRequestHeaders middleware, so the obvious "rewrite Host to localhost" workaround is not available on Traefik without a sidecar.

Expected / suggested fix

Add a configuration option to extend the trusted host/origin sets, e.g.:

  • --trusted-hosts <comma-list> and --trusted-origins <comma-list> (and/or env SWAMP_SERVE_TRUSTED_HOSTS / SWAMP_SERVE_TRUSTED_ORIGINS), or
  • when --trust-proxy is set, honor X-Forwarded-Host for the allowlist check, or
  • derive trusted hosts from the TLS cert SAN / a configured public URL (SWAMP_SERVE_URL).

Any of these would let serve run behind a reverse proxy as documented while keeping the anti-DNS-rebinding protection for the unconfigured default.

02Bog Flow
OPENTRIAGEDIN PROGRESSCLOSED

Closed

6/29/2026, 10:14:21 PM

No activity in this phase yet.

03Sludge Pulse
Editable. Press Enter to edit.

stack72 commented 6/29/2026, 10:14:20 PM

Closing this - we have this working and there's a guide on it in our manual

stack72 commented 6/30/2026, 9:06:34 AM

There was a bug found here that shipped as part of https://github.com/swamp-club/swamp/pull/1717 where it was blocking the localhost / 127.0.0.1 setup - that's been resolved now

Sign in to post a ripple.