Run behind a load balancer
Problem: you front mcpgw with NGINX, HAProxy, an AWS NLB, or a Cloudflare Tunnel. Without care, every request looks like it came from the load balancer’s IP and IP-based rate-limit buckets collapse onto a single client.
Solution: pick one of the two supported topologies below. Header-based forwarding (X-Forwarded-For) is intentionally not an mcpgw input — see Explanation: rate-limit identity for the security reasoning.
Topology A — mcpgw is the TLS edge (recommended)
The simplest deployment. mcpgw terminates TLS itself; there is no proxy in the path; RemoteAddr is the real client IP.
listen: 0.0.0.0:7332
tls:
cert_file: /etc/mcpgw/tls/fullchain.pem
key_file: /etc/mcpgw/tls/privkey.pem
Renew certs out-of-band (cert-manager, certbot, ACM-side renewals + bind-mount swap, etc.). mcpgw re-reads cert/key on SIGHUP.
This is the recommended v1 topology because it eliminates an entire class of misconfiguration.
To require client certificates, add client_ca:
tls:
cert_file: /etc/mcpgw/tls/fullchain.pem
key_file: /etc/mcpgw/tls/privkey.pem
client_ca: /etc/mcpgw/tls/client-ca.pem
Changing certificate file contents can be applied with SIGHUP. Changing TLS from off to on, or on to off, requires a restart.
Topology B — PROXY protocol front
Put HAProxy or NGINX in front in PROXY-protocol mode. They prepend a small header that contains the real client RemoteAddr ahead of the TCP stream; mcpgw consumes it.
NGINX example:
stream {
upstream mcpgw {
server 10.0.0.10:7332;
}
server {
listen 443 ssl;
ssl_certificate /etc/nginx/tls/fullchain.pem;
ssl_certificate_key /etc/nginx/tls/privkey.pem;
proxy_pass mcpgw;
proxy_protocol on;
}
}
mcpgw config:
listen: 0.0.0.0:7332
proxy_protocol:
enabled: true
trusted_cidrs:
- 10.0.0.0/24 # only accept PROXY headers from these source IPs
trusted_cidrsis mandatory whenproxy_protocol.enabled: true. Without it, an attacker who reaches:7332directly could spoof their own client IP. The CIDR list is the IP range your LB is reachable from.
Native PROXY-protocol parsing is on the roadmap; today the proxy_protocol block is parsed but rejected at startup with a clear error. For now, prefer Topology A.
What does NOT work
# DO NOT rely on this for rate-limit identity
proxy_set_header X-Forwarded-For $remote_addr;
mcpgw ignores X-Forwarded-For and Forwarded for rate-limit purposes, by design. The reason is that any client can send those headers; if mcpgw trusted them, an attacker could spoof their identity to evade per-IP throttles. A future release will add an explicit trusted_proxies allowlist for operators with mature LB configs.
Verifying RemoteAddr
# From a client outside your LB, hit the gateway and check what IP was logged
curl -s -X POST https://mcpgw.acme.com/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"ping"}'
# On the gateway:
tail -1 /var/log/mcpgw/audit.jsonl | jq '.client_ip'
The IP should be your real client, not the LB.
Pitfalls
- HTTP-only LBs (Layer 7 ALBs that re-originate TCP) cannot pass PROXY headers. Use Topology A (terminate TLS at mcpgw) or upgrade to a Layer 4 LB.
- Cloudflare Tunnel terminates TLS at Cloudflare and forwards over a tunnel. The tunnel hides the real client IP from the origin — Cloudflare puts it in
CF-Connecting-IP, which mcpgw also ignores. Cloudflare’s WAF and rate-limit features sit at the edge; let them do the throttling and treat mcpgw rate-limits as a backstop with degraded identity. - Health checks consume one bucket entry. Configure your LB’s health check to hit
/healthz(which is excluded from rate-limit), not/mcp.