Configure Caddy 2 web server with comprehensive rate limiting, request throttling, and DDoS protection using built-in security modules and advanced filtering rules.
Prerequisites
- Root or sudo access
- Domain name pointed to your server
- Basic understanding of web server configuration
What this solves
Caddy 2 provides built-in rate limiting and DDoS protection capabilities that help protect your web applications from malicious traffic and resource exhaustion attacks. This tutorial shows you how to implement comprehensive security rules including request rate limiting, connection throttling, IP-based filtering, and automated threat response to keep your services available under attack conditions.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you have the latest security updates.
sudo apt update && sudo apt upgrade -y
Install Caddy 2 with security modules
Install Caddy 2 from the official repository to get the latest version with all security features enabled.
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install -y caddy
Create directory structure
Set up the necessary directories for Caddy configuration and security rules with proper permissions.
sudo mkdir -p /etc/caddy/security
sudo mkdir -p /var/log/caddy
sudo mkdir -p /var/www/html
sudo chown -R caddy:caddy /var/log/caddy
sudo chmod 755 /etc/caddy /etc/caddy/security
Configure basic rate limiting
Create a Caddyfile with fundamental rate limiting rules that protect against basic flooding attacks.
{
# Global rate limiting options
order rate_limit before basicauth
servers {
max_header_bytes 16KB
}
}
example.com {
# Enable access logging
log {
output file /var/log/caddy/access.log
format single_field common_log
}
# Basic rate limiting - 100 requests per minute per IP
rate_limit {
zone static_ip {
key {remote_host}
events 100
window 1m
}
# More restrictive for API endpoints
zone api_ip {
key {remote_host}
events 20
window 1m
}
}
# Apply rate limits to specific paths
@api path /api/*
rate_limit @api api_ip
# Default rate limit for all other requests
rate_limit static_ip
# Root directory
root * /var/www/html
file_server
}
Add advanced DDoS protection rules
Enhance the configuration with sophisticated protection against various types of DDoS attacks.
{
order rate_limit before basicauth
order request_body before rate_limit
servers {
max_header_bytes 16KB
read_timeout 30s
read_header_timeout 10s
write_timeout 60s
idle_timeout 120s
}
}
example.com {
log {
output file /var/log/caddy/access.log
format single_field common_log
}
# Request body size limits to prevent large payload attacks
request_body {
max_size 10MB
}
# Multiple rate limiting zones for different threat levels
rate_limit {
# Standard web traffic - 200 req/min per IP
zone web_traffic {
key {remote_host}
events 200
window 1m
}
# API endpoints - 50 req/min per IP
zone api_strict {
key {remote_host}
events 50
window 1m
}
# Authentication endpoints - 10 req/min per IP
zone auth_endpoints {
key {remote_host}
events 10
window 1m
}
# Search/query endpoints - 30 req/min per IP
zone search_endpoints {
key {remote_host}
events 30
window 1m
}
# File upload endpoints - 5 req/min per IP
zone upload_endpoints {
key {remote_host}
events 5
window 1m
}
}
# Define request matchers for different endpoint types
@auth path /login /register /auth/ /oauth/
@api path /api/*
@search path /search /query
@upload path /upload /files/
@static path .css .js .png .jpg .gif .ico .woff *.ttf
# Apply appropriate rate limits
rate_limit @auth auth_endpoints
rate_limit @api api_strict
rate_limit @search search_endpoints
rate_limit @upload upload_endpoints
# Lenient rate limiting for static assets
@static_with_rate {
path .css .js .png .jpg .gif .ico .woff *.ttf
}
rate_limit @static_with_rate {
zone static_assets {
key {remote_host}
events 500
window 1m
}
}
# Default rate limit for other requests
rate_limit web_traffic
root * /var/www/html
file_server
}
Implement IP blocking and security headers
Add IP-based access control and security headers to strengthen protection against attacks.
# Blocked IP addresses - one per line
192.168.1.100
10.0.0.50
{
order rate_limit before basicauth
order request_body before rate_limit
servers {
max_header_bytes 16KB
read_timeout 30s
read_header_timeout 10s
write_timeout 60s
idle_timeout 120s
}
}
example.com {
log {
output file /var/log/caddy/access.log
format single_field common_log
}
# Import blocked IPs (create matcher for blocked IPs)
@blocked_ips {
remote_ip 192.0.2.0/24 198.51.100.0/24
}
# Immediately respond with 403 for blocked IPs
respond @blocked_ips "Access Denied" 403
# Block requests with suspicious User-Agent strings
@malicious_ua {
header User-Agent bot crawler scanner nikto sqlmap
}
respond @malicious_ua "Forbidden" 403
# Block requests with no User-Agent
@no_ua {
not header User-Agent *
}
respond @no_ua "Bad Request" 400
# Security headers for all responses
header {
# Prevent clickjacking
X-Frame-Options DENY
# Prevent MIME type sniffing
X-Content-Type-Options nosniff
# Enable XSS protection
X-XSS-Protection "1; mode=block"
# Strict transport security
Strict-Transport-Security "max-age=31536000; includeSubDomains"
# Content security policy
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
# Referrer policy
Referrer-Policy "strict-origin-when-cross-origin"
# Remove server information
-Server
-X-Powered-By
}
# Request body size limits
request_body {
max_size 10MB
}
# Enhanced rate limiting with multiple zones
rate_limit {
zone web_traffic {
key {remote_host}
events 200
window 1m
}
zone api_strict {
key {remote_host}
events 50
window 1m
}
zone auth_endpoints {
key {remote_host}
events 10
window 1m
}
zone search_endpoints {
key {remote_host}
events 30
window 1m
}
zone upload_endpoints {
key {remote_host}
events 5
window 1m
}
# Burst protection - very restrictive short-term limit
zone burst_protection {
key {remote_host}
events 10
window 10s
}
}
# Apply burst protection to all requests first
rate_limit burst_protection
# Path-specific rate limiting
@auth path /login /register /auth/ /oauth/
@api path /api/*
@search path /search /query
@upload path /upload /files/
rate_limit @auth auth_endpoints
rate_limit @api api_strict
rate_limit @search search_endpoints
rate_limit @upload upload_endpoints
rate_limit web_traffic
root * /var/www/html
file_server
}
Set up connection limits and timeouts
Configure connection-level protections to prevent resource exhaustion attacks.
{
order rate_limit before basicauth
order request_body before rate_limit
# Global server settings with connection limits
servers {
max_header_bytes 16KB
read_timeout 30s
read_header_timeout 10s
write_timeout 60s
idle_timeout 120s
# Connection limits per IP
listener_wrappers {
http_redirect
tls
}
# Protocol settings
protocols h1 h2
}
# Error handling
handle_errors {
@404 expression {http.error.status_code} == 404
@429 expression {http.error.status_code} == 429
@500 expression {http.error.status_code} >= 500
handle @429 {
respond "Rate limit exceeded. Please try again later." 429
}
handle @404 {
respond "Page not found" 404
}
handle @500 {
respond "Internal server error" 500
}
}
}
example.com {
log {
output file /var/log/caddy/access.log {
roll_size 100MB
roll_keep 10
}
format single_field common_log
level INFO
}
# Enhanced IP blocking with network ranges
@blocked_networks {
remote_ip 192.0.2.0/24 198.51.100.0/24 203.0.113.0/24
}
respond @blocked_networks "Access Denied" 403
# Block common attack patterns
@attack_patterns {
query union select drop insert update delete
}
respond @attack_patterns "Malicious request detected" 403
# Block suspicious request methods
@suspicious_methods {
method TRACE OPTIONS CONNECT
}
respond @suspicious_methods "Method not allowed" 405
# Comprehensive security headers
header {
X-Frame-Options DENY
X-Content-Type-Options nosniff
X-XSS-Protection "1; mode=block"
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; media-src 'self'; object-src 'none'; child-src 'none'; frame-src 'none'; worker-src 'none'; frame-ancestors 'none'; form-action 'self'; base-uri 'self'"
Referrer-Policy "strict-origin-when-cross-origin"
Permissions-Policy "geolocation=(), microphone=(), camera=()"
X-Permitted-Cross-Domain-Policies "none"
-Server
-X-Powered-By
}
# Request size and timeout limits
request_body {
max_size 10MB
}
# Multi-tier rate limiting system
rate_limit {
# Tier 1: Burst protection (very short window)
zone burst_guard {
key {remote_host}
events 5
window 1s
}
# Tier 2: Short-term protection
zone short_term {
key {remote_host}
events 20
window 10s
}
# Tier 3: Medium-term protection
zone medium_term {
key {remote_host}
events 100
window 1m
}
# Tier 4: Long-term protection
zone long_term {
key {remote_host}
events 500
window 5m
}
# Specialized zones
zone api_endpoints {
key {remote_host}
events 30
window 1m
}
zone auth_endpoints {
key {remote_host}
events 5
window 1m
}
zone upload_endpoints {
key {remote_host}
events 3
window 1m
}
}
# Apply multi-tier rate limiting to all requests
rate_limit burst_guard
rate_limit short_term
rate_limit medium_term
rate_limit long_term
# Path-specific additional limits
@api path /api/*
@auth path /login /register /auth/ /password-reset
@upload path /upload /files/ /media/*
rate_limit @api api_endpoints
rate_limit @auth auth_endpoints
rate_limit @upload upload_endpoints
root * /var/www/html
file_server
}
Enable and start Caddy service
Start Caddy and enable it to run automatically on system boot.
sudo systemctl daemon-reload
sudo systemctl enable caddy
sudo systemctl start caddy
sudo systemctl status caddy
Create test web content
Add some basic content to test the rate limiting and security features.
sudo tee /var/www/html/index.html > /dev/null << 'EOF'
Caddy Security Test
Caddy Server with DDoS Protection
This server is protected by Caddy's rate limiting and security features.
EOF
sudo chown -R caddy:caddy /var/www/html
Configure monitoring and alerting
Set up log monitoring
Create a script to monitor Caddy logs for rate limiting events and potential attacks.
#!/bin/bash
Log file paths
ACCESS_LOG="/var/log/caddy/access.log"
ALERT_LOG="/var/log/caddy/security-alerts.log"
EMAIL="admin@example.com"
Function to send alert
send_alert() {
local message="$1"
echo "$(date): $message" >> "$ALERT_LOG"
# Uncomment to send email alerts
# echo "$message" | mail -s "Caddy Security Alert" "$EMAIL"
}
Monitor for rate limiting events
if [ -f "$ACCESS_LOG" ]; then
# Check for 429 (Too Many Requests) responses in the last minute
recent_429s=$(tail -n 1000 "$ACCESS_LOG" | grep "$(date -d '1 minute ago' '+%d/%b/%Y:%H:%M')" | grep -c " 429 ")
if [ "$recent_429s" -gt 10 ]; then
send_alert "High rate limiting activity detected: $recent_429s requests blocked in the last minute"
fi
# Check for blocked IPs (403 responses)
recent_403s=$(tail -n 1000 "$ACCESS_LOG" | grep "$(date -d '1 minute ago' '+%d/%b/%Y:%H:%M')" | grep -c " 403 ")
if [ "$recent_403s" -gt 5 ]; then
send_alert "Suspicious activity detected: $recent_403s blocked requests in the last minute"
fi
# Check for potential DDoS (high request volume from single IPs)
high_volume_ips=$(tail -n 5000 "$ACCESS_LOG" | grep "$(date '+%d/%b/%Y:%H:%M')" | awk '{print $1}' | sort | uniq -c | awk '$1 > 50 {print $2 " (" $1 " requests)"}')
if [ -n "$high_volume_ips" ]; then
send_alert "High volume requests detected from IPs: $high_volume_ips"
fi
fi
sudo chmod +x /etc/caddy/security/monitor-attacks.sh
Set up automated monitoring cron job
Schedule the monitoring script to run every minute to detect attacks quickly.
sudo crontab -e
Add this line to run the monitoring script every minute:
* /etc/caddy/security/monitor-attacks.sh
Create IP blocking automation script
Set up a script that can automatically block IPs that exceed rate limits.
#!/bin/bash
ACCESS_LOG="/var/log/caddy/access.log"
BLOCKED_IPS_FILE="/etc/caddy/security/blocked-ips.txt"
CADDYFILE="/etc/caddy/Caddyfile"
Find IPs with more than 1000 requests in the last hour
offending_ips=$(tail -n 10000 "$ACCESS_LOG" | grep "$(date -d '1 hour ago' '+%d/%b/%Y:%H')" | awk '{print $1}' | sort | uniq -c | awk '$1 > 1000 {print $2}')
for ip in $offending_ips; do
# Check if IP is already blocked
if ! grep -q "^$ip$" "$BLOCKED_IPS_FILE" 2>/dev/null; then
echo "$ip" >> "$BLOCKED_IPS_FILE"
echo "$(date): Auto-blocked IP $ip for excessive requests" >> /var/log/caddy/security-alerts.log
# Update Caddyfile with new blocked IP
# This is a simple approach - for production, consider a more sophisticated method
sed -i "/remote_ip.*203\.0\.113\.0\/24/a\ remote_ip $ip" "$CADDYFILE"
# Reload Caddy configuration
systemctl reload caddy
fi
done
sudo chmod +x /etc/caddy/security/auto-block.sh
Test rate limiting and security features
Test basic rate limiting
Use curl to test that rate limiting is working correctly by making rapid requests.
# Test burst protection (should start blocking after 5 requests per second)
for i in {1..10}; do
curl -w "%{http_code}\n" -o /dev/null -s http://example.com/
done
Test API rate limiting
for i in {1..25}; do
curl -w "%{http_code}\n" -o /dev/null -s http://example.com/api/test
sleep 1
done
Test security headers
Verify that security headers are being added to responses.
curl -I http://example.com/
Check for specific security headers
curl -s -D- http://example.com/ | grep -E "X-Frame-Options|X-Content-Type-Options|Strict-Transport-Security"
Test malicious request blocking
Verify that suspicious requests are being blocked properly.
# Test SQL injection attempt blocking
curl -w "%{http_code}\n" "http://example.com/?id=1 UNION SELECT * FROM users"
Test suspicious User-Agent blocking
curl -w "%{http_code}\n" -H "User-Agent: sqlmap/1.0" http://example.com/
Test missing User-Agent blocking
curl -w "%{http_code}\n" -H "User-Agent:" http://example.com/
Verify your setup
# Check Caddy service status
sudo systemctl status caddy
Verify Caddy configuration syntax
sudo caddy validate --config /etc/caddy/Caddyfile
Check rate limiting is working
curl -w "Status: %{http_code}, Time: %{time_total}s\n" -o /dev/null -s http://example.com/
View recent access logs
sudo tail -n 20 /var/log/caddy/access.log
Check security alerts
sudo tail -n 10 /var/log/caddy/security-alerts.log
Test security headers
curl -I http://example.com/ | grep -E "X-Frame-Options|Strict-Transport-Security|Content-Security-Policy"
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Caddy won't start | Configuration syntax error | sudo caddy validate --config /etc/caddy/Caddyfile |
| Rate limiting not working | Module not loaded or incorrect syntax | Check order rate_limit before basicauth in global options |
| All requests getting blocked | Rate limits too restrictive | Increase events count in rate limit zones |
| Logs not being written | Permission issues or disk space | sudo chown -R caddy:caddy /var/log/caddy |
| Security headers not appearing | Header directive not in right location | Move header block inside site configuration |
| Auto-blocking script not working | Script permissions or cron not running | sudo chmod +x /etc/caddy/security/auto-block.sh |
Next steps
- Compare with NGINX rate limiting implementation
- Implement additional server-level security hardening
- Set up centralized logging for security events
- Configure firewall rules as an additional security layer
- Implement backup strategies for configuration files
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Caddy 2 DDoS Protection and Rate Limiting Installation Script
# Production-ready script with multi-distro support
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Configuration
DOMAIN="${1:-example.com}"
WEBROOT="/var/www/html"
CADDY_CONFIG="/etc/caddy/Caddyfile"
LOG_DIR="/var/log/caddy"
usage() {
echo "Usage: $0 [domain.com]"
echo "Example: $0 mysite.com"
exit 1
}
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
cleanup() {
log_error "Installation failed. Cleaning up..."
systemctl stop caddy 2>/dev/null || true
rm -f /etc/systemd/system/caddy.service 2>/dev/null || true
systemctl daemon-reload 2>/dev/null || true
}
trap cleanup ERR
check_root() {
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root"
exit 1
fi
}
detect_distro() {
if [ ! -f /etc/os-release ]; then
log_error "/etc/os-release not found. Cannot detect distribution."
exit 1
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update && apt upgrade -y"
PKG_INSTALL="apt install -y"
FIREWALL_CMD="ufw"
;;
almalinux|rocky|centos|rhel|ol)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
FIREWALL_CMD="firewall-cmd"
;;
fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
FIREWALL_CMD="firewall-cmd"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
FIREWALL_CMD="firewall-cmd"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
log_info "Detected distribution: $ID"
}
update_system() {
log_info "[1/7] Updating system packages..."
$PKG_UPDATE
}
install_caddy() {
log_info "[2/7] Installing Caddy 2..."
case "$PKG_MGR" in
apt)
$PKG_INSTALL debian-keyring debian-archive-keyring apt-transport-https curl gnupg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list > /dev/null
apt update
$PKG_INSTALL caddy
;;
dnf)
$PKG_INSTALL curl
dnf copr enable -y caddy/caddy 2>/dev/null || {
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/setup.rpm.sh' | bash
}
$PKG_INSTALL caddy
;;
yum)
$PKG_INSTALL curl yum-utils
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/setup.rpm.sh' | bash
$PKG_INSTALL caddy
;;
esac
}
create_directories() {
log_info "[3/7] Creating directory structure..."
mkdir -p /etc/caddy/security
mkdir -p "$LOG_DIR"
mkdir -p "$WEBROOT"
# Create caddy user if not exists
if ! id caddy >/dev/null 2>&1; then
useradd --system --home /var/lib/caddy --create-home --shell /bin/false caddy
fi
# Set proper ownership and permissions
chown -R caddy:caddy "$LOG_DIR"
chown -R caddy:caddy "$WEBROOT"
chmod 755 /etc/caddy /etc/caddy/security
chmod 755 "$LOG_DIR" "$WEBROOT"
}
configure_caddyfile() {
log_info "[4/7] Configuring Caddy with DDoS protection..."
cat > "$CADDY_CONFIG" << EOF
{
order rate_limit before basicauth
order request_body before rate_limit
servers {
max_header_bytes 16KB
read_timeout 30s
read_header_timeout 10s
write_timeout 60s
idle_timeout 120s
}
}
$DOMAIN {
log {
output file $LOG_DIR/access.log
format single_field common_log
}
# Request body size limits
request_body {
max_size 10MB
}
# Rate limiting zones
rate_limit {
# Standard web traffic - 200 req/min per IP
zone web_traffic {
key {remote_host}
events 200
window 1m
}
# API endpoints - 50 req/min per IP
zone api_strict {
key {remote_host}
events 50
window 1m
}
# Authentication endpoints - 10 req/min per IP
zone auth_endpoints {
key {remote_host}
events 10
window 1m
}
# Search endpoints - 30 req/min per IP
zone search_endpoints {
key {remote_host}
events 30
window 1m
}
}
# Apply rate limits to specific paths
@api path /api/*
@auth path /login* /auth/* /signin* /register*
@search path /search* /query*
rate_limit @api api_strict
rate_limit @auth auth_endpoints
rate_limit @search search_endpoints
rate_limit web_traffic
# Security headers
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
X-XSS-Protection "1; mode=block"
Referrer-Policy strict-origin-when-cross-origin
-Server
}
# Block common attack patterns
@blocked {
path *.php* *wp-admin* *wp-login* *.asp* *.aspx*
}
respond @blocked 404
root * $WEBROOT
file_server
# Custom error pages
handle_errors {
respond "{err.status_code} {err.status_text}"
}
}
EOF
chmod 644 "$CADDY_CONFIG"
chown caddy:caddy "$CADDY_CONFIG"
}
configure_firewall() {
log_info "[5/7] Configuring firewall..."
case "$FIREWALL_CMD" in
ufw)
if command -v ufw >/dev/null 2>&1; then
ufw --force enable
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow 22/tcp
fi
;;
firewall-cmd)
if command -v firewall-cmd >/dev/null 2>&1; then
systemctl enable --now firewalld
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --permanent --add-service=ssh
firewall-cmd --reload
fi
;;
esac
}
setup_service() {
log_info "[6/7] Setting up Caddy service..."
systemctl daemon-reload
systemctl enable caddy
systemctl start caddy
sleep 3
}
verify_installation() {
log_info "[7/7] Verifying installation..."
if ! systemctl is-active --quiet caddy; then
log_error "Caddy service is not running"
systemctl status caddy
exit 1
fi
if ! curl -s -I "http://localhost" >/dev/null 2>&1; then
log_warn "Caddy is running but not responding to HTTP requests"
log_warn "Check your domain configuration and DNS settings"
fi
log_info "Caddy configuration test:"
caddy validate --config "$CADDY_CONFIG"
}
main() {
if [[ $# -gt 1 ]]; then
usage
fi
if [[ "$DOMAIN" == "example.com" ]]; then
log_warn "Using default domain 'example.com'. Consider specifying your domain."
fi
check_root
detect_distro
update_system
install_caddy
create_directories
configure_caddyfile
configure_firewall
setup_service
verify_installation
log_info "Installation completed successfully!"
log_info "Domain: $DOMAIN"
log_info "Config: $CADDY_CONFIG"
log_info "Logs: $LOG_DIR"
log_info "Webroot: $WEBROOT"
log_info ""
log_info "Rate limits configured:"
log_info " - Web traffic: 200 req/min per IP"
log_info " - API endpoints: 50 req/min per IP"
log_info " - Auth endpoints: 10 req/min per IP"
log_info " - Search endpoints: 30 req/min per IP"
}
main "$@"
Review the script before running. Execute with: bash install.sh