Reverse proxies must not point at localhost
On this page
Reject reverse-proxy config files that name localhost as the upstream target. Modern Go-based proxies (cloudflared, anything using the Go net resolver) try IPv6 ([::1]) before IPv4 (127.0.0.1) on Linux. If the destination service binds only 0.0.0.0:<port> and not [::]:<port>, every request fails with connection refused on the host side and an opaque error on the client side.
Typical failure shape: service: ssh://localhost:22 in /etc/cloudflared/config.yml makes cloudflared dial [::1]:22 first. sshd is bound only on 0.0.0.0:22. Every SSH-via-tunnel attempt fails with dial tcp [::1]:22: connect: connection refused on the host and websocket: bad handshake on the client laptop - with no useful signal on the client side. Hours of debugging until the IPv6 resolution-order theory lands.
What this catches#
localhost references in upstream / proxy-pass / reverse_proxy directives across the common reverse-proxy config formats:
| Proxy | Pattern that fires |
|---|---|
| cloudflared | service: <scheme>://localhost:<port> |
| nginx | proxy_pass http(s)://localhost... |
| nginx upstream | server localhost:<port>; |
| Caddy | reverse_proxy localhost:<port> |
| HAProxy | server <name> localhost:<port> |
| Traefik | url = "http(s)://localhost:<port>" |
Fix#
Replace localhost with the literal loopback IP:
# WRONG (cloudflared) - Go resolver picks IPv6 first
service: ssh://localhost:22
# RIGHT - pin the address family
service: ssh://127.0.0.1:22 # IPv4 listener
service: ssh://[::1]:22 # IPv6 listener (verify with `ss -tlnp` first)
# WRONG
proxy_pass http://localhost:8080;
# RIGHT
proxy_pass http://127.0.0.1:8080;
# WRONG
reverse_proxy localhost:3000
# RIGHT
reverse_proxy 127.0.0.1:3000
If you genuinely need dual-stack (rare for loopback), bind the destination service on [::]:<port> too and choose 127.0.0.1 here. The point: pin the family at config time, don’t let the resolver pick.
Suppressing intentional cases#
For documentation comments / example configs that intentionally show localhost:
# appframes:disable-next-line network/no-localhost-in-proxy-config
# Example: service: http://localhost:8080
Generalizes to#
Any service-config file that takes an upstream hostname:
- Cloudflare Tunnel (
cloudflared) - nginx
proxy_pass/upstream server - Caddy
reverse_proxy - HAProxy
server - Traefik service URLs
- Docker
host.docker.internalis a similar trap; pin tohost-gatewayor a literal IP - Redis
bind localhostvsbind 127.0.0.1 - Postgres
listen_addresses = 'localhost'vs'127.0.0.1'
The frame currently covers reverse-proxy configs only; expanding to Redis/Postgres configs is a future addition.
WARN lets the push through and records the finding. Turn frames on per repo on the dashboard's Policy page - see choosing what the gate checks.
Source on GitHub Live demo How it works Questions: contact@nimblegate.com