Deployment Security & Reverse Proxy Configuration¶
Complete guide for securely deploying ProxyWhirl in production with trusted reverse proxy configurations.
Overview¶
When deploying ProxyWhirl behind a reverse proxy in production, the rate limiting and client identification features depend on accurately determining the real client IP address. This is the critical security boundary that prevents attackers from spoofing their IP to bypass rate limits.
Recommended Architecture:
Internet
↓
[Reverse Proxy: Nginx/Caddy/HAProxy/Cloud LB] ← Clears untrusted headers
↓ (X-Real-IP or X-Forwarded-For set here)
[ProxyWhirl API Server] ← Reads trusted headers
↓
[Proxy Pool Management]
X-Forwarded-For Security¶
The Attack¶
The X-Forwarded-For HTTP header is designed to pass the real client IP through proxy chains. However, since HTTP headers can be set by any client, an attacker can forge this header to bypass security controls:
Attack Scenario:
Attacker sends request with forged header:
GET /api/v1/request HTTP/1.1 Host: api.example.com X-Forwarded-For: 192.0.2.1, 198.51.100.2
Untrusted reverse proxy appends attacker’s IP:
X-Forwarded-For: 192.0.2.1, 198.51.100.2, 203.0.113.50If ProxyWhirl reads leftmost IP (192.0.2.1), rate limiting is bypassed:
Attacker can exhaust rate limits under spoofed IPs
1000 requests/min limit becomes 1000 per unique forged IP
Legitimate rate limiting is rendered ineffective
The Defense¶
Key Principle: Your reverse proxy must be configured to clear or overwrite any client-provided X-Forwarded-For header and set it to the real connecting IP address.
Three Critical Requirements:
Clear untrusted headers from incoming requests
Set correct forwarding headers with the real client IP
ProxyWhirl trusts only the configured header (e.g.,
X-Real-IPor the rightmost IP inX-Forwarded-For)
Reverse Proxy Configurations¶
Nginx¶
Secure Configuration (Single Proxy):
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://proxywhirl:8000;
proxy_set_header Host $host;
# Critical: Use $remote_addr (cannot be spoofed by clients)
proxy_set_header X-Real-IP $remote_addr;
# Set clean X-Forwarded-For from real client IP only
proxy_set_header X-Forwarded-For $remote_addr;
# Preserve other forwarding context
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# Connection handling
proxy_set_header Connection "";
proxy_http_version 1.1;
}
}
Secure Configuration (Multi-Proxy Chain with ngx_realip Module):
When ProxyWhirl sits behind multiple proxies (e.g., CDN → nginx → app), use the ngx_realip module:
# Load the realip module (may be compiled in or as a dynamic module)
# If dynamic: load_module modules/ngx_http_realip_module.so;
server {
listen 80;
server_name api.example.com;
# Configure which upstream proxies are trusted
set_real_ip_from 10.0.0.0/8; # Trust internal network
set_real_ip_from 172.16.0.0/12; # Docker subnet
set_real_ip_from 203.0.113.0/24; # Your CDN IP range
real_ip_header X-Forwarded-For;
real_ip_recursive on;
location / {
proxy_pass http://proxywhirl:8000;
proxy_set_header Host $host;
# After real_ip processing, $remote_addr is the true client IP
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Key Points:
$remote_addris the connecting client’s IP (cannot be spoofed)set_real_ip_fromwhitelists trusted upstream proxiesreal_ip_recursive onprocesses the entire X-Forwarded-For chainOnly trust proxies within your control
Caddy¶
Secure Configuration:
api.example.com {
reverse_proxy localhost:8000 {
# Replace X-Forwarded-For with real client IP
# (Caddy removes untrusted headers by default)
header_up -X-Forwarded-For
header_up X-Forwarded-For {http.request.remote.host}
# Set X-Real-IP to the actual client IP
header_up X-Real-IP {http.request.remote.host}
# Preserve protocol and host information
header_up X-Forwarded-Proto {http.request.proto}
header_up X-Forwarded-Host {http.request.host}
header_up X-Forwarded-Port {http.request.port}
}
}
Advantages:
Caddy automatically removes untrusted X-Forwarded-For headers from clients
Simple, secure-by-default configuration
Excellent choice for production without extensive tuning
HAProxy¶
Secure Configuration:
global
log stdout local0
maxconn 4096
defaults
log global
mode http
timeout connect 5s
timeout client 50s
timeout server 50s
frontend api_frontend
bind 0.0.0.0:80
default_backend api_backend
# Security: Normalize headers to prevent spoofing
# Extract real client IP from connection
http-request set-header X-Real-IP %[src]
# For X-Forwarded-For chains, only keep the real client IP
http-request set-header X-Forwarded-For %[src]
http-request set-header X-Forwarded-Proto %[req.hdr(X-Forwarded-Proto)]
# Additional security headers
http-request set-header X-Forwarded-Host %[req.hdr(Host)]
backend api_backend
balance roundrobin
# Configure ProxyWhirl server
server api1 localhost:8000 check
# Preserve client IP in backend communication
option forwardfor
# Additional security response headers
http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains"
http-response set-header X-Content-Type-Options "nosniff"
Key Points:
%[src]is HAProxy’s source IP (cannot be spoofed)http-request set-headeris evaluated per request (before any client manipulation)option forwardforadds/updates X-Forwarded-For with the real client IP
Traefik¶
Secure Configuration (docker-compose):
version: '3.8'
services:
traefik:
image: traefik:latest
ports:
- "80:80"
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
networks:
- proxywhirl-net
proxywhirl:
image: proxywhirl-api:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.proxywhirl.rule=Host(`api.example.com`)"
- "traefik.http.routers.proxywhirl.entrypoints=web"
- "traefik.http.services.proxywhirl.loadbalancer.server.port=8000"
# Security: Forward real client IP
- "traefik.http.middlewares.client-ip.headers.customrequestheaders.X-Real-IP={{ .ClientIP }}"
- "traefik.http.middlewares.client-ip.headers.customrequestheaders.X-Forwarded-For={{ .ClientIP }}"
- "traefik.http.routers.proxywhirl.middlewares=client-ip"
environment:
PROXYWHIRL_STORAGE_PATH: /data/proxies.db
volumes:
- proxywhirl-data:/data
networks:
- proxywhirl-net
networks:
proxywhirl-net:
driver: bridge
volumes:
proxywhirl-data:
Alternative (File-based Configuration):
# traefik-config.yml
http:
middlewares:
client-ip:
headers:
customRequestHeaders:
X-Real-IP: "{{ .ClientIP }}"
X-Forwarded-For: "{{ .ClientIP }}"
routers:
proxywhirl:
rule: "Host(`api.example.com`)"
service: proxywhirl-api
middlewares:
- client-ip
services:
proxywhirl-api:
loadBalancer:
servers:
- url: "http://localhost:8000"
AWS Application Load Balancer¶
Secure Configuration (Terraform):
resource "aws_lb_target_group" "proxywhirl" {
name = "proxywhirl-api"
port = 8000
protocol = "HTTP"
vpc_id = aws_vpc.main.id
# Essential: Preserve client IP in ALB
stickiness {
enabled = false
}
# Health check configuration
health_check {
path = "/api/v1/health"
port = "8000"
protocol = "HTTP"
healthy_threshold = 2
unhealthy_threshold = 2
timeout = 5
interval = 30
matcher = "200"
}
tags = {
Name = "proxywhirl-api"
}
}
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.main.arn
port = 80
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.proxywhirl.arn
}
}
# Security Group: Restrict to ALB traffic
resource "aws_security_group" "proxywhirl" {
name_prefix = "proxywhirl-"
vpc_id = aws_vpc.main.id
ingress {
from_port = 8000
to_port = 8000
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
description = "From ALB only"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "proxywhirl-sg"
}
}
ALB Sets These Headers:
X-Forwarded-For: <client-ip>
X-Forwarded-Proto: https
X-Forwarded-Port: 443
X-Amzn-Trace-Id: <aws-trace-id>
Security Notes:
ALB is managed by AWS and automatically clears untrusted headers
Enable “Preserve client IP” in target group attributes
Trust ALB’s IP range (use security groups)
ProxyWhirl should read
X-Forwarded-For(rightmost IP is client)
Google Cloud Load Balancer¶
Secure Configuration (gcloud):
# Create backend service
gcloud compute backend-services create proxywhirl-backend \
--protocol=HTTP \
--health-checks=proxywhirl-health-check \
--global \
--session-affinity=NONE
# Add instance group to backend
gcloud compute backend-services add-backend proxywhirl-backend \
--instance-group=proxywhirl-ig \
--instance-group-zone=us-central1-a \
--global
# Create URL map
gcloud compute url-maps create proxywhirl-lb \
--default-service=proxywhirl-backend
# Create HTTP proxy
gcloud compute target-http-proxies create proxywhirl-proxy \
--url-map=proxywhirl-lb
# Create forwarding rule
gcloud compute forwarding-rules create proxywhirl-forwarding \
--global \
--target-http-proxy=proxywhirl-proxy \
--address=proxywhirl-ip \
--port-range=80
GCP Load Balancer Sets:
X-Forwarded-For: <client-ip>
X-Forwarded-Proto: https
Security Notes:
GCP Load Balancer automatically manages header security
Use Cloud Armor to enforce additional IP restrictions
Restrict backend access to LB IP range only
ProxyWhirl Configuration¶
See also
For the complete list of environment variables and configuration options, see Configuration Reference. For the REST API endpoint documentation, see ProxyWhirl REST API Usage Guide.
Reading Client IP from Headers¶
ProxyWhirl automatically reads the client IP from trusted headers for rate limiting:
Priority Order (first found is used):
X-Real-IPheader (recommended for reverse proxies)Rightmost IP in
X-Forwarded-ForheaderDirect connection IP (fallback)
Environment Variables:
# Specify which header to trust for client IP
# Options: "X-Real-IP", "X-Forwarded-For" (rightmost), or "remote-addr"
export PROXYWHIRL_CLIENT_IP_HEADER="X-Real-IP"
# Define trusted upstream proxy IP ranges (CIDR notation)
# ProxyWhirl will only trust headers from these IPs
export PROXYWHIRL_TRUSTED_PROXIES="10.0.0.0/8,172.16.0.0/12,203.0.113.50/32"
# Rate limiting per IP
export PROXYWHIRL_RATE_LIMIT_DEFAULT=100 # requests/minute
export PROXYWHIRL_RATE_LIMIT_REQUEST=50 # requests/minute for /api/v1/request
Docker Compose Example¶
version: '3.8'
services:
nginx:
image: nginx:latest
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
networks:
- secure-net
proxywhirl:
image: proxywhirl-api:latest
environment:
PROXYWHIRL_STRATEGY: "round-robin"
PROXYWHIRL_TIMEOUT: 30
PROXYWHIRL_STORAGE_PATH: /data/proxies.db
PROXYWHIRL_CLIENT_IP_HEADER: "X-Real-IP"
PROXYWHIRL_TRUSTED_PROXIES: "10.0.0.0/8"
PROXYWHIRL_RATE_LIMIT_DEFAULT: 100
PROXYWHIRL_RATE_LIMIT_REQUEST: 50
volumes:
- proxywhirl-data:/data
networks:
- secure-net
expose:
- 8000
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/health"]
interval: 30s
timeout: 10s
retries: 3
networks:
secure-net:
driver: bridge
volumes:
proxywhirl-data:
Warning
For credential encryption at the cache layer, see Caching Subsystem Guide which covers Fernet encryption, key rotation, and SecretStr usage. All proxy credentials should be encrypted at rest.
Security Checklist¶
Before Production Deployment:
[ ] Reverse proxy installed between internet and ProxyWhirl
[ ] Untrusted X-Forwarded-For headers cleared by reverse proxy
[ ] X-Real-IP or X-Forwarded-For set by reverse proxy with real client IP
[ ] ProxyWhirl configured to read correct header (X-Real-IP preferred)
[ ] Trusted proxy IP ranges configured in ProxyWhirl
[ ] Rate limiting tested with spoofed headers (verified to fail)
[ ] Access logs reviewed to verify correct client IP attribution
[ ] Direct internet access blocked (no exposure on :8000 from outside)
[ ] Security group/firewall rules restrict to reverse proxy only
[ ] HTTPS/TLS enabled on reverse proxy (use Let’s Encrypt or managed service)
[ ] API key authentication enabled if sensitive operations are exposed
[ ] CORS origins restricted to known domains
[ ] Regular security updates applied (nginx, HAProxy, ProxyWhirl)
Troubleshooting¶
Rate Limiting Not Working¶
Symptom: Requests with different X-Forwarded-For values bypass rate limit
Solution:
Verify reverse proxy is clearing client-provided
X-Forwarded-ForCheck ProxyWhirl logs for detected client IP:
# Enable debug logging export LOGLEVEL=DEBUG
Test with direct request (no reverse proxy):
curl http://localhost:8000/api/v1/proxies \ -H "X-Forwarded-For: 1.2.3.4" # Should rate limit based on real connection IP, not header value
Metrics Showing Wrong Client IPs¶
Symptom: Metrics endpoint shows incorrect client IP distribution
Solution:
Verify
PROXYWHIRL_CLIENT_IP_HEADERenvironment variableCheck reverse proxy is setting the header correctly:
curl -v http://localhost/api/v1/metrics # Look at response headers from reverse proxy
Review reverse proxy logs for X-Real-IP values being set
Connection Refused Behind Reverse Proxy¶
Symptom: HTTP 502 Bad Gateway
Solution:
Verify ProxyWhirl is running:
docker logs proxywhirl-containerTest direct connection:
curl http://localhost:8000/api/v1/healthCheck reverse proxy network connectivity
Review reverse proxy logs for backend errors
SSL Certificate Errors¶
Symptom: HTTPS requests return certificate warnings
Solution:
Obtain valid certificate (Let’s Encrypt recommended)
Configure reverse proxy with certificate
Set
X-Forwarded-Proto: httpsin reverse proxy configurationProxyWhirl will correctly generate links with
https://
References¶
Security Standards¶
RFC 7239 - Forwarded HTTP Extension - Standard for HTTP forwarding headers
MDN X-Forwarded-For - X-Forwarded-For header documentation
OWASP HTTP Response Splitting - Header injection attacks
OWASP Web Security Testing Guide - HTTP request validation best practices
Reverse Proxy Documentation¶
Nginx Proxy Module - Official proxy configuration
Nginx Real IP Module - Real IP extraction
Caddy Reverse Proxy - Caddy proxy documentation
HAProxy Documentation - HAProxy documentation
Traefik Middleware - Traefik middleware reference
Cloud Documentation¶
AWS ALB Documentation - AWS Application Load Balancer
GCP Load Balancing - Google Cloud Load Balancer
Azure Application Gateway - Azure gateway documentation
ProxyWhirl Documentation¶
Full REST API documentation including rate limiting endpoints and authentication.
Detailed rate limiting configuration and token bucket algorithm reference.
All environment variables, TOML keys, and configuration options.
Credential encryption, key rotation, and secure cache storage.
CLI commands for health monitoring, proxy management, and configuration.
CI/CD workflows for automated proxy refresh and source validation.