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.

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/8 by reflex.
  • Cloudflare-specific headers are not parsed. Configure your origin proxy/tunnel to emit Forwarded or X-Forwarded-For, or let Cloudflare enforce per-client limits and leave trusted_proxies unset.
  • Changes require a restart. A SIGHUP that changes trusted_proxies leaves the old set active and surfaces trusted_proxies in /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.