SSL Reverse Proxy with Nginx, Let's Encrypt, and CrowdSec: Production Guide
Why a Dedicated Reverse Proxy
A dedicated reverse proxy layer is the single most impactful security investment you can make for a web application. It handles SSL termination, rate limiting, WAF inspection, and upstream routing — all concerns that individual application servers should not manage themselves.
The stack presented here combines three battle-tested tools: Nginx for request handling, Certbot for automatic SSL, and CrowdSec for behavioral WAF/IPS.
Architecture

Nginx Configuration Decisions
Worker Tuning
Nginx's event-driven architecture means worker configuration directly impacts throughput:
worker_processes auto;
worker_connections 1024;
multi_accept on;
use epoll;
autoworker processes = number of CPU cores- 1024 connections per worker = 4096 concurrent connections on a 4-core VM
epollis the Linux-native event loop — significantly more efficient thanselectorpoll
SSL Hardening
SSL configuration follows Mozilla's "Modern" profile:
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:...;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_session_tickets off;
Key decisions:
- TLS 1.2 + 1.3 only: TLS 1.0/1.1 are universally deprecated. No modern client connects without 1.2+.
- Prefer server ciphers off: Allows clients to negotiate their preferred cipher within the allowed set. Modern clients pick ChaCha20 or AES-GCM which are both acceptable.
- Session cache enabled: 10MB cache holds ~40,000 sessions, reducing handshake overhead for returning clients.
Rate Limiting
Rate limiting is configured per zone, not per-request:
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;
limit_conn_zone $binary_remote_addr zone=addr:10m;
The $binary_remote_addr key uses the client IP in binary format (4 bytes for IPv4, 16 for IPv6), minimizing memory usage. The 10MB zone stores ~80,000 entries.
Certbot and Let's Encrypt
Certificate Lifecycle
The Certbot container handles the full certificate lifecycle:
- Initial issuance: On first start, certbot performs HTTP-01 challenge via webroot at
/var/www/certbot - Auto-renewal: A cron job runs every 12 hours (
0 */12 * * *) to check for renewals - Grace period: Let's Encrypt certificates are valid for 90 days. Certbot auto-renews at 30 days remaining, providing 60 days of buffer
Why Webroot Mode
Certbot supports multiple challenge types. Webroot mode (--webroot -w /var/www/certbot) is the best choice for this stack:
- No port conflicts: Unlike standalone mode, the ACME challenge is served through Nginx on port 80
- No downtime: Certbot does not need to stop Nginx or bind to ports
- Works within Docker: The webroot is a shared volume between Nginx and Certbot containers
CrowdSec WAF/IPS
How CrowdSec Works
CrowdSec is a behavior-based security engine. It ingests logs, detects attacks using community-contributed scenarios, and blocks malicious IPs at the reverse proxy level:
- Log parsing: CrowdSec reads Nginx access logs (mounted as a volume)
- Scenario matching: Pre-built scenarios detect SQL injection, XSS, path traversal, brute-force, and scanner activity
- Decision propagation: When a scenario triggers, CrowdSec creates a decision (ban, captcha, throttle) that the bouncer enforces
- Community blocklist: CrowdSec shares anonymized attack data, providing a global blocklist updated in real-time
Configuration
The CrowdSec container is configured with the Nginx collection, which includes:
crowdsecurity/nginx: Base Nginx parsercrowdsecurity/http-cve: HTTP vulnerability scanning detectioncrowdsecurity/http-bf: Brute force detection on login endpointscrowdsecurity/http-path-traversal: Path traversal attempts
The bouncer runs as a separate container (nftables-based) that communicates with CrowdSec via API:
# View blocked IPs
docker compose exec crowdsec cscli decisions list
# Manually ban an IP
docker compose exec crowdsec cscli decisions add --ip 1.2.3.4 --duration 24h
Production Hardening Checklist
- Disable server tokens (
server_tokens off) — prevents version disclosure - Set
client_max_body_sizeper application — default 1MB blocks file uploads - Configure
limit_reqper upstream — prevents one slow service from starving others - Set
proxy_read_timeoutappropriately — long-polling endpoints need 300s+ - Configure real IP from Docker networks — preserves original client IP behind Docker networking
- Remove Watchtower profile in production — never auto-update reverse proxy
- Backup
/etc/letsencrypt— losing SSL certs requires re-issuance and re-propagation - Monitor CrowdSec metrics —
cscli metricsshows detection volume and blocked IPs
Key Takeaways
- A reverse proxy is your first line of defense — TLS termination, rate limiting, and WAF should never be handled by application servers
- Certbot webroot mode is Docker-friendly — works behind Nginx without port conflicts
- CrowdSec autodetects attacks — no need to manually write WAF rules for common attacks
- The stack is service-agnostic — add any upstream container and it's protected