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:

  1. 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
    
  2. Untrusted reverse proxy appends attacker’s IP:

    X-Forwarded-For: 192.0.2.1, 198.51.100.2, 203.0.113.50
    
  3. If 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:

  1. Clear untrusted headers from incoming requests

  2. Set correct forwarding headers with the real client IP

  3. ProxyWhirl trusts only the configured header (e.g., X-Real-IP or the rightmost IP in X-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_addr is the connecting client’s IP (cannot be spoofed)

  • set_real_ip_from whitelists trusted upstream proxies

  • real_ip_recursive on processes the entire X-Forwarded-For chain

  • Only 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-header is evaluated per request (before any client manipulation)

  • option forwardfor adds/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):

  1. X-Real-IP header (recommended for reverse proxies)

  2. Rightmost IP in X-Forwarded-For header

  3. Direct 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:

  1. Verify reverse proxy is clearing client-provided X-Forwarded-For

  2. Check ProxyWhirl logs for detected client IP:

    # Enable debug logging
    export LOGLEVEL=DEBUG
    
  3. 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:

  1. Verify PROXYWHIRL_CLIENT_IP_HEADER environment variable

  2. Check reverse proxy is setting the header correctly:

    curl -v http://localhost/api/v1/metrics
    # Look at response headers from reverse proxy
    
  3. Review reverse proxy logs for X-Real-IP values being set

Connection Refused Behind Reverse Proxy

Symptom: HTTP 502 Bad Gateway

Solution:

  1. Verify ProxyWhirl is running:

    docker logs proxywhirl-container
    
  2. Test direct connection:

    curl http://localhost:8000/api/v1/health
    
  3. Check reverse proxy network connectivity

  4. Review reverse proxy logs for backend errors

SSL Certificate Errors

Symptom: HTTPS requests return certificate warnings

Solution:

  1. Obtain valid certificate (Let’s Encrypt recommended)

  2. Configure reverse proxy with certificate

  3. Set X-Forwarded-Proto: https in reverse proxy configuration

  4. ProxyWhirl will correctly generate links with https://

References

Security Standards

Reverse Proxy Documentation

Cloud Documentation

ProxyWhirl Documentation

REST API Reference

Full REST API documentation including rate limiting endpoints and authentication.

ProxyWhirl REST API Usage Guide
Rate Limiting API

Detailed rate limiting configuration and token bucket algorithm reference.

Rate Limiting API Reference
Configuration Reference

All environment variables, TOML keys, and configuration options.

Configuration Reference
Caching Subsystem

Credential encryption, key rotation, and secure cache storage.

Caching Subsystem Guide
CLI Reference

CLI commands for health monitoring, proxy management, and configuration.

CLI Reference
Automation Guide

CI/CD workflows for automated proxy refresh and source validation.

Automation & CI/CD Runbook