Tutorial

Blocking IP addresses with Caddy

October 7, 2025
caddy security ip-blocking bots

Caddy is a modern web server designed for simplicity and security. One of the ways you can harden your server is by blocking unwanted IP addresses directly at the Caddy layer — before requests reach your applications or APIs. This guide shows several effective methods to block or limit traffic using built-in features, reusable snippets, and optional extensions.

For the complete official documentation, see Caddy Documentation.

Method 1: Basic IP Blocking with remote_ip

Caddy provides the remote_ip matcher to target clients based on their IP address or network range. You can use it to block specific IPs or CIDR subnets directly in your site block.

example.com {
	# Match incoming requests from blocked IP ranges
	@blocked {
		remote_ip 142.44.220.0/24
		remote_ip 148.113.128.0/24
		remote_ip 15.235.27.0/24
		remote_ip 198.244.168.0/24
	}

	# Immediately close connection for blocked IPs
	abort @blocked

	# Normal site configuration
	root * /var/www/html
	file_server
}

The @blocked matcher defines the list of IP addresses or CIDR ranges to block. When a request matches, Caddy uses the abort directive to immediately close the connection without sending any response. This is more secure than sending a 403 error because it provides no information to attackers and uses fewer resources. Non-matching requests are handled normally.

Method 2: Whitelisting Specific IPs

You can also restrict access to certain resources by allowing only approved IPs and denying all others.

example.com {
	# Deny all requests not from this specific IP
	@not_allowed {
		not remote_ip 192.168.1.50
	}

	abort @not_allowed

	root * /var/www/html
	file_server
}

This approach is useful for private areas, internal dashboards, or staging sites that should only be accessible from known networks.

You can also use the private_ranges shortcut to allow only private network traffic and block all public IPs:

example.com {
	# Block all public IPs, allow only private networks
	@public {
		not remote_ip private_ranges
	}

	abort @public

	root * /var/www/html
	file_server
}

The private_ranges shortcut automatically includes all private IPv4 and IPv6 ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.1/8, etc.).

Method 3: Blocking IPs Behind a Proxy or Load Balancer

Important: If your Caddy server sits behind a reverse proxy, load balancer, or CDN (such as Cloudflare, AWS ALB, Nginx, or HAProxy), the remote_ip matcher will only see the proxy's IP address, not the real client IP. This makes IP blocking ineffective.

To block real client IPs in proxied environments, you need to:

  1. Configure trusted_proxies globally to tell Caddy which proxy IPs to trust
  2. Use the client_ip matcher instead of remote_ip
{
	servers {
		trusted_proxies static private_ranges
	}
}

example.com {
	# Match real client IPs from headers like X-Forwarded-For
	@blocked {
		client_ip 142.44.220.0/24
		client_ip 148.113.128.0/24
		client_ip 15.235.27.0/24
	}

	abort @blocked

	root * /var/www/html
	file_server
}

When trusted_proxies is configured, Caddy parses the real client IP from HTTP headers like X-Forwarded-For for requests coming from trusted proxy addresses. The client_ip matcher then uses this parsed IP instead of the immediate connection IP.

Common trusted_proxies configurations:

# Trust all private network ranges (common for internal load balancers)
trusted_proxies static private_ranges

# Trust specific proxy IPs
trusted_proxies static 10.0.1.5 10.0.1.6

# Trust a CIDR range
trusted_proxies static 172.16.0.0/12

Without trusted_proxies configured, client_ip behaves identically to remote_ip.

Method 4: External Blocklist File

If you manage a large or frequently updated list of IPs, you can store them in a separate file and include them as a reusable configuration snippet.

Create /etc/caddy/blocklist.caddy:

@blocked {
	remote_ip 142.44.220.0/24
	remote_ip 148.113.128.0/24
	remote_ip 15.235.27.0/24
	remote_ip 198.244.168.0/24
}

Then include it in your main Caddyfile:

example.com {
	import /etc/caddy/blocklist.caddy

	abort @blocked

	root * /var/www/html
	file_server
}

Using an external blocklist keeps your main configuration file clean, makes it easy to update the list automatically (e.g., with a cron job pulling from an IP threat feed), and allows sharing the blocklist across multiple sites or services.

Method 5: Advanced Blocking Patterns

You can combine IP blocking with other matchers to create more sophisticated blocking rules.

Block specific IPs only on admin paths:

example.com {
	@blocked_admin {
		remote_ip 142.44.220.0/24 148.113.128.0/24
		path /admin/* /dashboard/*
	}

	abort @blocked_admin

	root * /var/www/html
	file_server
}

Block IPs from specific API operations:

api.example.com {
	@blocked_api {
		remote_ip 198.244.168.0/24
		path /api/*
		method POST PUT DELETE
	}

	abort @blocked_api

	reverse_proxy localhost:8080
}

Block IPs except for specific safe paths:

example.com {
	@blocked_except_health {
		remote_ip 203.0.113.0/24
		not path /health /status
	}

	abort @blocked_except_health

	reverse_proxy localhost:8080
}

These patterns allow you to implement granular access control by combining IP blocking with path, method, header, or other request attributes.

Method 6: Dynamic IP Blocking with Fail2Ban

For automated defense against repeated attacks, integrate Caddy with Fail2ban. This monitors Caddy logs and dynamically blocks IPs at the system firewall after they exceed failure thresholds.

Important: Caddy uses JSON structured logging by default. Your Fail2ban filter must parse JSON format.

Create a filter file /etc/fail2ban/filter.d/caddy-deny.conf:

[Definition]
# Match JSON log entries with 403 status
failregex = .*"remote_ip":"<HOST>".*"status":403
ignoreregex =

Add a jail configuration in /etc/fail2ban/jail.local:

[caddy-deny]
enabled = true
filter = caddy-deny
action = iptables-multiport[name=CaddyDeny, port="http,https", protocol=tcp]
logpath = /var/log/caddy/access.log
findtime = 600
bantime = 7200
maxretry = 5

This blocks IPs at the system firewall once they repeatedly trigger 403 responses in your Caddy logs.

Testing and Reloading Configuration

Caddy provides built-in configuration validation similar to nginx -t.

# Validate Caddyfile syntax
caddy validate --config /etc/caddy/Caddyfile

# If validation passes, reload Caddy safely
caddy reload --config /etc/caddy/Caddyfile

What caddy validate checks:

The command deserializes your configuration, then loads and provisions all modules to ensure they're properly configured.

Example output:

Valid configuration

If there are errors, Caddy will show the file and line number with an explanation:

Error: /etc/caddy/Caddyfile:15 - parsing caddyfile tokens for 'remote_ip': wrong argument count or unexpected line ending after 'remote_ip'

Always test before reloading:

# Safe workflow
caddy validate --config /etc/caddy/Caddyfile && caddy reload --config /etc/caddy/Caddyfile

Troubleshooting

IP blocking not working behind a proxy or load balancer?

  • Problem: The remote_ip matcher sees only the proxy's IP, not the real client
  • Solution: Configure trusted_proxies in global options and use client_ip matcher instead
  • Verify: Check Caddy access logs to see what IP is being matched
{
	servers {
		trusted_proxies static private_ranges
	}
}

example.com {
	@blocked {
		client_ip 142.44.220.0/24  # Use client_ip, not remote_ip
	}
	abort @blocked
}

How to test if blocking is working?

  • Using curl: curl -v https://example.com from a blocked IP should show connection closed or timeout
  • Check logs: Review Caddy access logs to verify blocked IPs are being matched
  • Test from different IPs: Use a VPN or different network to verify allowed traffic works

Configuration validation fails?

  • Always validate before reloading: caddy validate --config /etc/caddy/Caddyfile
  • Check syntax carefully: Matchers must be inside @name { } blocks
  • Review error messages: Caddy shows the exact file and line number with errors

Blocked IPs still getting through?

  • Check matcher syntax: Ensure CIDR notation is correct (e.g., 192.168.1.0/24)
  • Verify directive order: The abort directive should come before other handlers
  • Look for multiple site blocks: Ensure blocking is applied to the correct site block
  • Behind a CDN? Services like Cloudflare require special configuration to see real IPs

Best Practices

  • Use abort over respond: The abort directive immediately closes connections without sending any response, providing no information to malicious actors and using fewer resources
  • Choose the right matcher: Use remote_ip for direct connections, client_ip for traffic behind proxies or load balancers
  • Configure trusted_proxies: Essential for accurate IP matching when Caddy sits behind reverse proxies, load balancers, or CDNs
  • Use private_ranges shortcut: Convenient built-in matcher for all private IPv4 and IPv6 network ranges
  • Monitor access logs: Review what IPs you're blocking and their behavior to refine your rules
  • Keep lists updated: Regularly review and update blocklists to stay current with threats
  • Use CIDR ranges: Simplify lists by blocking entire subnets when appropriate rather than individual IPs
  • Automate updates: Refresh blocklists periodically from trusted threat intelligence sources
  • Combine layers: Use both Caddy-level blocking and OS-level firewalls (iptables, nftables) for defense in depth
  • Test before applying: Always run caddy validate before reloading configuration to catch errors
  • Understand JSON logging: Caddy uses structured JSON logs by default, which affects log parsing for monitoring tools and Fail2ban filters