How to run mcpgw behind a load balancer
Problem: you front mcpgw with NGINX, HAProxy, an ingress controller, or a cloud load balancer. Without explicit trust configuration, every request looks like it came from the proxy and IP-based rate-limit buckets collapse onto a single client.
Solution: either terminate TLS directly at mcpgw or configure the exact proxy source CIDRs under trusted_proxies.
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 topology eliminates proxy-header trust configuration.
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 — trusted HTTP reverse proxy
Configure the network CIDRs from which the gateway receives proxy connections:
listen: 0.0.0.0:7332
trusted_proxies:
- 10.0.0.0/24
Configure the proxy to append or replace one of the supported headers. RFC Forwarded is preferred; X-Forwarded-For is the fallback:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
mcpgw trusts those headers only when the immediate TCP peer matches trusted_proxies. It walks the chain right-to-left, skips trusted proxy hops, and uses the first untrusted address. A direct client outside the allowlist cannot spoof a forwarding header.
Enforce the same boundary at the network layer: only the ingress/LB should be able to reach mcpgw. A host inside a trusted CIDR can assert client identity, so never use a broader range than the actual proxy network.
Verifying RemoteAddr
# From two clients outside your LB, make enough requests to exercise the input
# limiter and verify they receive independent buckets.
curl -s -X POST https://mcpgw.acme.com/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"ping"}'
# A blocked request is audited as input_rate_limited. The audit client_ip field
# remains the TCP peer for connection traceability; anonymous principal keys use
# the resolved client IP.
tail -20 /var/log/mcpgw/audit.jsonl | jq 'select(.error_kind == "input_rate_limited")'
Pitfalls
- Do not trust all private networks. Use the ingress pod/node CIDR that actually reaches mcpgw, not
10.0.0.0/8by reflex. - Cloudflare-specific headers are not parsed. Configure your origin proxy/tunnel to emit
ForwardedorX-Forwarded-For, or let Cloudflare enforce per-client limits and leavetrusted_proxiesunset. - Changes require a restart. A SIGHUP that changes
trusted_proxiesleaves the old set active and surfacestrusted_proxiesin/readyz.restart_pending. - Health checks consume one bucket entry. Configure your LB’s health check to hit
/healthz(which is excluded from rate-limit), not/mcp.