-
-
Notifications
You must be signed in to change notification settings - Fork 20
Description
Problem Statement
Currently, caddy-defender only supports IP ranges defined directly in the Caddyfile configuration. While this works for static, predefined ranges (like openai, aws, etc.), it has limitations:
- No runtime updates: Changing the blocklist requires modifying the Caddyfile and restarting Caddy
- Difficult for automation: External tools can't easily update the blocklist
- Container deployment challenges: In containerized environments (Docker, Kubernetes), editing configuration files inside containers is cumbersome
- Separation of concerns: Mixing frequently-changing IP lists with static configuration is not ideal
Proposed Solution
Add support for loading IP ranges from an external file and a REST API integrated with Caddy's existing admin API to manage the blacklist with automatic reload capability. The file would:
- Contain one IP address or CIDR range per line
- Be monitored for changes using file system events
- Automatically reload when modified (no Caddy restart required)
- Support bind-mounting from the host in Docker/container environments
- Merge with existing configured ranges
The endpoints would be:
- GET /defender/blocklist - List all blocked IPs from all sources (file-based, dynamic, configured)
- POST /defender/blocklist - Add IPs to the dynamic blocklist
- DELETE /defender/blocklist/{ip} - Remove IPs from the dynamic blocklist
- GET /defender/stats - View statistics about blocked ranges
Use Cases
- Security Automation: Integration with SIEM systems, intrusion detection tools, and security orchestration platforms
- Incident Response: Quickly block malicious IPs during active attacks without server restart
- Threat Intelligence: Programmatically update blocklists from threat intelligence feeds
- Workflow Integration: Tools like n8n, Zapier, or custom automation can manage blocklists
- Monitoring: Real-time visibility into what IPs are currently blocked
Architecture
The implementation uses Caddy's built-in AdminRouter interface to expose admin API endpoints. This approach:
- Leverages Caddy's existing admin API infrastructure
- Inherits Caddy's admin API security (listen address restrictions)
- Follows Caddy's architectural patterns for modules
- Uses
fsnotifylibrary for efficient file system monitoring: -
- Event-driven: Only reloads when file actually changes
-
- No polling: Minimal CPU overhead
-
- Cross-platform: Works on Linux, macOS, Windows
-
- Graceful handling: Handles file rotation, atomic writes, etc.
Components
-
DefenderAdmin App Module (
admin.api.defender)- Implements
caddy.Appandcaddy.AdminRouterinterfaces - Manages a registry of Defender middleware instances
- Routes API requests to appropriate Defender instances
- Auto-loads when Caddy starts (admin.api namespace)
- Implements
-
Dynamic Blocklist (in-memory storage)
- Thread-safe map for dynamically added IPs
- Separate from file-based and configured ranges
- Persists only in memory (clears on restart)
-
Integration with Existing System
- Merges dynamic IPs with file-based and configured ranges
- Updates IPChecker when blocklist changes
- Maintains compatibility with existing features
-
During Startup:
- Load initial IP ranges from file
- Merge with configured ranges
- Initialize file watcher
- Log number of IPs loaded
-
On File Change:
- Detect file write/modify events
- Parse new contents
- Validate CIDR formats
- Update IPChecker with merged ranges
- Log reload event
-
Cleanup:
- Close file watcher on Caddy shutdown
- No goroutine leaks
Modified Components
File: plugin.go
- Add
BlocklistFilefield to Defender struct - Initialize FileFetcher during Provision()
- Register file change callback
- Close watcher during Cleanup()
File: config.go
- Add
blocklist_filedirective parser - Validate file path exists and is readable
File: matchers/ip/ip.go
- Add
UpdateRanges()method to IPChecker - Thread-safe range updates
- Rebuild BART routing table
- Clear/recreate cache
Example Configurations
Basic Setup
example.com {
defender block {
blocklist_file /etc/caddy/blocklist.txt
}
reverse_proxy backend:8080
}Combined with Static Ranges
example.com {
defender block {
ranges openai aws deepseek
blocklist_file /etc/caddy/blocklist.txt
}
reverse_proxy backend:8080
}Docker Compose Example
version: '3.8'
services:
caddy:
image: caddy-defender:latest
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./blocklist.txt:/etc/caddy/blocklist.txt:ro
- caddy_data:/data
ports:
- "80:80"
- "443:443"
restart: unless-stopped
volumes:
caddy_data:Kubernetes ConfigMap Example
apiVersion: v1
kind: ConfigMap
metadata:
name: caddy-blocklist
data:
blocklist.txt: |
192.168.1.100/32
10.0.0.0/8
172.16.0.0/12
---
apiVersion: v1
kind: Pod
metadata:
name: caddy
spec:
containers:
- name: caddy
image: caddy-defender:latest
volumeMounts:
- name: blocklist
mountPath: /etc/caddy/blocklist.txt
subPath: blocklist.txt
volumes:
- name: blocklist
configMap:
name: caddy-blocklistAPI Endpoints
GET /defender/blocklist
Returns all blocked IPs from all sources with categorization.
Example Response:
{
"total": 5,
"sources": {
"file": 2,
"dynamic": 3
},
"ips": [
{"ip": "1.2.3.4/32", "source": "file"},
{"ip": "10.0.0.0/8", "source": "dynamic"}
]
}POST /defender/blocklist
Adds IPs to the dynamic blocklist.
Request:
{
"ips": ["192.168.1.100/32", "10.0.0.0/8"]
}Response:
{
"added": ["192.168.1.100/32", "10.0.0.0/8"],
"count": 2
}Requirements:
- IPs must be in CIDR format (e.g.,
192.168.1.1/32) - Validation ensures correct format before adding
- Idempotent (adding same IP twice is safe)
DELETE /defender/blocklist/{ip}
Removes an IP from the dynamic blocklist.
Example:
DELETE /defender/blocklist/192.168.1.100/32Response:
{
"removed": "192.168.1.100/32"
}Note: Only removes from dynamic blocklist, not file-based or configured ranges.
GET /defender/stats
Returns statistics about blocked ranges.
Example Response:
{
"configured_ranges": ["openai", "aws"],
"blocklist_file": "/etc/caddy/blocklist.txt",
"counts": {
"configured_ranges": 2,
"file_ranges": 5,
"dynamic_ranges": 3,
"total": 10
},
"responder": "block"
}Security Considerations
Authentication & Authorization
- Uses Caddy's admin API security model
- Default: Only accessible from
localhost:2019 - Can be configured with admin API listen address restrictions
- No additional authentication layer needed (inherits from Caddy)
Input Validation
- All IP inputs validated for correct CIDR format
- Malformed requests rejected with appropriate error codes
- No arbitrary code execution risk
Thread Safety
- All operations use proper mutex locking
- Safe for concurrent access from multiple clients
- No race conditions in blocklist management
Operational Security
- Dynamic blocklist only persists in memory
- Clears on restart (intentional design)
- File-based blocklist provides persistence if needed
- Separation of concerns: dynamic vs. persistent blocking
Testing
Comprehensive test suite included:
- Unit tests for all API endpoints (GET, POST, DELETE)
- Concurrent access testing
- Error handling validation
- Integration with existing IPChecker
- Edge cases (duplicate IPs, invalid formats, etc.)
Test Coverage
- ✅ Basic file loading and parsing
- ✅ CIDR format validation
- ✅ Comment and empty line handling
- ✅ File watching and reload on changes
- ✅ Concurrent read access
- ✅ Error handling (missing files, invalid formats)
- ✅ File rotation scenarios
- ✅ Atomic write operations
- ✅ Resource cleanup
Integration Tests
- ✅ IPChecker updates when file changes
- ✅ Merging with configured ranges
- ✅ Thread safety under concurrent updates
- ✅ Memory leak prevention
All tests pass withgo test ./...
Backwards Compatibility
✅ Fully backwards compatible:
- No changes to existing Caddyfile syntax
- Existing features continue working unchanged
- Admin API is opt-in (auto-loads but only accessible if admin API enabled)
- No breaking changes to any interfaces
Implementation Status
This feature has been implemented and tested on a fork:
- Repository: https://github.com/chunkychode/caddy-defender
- Docker image:
cechode/caddy-defender:latest - Production tested with n8n automation workflows
Ready to submit as PR if approved.