Skip to content

Dynamic IP blocking to help automations #106

@chunkychode

Description

@chunkychode

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:

  1. No runtime updates: Changing the blocklist requires modifying the Caddyfile and restarting Caddy
  2. Difficult for automation: External tools can't easily update the blocklist
  3. Container deployment challenges: In containerized environments (Docker, Kubernetes), editing configuration files inside containers is cumbersome
  4. 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

  1. Security Automation: Integration with SIEM systems, intrusion detection tools, and security orchestration platforms
  2. Incident Response: Quickly block malicious IPs during active attacks without server restart
  3. Threat Intelligence: Programmatically update blocklists from threat intelligence feeds
  4. Workflow Integration: Tools like n8n, Zapier, or custom automation can manage blocklists
  5. 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 fsnotify library 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

  1. DefenderAdmin App Module (admin.api.defender)

    • Implements caddy.App and caddy.AdminRouter interfaces
    • Manages a registry of Defender middleware instances
    • Routes API requests to appropriate Defender instances
    • Auto-loads when Caddy starts (admin.api namespace)
  2. 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)
  3. Integration with Existing System

    • Merges dynamic IPs with file-based and configured ranges
    • Updates IPChecker when blocklist changes
    • Maintains compatibility with existing features
  4. During Startup:

    • Load initial IP ranges from file
    • Merge with configured ranges
    • Initialize file watcher
    • Log number of IPs loaded
  5. On File Change:

    • Detect file write/modify events
    • Parse new contents
    • Validate CIDR formats
    • Update IPChecker with merged ranges
    • Log reload event
  6. Cleanup:

    • Close file watcher on Caddy shutdown
    • No goroutine leaks

Modified Components

File: plugin.go

  • Add BlocklistFile field to Defender struct
  • Initialize FileFetcher during Provision()
  • Register file change callback
  • Close watcher during Cleanup()

File: config.go

  • Add blocklist_file directive 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-blocklist

API 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/32

Response:

{
  "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 with go 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:

Ready to submit as PR if approved.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions