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
- Run
swamp serve --host 0.0.0.0 --auth-mode token ...behind a reverse proxy that routesHost: serve.example.comto it. - From a client:
swamp model method run <model> <method> --server wss://serve.example.com --token <name>.<secret> - Client fails with:
Could not connect to wss://serve.example.com/ - 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 servecannot 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 rewriteHosttolocalhost.- Note: Traefik v3 does not allow overriding the
Hostheader via theheaders.customRequestHeadersmiddleware, 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 envSWAMP_SERVE_TRUSTED_HOSTS/SWAMP_SERVE_TRUSTED_ORIGINS), or- when
--trust-proxyis set, honorX-Forwarded-Hostfor 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.
Closed
No activity in this phase yet.
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.