IP Restriction #

In a layered defense strategy (defense in depth), restricting access based on the client’s IP address is one of the most direct, robust, and efficient security methods. By filtering traffic at the entrance gate level, you can isolate sensitive endpoints (like admin panels or internal monitoring metrics) so only trusted teams can access them, while instantly blocking malicious actors’ IP addresses. Caddy provides the built-in remote_ip matcher supporting CIDR notation to dynamically filter single IP addresses and subnet ranges. We’ll discuss in depth the Allowlist and Blocklist pattern configurations, the importance of Trusted Proxies configuration to prevent IP spoofing, Geo-blocking tactics, special logging for audits, and Server-Side Request Forgery (SSRF) attack mitigation in cloud architectures.


The remote_ip Matcher and CIDR Notation #

Caddy’s remote_ip matcher detects the origin IP address sending the request and matches it against the rule list you define. This matcher runs very fast at the request processing level because it only does binary matching on the IP data structure.

You can define IP address matching using single address notation or CIDR block ranges (Classless Inter-Domain Routing):

# Basic IP blocking example
example.com {
    # Filter a single IP and custom subnet ranges
    @bad_client remote_ip 203.0.113.5 198.51.100.0/24
    respond @bad_client "Access Denied!" 403
    
    file_server {
        root /var/www/html
    }
}

The IP address and subnet writing formats supported by Caddy include:

  • Single Address (IPv4 / IPv6): Targets one specific device (e.g., 192.168.1.10 or 2001:db8::1).
  • CIDR Notation (Subnetting): Targets an entire cluster of network addresses. The /24 format on IPv4 represents 256 IP addresses (e.g., 192.168.1.0/24 covers 192.168.1.0 to 192.168.1.255), while /16 represents 65,536 IP addresses.
  • Standard Private Networks (RFC 1918):
    • 10.0.0.0/8 (Large private IP range, commonly used in cloud VPCs).
    • 172.16.0.0/12 (Medium private IP range).
    • 192.168.0.0/16 (Local LAN private IP range for offices/homes).
  • Loopback Address: 127.0.0.1/32 (IPv4) and ::1 (IPv6), referring to the local machine itself.

Allowlist vs Blocklist Patterns #

When designing network security policies, you can choose between two main strategies: Allowlist (allow only the known, block the rest) or Blocklist (allow everything, block the suspected).

+--------------------------------------------------------------------------+
||                       IP Filtering Pattern Comparison                   ||
+--------------------------------------------------------------------------+
||  Allowlist Pattern (High Security Level):                               ||
||  [All Requests] ---> {Is the IP Registered?} -- Yes --> [Allow In]      ||
||                                              -- No ---> [Block (403)]  ||
||                                                                          ||
||  Blocklist Pattern (High Accessibility Level):                           ||
||  [All Requests] ---> {Is the IP Blocked?} -- Yes --> [Block (403)]      ||
||                                              -- No ---> [Allow In]      ||
+--------------------------------------------------------------------------+

1. The Allowlist Pattern (Permission List) #

This pattern is highly recommended for securing corporate internal sites, audit dashboards, or administrative endpoints. Philosophically, you consider all internet connections as threats unless the client IP address is registered in your permission list:

# CORRECT: Using handle to lock the entire site with an allowlist
admin-panel.example.com {
    # Matcher to filter out anyone who is NOT part of our office
    @not_office {
        not remote_ip 203.0.113.100/32 203.0.113.101/32 192.168.0.0/16
    }
    
    # Reject requests outside the office immediately with a custom error message
    respond @not_office "Access Denied: Only the internal office network is allowed." 403
    
    reverse_proxy localhost:9000
}

2. The Blocklist Pattern (Block List) #

This pattern is commonly used for public websites (like e-commerce or blogs). You want everyone to access the web page, but you want to block certain IPs detected doing vulnerability scanning attacks or spamming:

# Dynamic Blocklist application
example.com {
    # Matcher containing malicious actor IPs detected by the monitoring system
    @blocked_clients {
        remote_ip 198.51.100.50
        remote_ip 203.0.113.0/24
    }
    
    respond @blocked_clients 403
    
    file_server {
        root /var/www/html
    }
}

Path-Level IP Restriction #

Often you don’t want to lock the entire website, but only secure certain subdirectories (like /admin/ or /metrics) without disturbing public visitors’ access to the main pages.

You can combine path and IP matchers inside a Caddyfile named matcher:

# Securing sensitive endpoints specifically
example.com {
    # 1. Lock the admin panel route to office IPs only
    @admin_area {
        path /admin/*
        not remote_ip 203.0.113.100/32 10.0.0.0/8
    }
    respond @admin_area "Forbidden" 403

    # 2. Lock the Prometheus Metrics endpoint to our monitoring server only
    @monitoring_area {
        path /metrics /status
        not remote_ip 10.10.1.50/32
    }
    respond @monitoring_area "Access Denied" 403

    # Normal public traffic
    root * /var/www/public
    file_server
}

Layered Defense (IP Restriction + Basic Auth) #

Relying on IP filtering alone has risks if your office IP addresses change or if employees are working from outside the office (remote working). For this, you must apply Layered Defense (Defense in Depth) by combining IP restrictions with Basic Auth authentication:

# Combining IP restriction and Basic Auth for admin panel protection
admin.example.com {
    # Layer 1: Only allow office IPs and office VPN IPs
    @unauthorized {
        not remote_ip 203.0.113.100/32 10.8.0.0/24
    }
    respond @unauthorized "Unknown network" 403

    # Layer 2: Require Basic Auth for everything that passes the IP check
    basicauth {
        sysadmin $2a$14$sysadminBcryptHashHere
    }

    # Layer 3: Special audit logging
    log {
        output file /var/log/caddy/admin_security.log
        format json
    }

    reverse_proxy localhost:8080
}

IP Restriction Behind a CDN / Load Balancer (Trusted Proxies) #

In modern production environments, your Caddy server is almost always placed behind a Load Balancer (like AWS ALB) or a Content Delivery Network (like Cloudflare).

If this happens, the TCP connections received by the Caddy server’s OS no longer come from the real visitor IP, but from the internal Load Balancer IP or Cloudflare’s public IPs. As a result, Caddy’s built-in remote_ip matcher reads the Load Balancer IP, making your filtering rules ineffective (all visitors are considered to have the same IP).

To overcome this, the intermediary proxy usually inserts the HTTP X-Forwarded-For header containing the visitor’s real IP address. However, Caddy won’t read this header automatically because it can be easily manipulated by attackers (IP Spoofing) if not validated.

You must configure the trusted_proxies option in Caddy’s global options block to register your Load Balancer/CDN IP addresses. Once configured, Caddy validates the connection and safely extracts the visitor’s real IP from the X-Forwarded-For header for use in the remote_ip matcher:

# Trusted Proxies global options configuration
{
    servers {
        # Define the proxy/load balancer IP addresses we trust
        # Example: All our AWS VPC internal subnets
        trusted_proxies static 10.0.0.0/8
        
        # Example: If using Cloudflare, register all Cloudflare public IP ranges
        # trusted_proxies static 103.21.244.0/22 103.22.200.0/22 104.16.0.0/13
    }
}

example.com {
    # After trusted_proxies is active, remote_ip reads the real visitor IP
    # from the X-Forwarded-For header safely.
    @bad_client remote_ip 198.51.100.50
    respond @bad_client 403

    reverse_proxy backend:8080
}

[!WARNING] Never include trusted_proxies static 0.0.0.0/0 (trusting all IPs on the internet). This action exposes your server to high-level IP Spoofing attacks, where attackers can send requests with a manipulated custom X-Forwarded-For header to pretend to be your corporate internal IP and bypass all security rules.


Geo-blocking Mechanism #

Restricting access based on geographic location (country) is very useful for protecting websites from massive spam attacks or aligning content distribution with certain countries’ copyright laws.

There are two main approaches for configuring Geo-blocking on Caddy:

1. Leveraging CDN Headers (Cloudflare CF-IPCountry) #

If your website uses Cloudflare as a CDN, Cloudflare automatically inserts the CF-IPCountry response header containing the two-letter ISO country code (e.g., ID, SG, US). You can filter requests based on this header directly:

# Blocking traffic from certain countries using the CDN header
example.com {
    @blocked_countries {
        # Block access from RU (Russia) and CN (China)
        header CF-IPCountry RU
        header CF-IPCountry CN
    }
    
    respond @blocked_countries "Access from your region is restricted." 403
    
    file_server
}

2. Using a Local Geolocation Database (MaxMind GeoIP) #

If you don’t use Cloudflare and want to do self-hosted IP mapping independently, you must compile Caddy with the caddy-maxmind-geolocation plugin and download the GeoIP database (.mmdb) from MaxMind:

# Compile Caddy with the MaxMind Geolocation plugin
xcaddy build --with github.com/porech/caddy-maxmind-geolocation

After installation, set up the database configuration in the Caddyfile:

# Filtering using a local MaxMind GeoIP database
example.com {
    @not_indonesia {
        # Evaluate IPs using the MaxMind module
        maxmind_geolocation {
            db_path /var/lib/GeoIP/GeoLite2-Country.mmdb
            # Invert the logic: block if the country is NOT ID (Indonesia) or SG (Singapore)
            not allow_countries ID SG
        }
    }
    
    respond @not_indonesia "This service is only available in Indonesia and Singapore." 403
    
    reverse_proxy localhost:3000
}

Special Logging for Blocked IP Audits #

To detect bot scanning attack patterns and monitor your filtering rules’ effectiveness, you must record every blocked request into a special security log file:

# Centralized audit log configuration
example.com {
    # IP blocklist
    @blocked_ip remote_ip 198.51.100.0/24

    # Create a special sub-route to log before dropping the connection
    handle @blocked_ip {
        log {
            output file /var/log/caddy/security_blocks.log
            format json
        }
        respond "Forbidden" 403
    }

    # Normal traffic
    root * /var/www/html
    file_server
}

You can analyze that log data using standard terminal commands:

# Monitor IP blocking logs in real time
tail -f /var/log/caddy/security_blocks.log | jq -r '"[\(.ts)] BLOCKED: \(.request.remote_addr) -> Path: \(.request.uri)"'

Log-Based Dynamic IP Blocking (Scripted Auto-Blocking) #

In production environments receiving relentless spam attacks, you can create an automated Bash script running on the server as a cron job to scan Caddy’s main access log, filter suspicious IPs (e.g., IPs triggering HTTP 4xx > 100 times within 5 minutes), then dynamically write them into Caddy’s blocklist file:

#!/bin/bash
# caddy-auto-blocker.sh
# Analyzes Caddy access logs and updates the IP blocklist automatically

LOG_FILE="/var/log/caddy/access.log"
BLOCK_LIST="/etc/caddy/blocklist.txt"
THRESHOLD=100
WINDOW_SECONDS=300

# 1. Detect suspicious IPs producing 4xx/5xx statuses within the last 5 minutes
SUSPICIOUS_IPS=$(cat "$LOG_FILE" | \
    jq -r 'select(.ts > (now - '"$WINDOW_SECONDS"') and .status >= 400) | .request.remote_addr' | \
    cut -d: -f1 | sort | uniq -c | \
    awk -v t="$THRESHOLD" '$1 > t {print $2}')

if [ -z "$SUSPICIOUS_IPS" ]; then
    echo "No suspicious activity detected."
    exit 0
fi

echo "Detected suspicious IP addresses: $SUSPICIOUS_IPS"

# 2. Update the blocklist file
for ip in $SUSPICIOUS_IPS; do
    # Add the IP to the file if not already registered
    if ! grep -q "$ip" "$BLOCK_LIST" 2>/dev/null; then
        echo "Adding $ip to the blocklist..."
        echo "remote_ip $ip" >> "$BLOCK_LIST"
    fi
done

# 3. Trigger a Caddy configuration reload so the changes take effect
echo "Reloading the Caddy configuration..."
sudo systemctl reload caddy

On the Caddyfile side, you just import that blocklist.txt file into a named matcher:

# Importing the blocklist from an external file
example.com {
    @blacklisted_clients {
        # Import the file updated by the automatic script
        import /etc/caddy/blocklist.txt
    }
    respond @blacklisted_clients "Your access is restricted due to suspicious activity." 403

    file_server { root /var/www/html }
}

Securing CI/CD Webhooks Using an IP Allowlist #

If your website has a webhook endpoint (e.g., /webhooks/deploy) that triggers automatic deployment processes from external Git services like GitHub, you must restrict that endpoint’s access so only official GitHub IP addresses can call it.

This prevents external actors from sending fake requests to trigger looping build processes on your server:

# Webhook deployment endpoint protection
example.com {
    # Only allow webhook access from official GitHub Actions IPs
    # (IPs obtained from the GitHub metadata endpoint)
    @not_github {
        path /webhooks/deploy
        not remote_ip 192.30.252.0/22 185.199.108.0/22 140.82.112.0/20
    }
    respond @not_github "Unauthorized Webhook Source" 403

    # Normal routes
    reverse_proxy localhost:3000
}

Preventing SSRF by Blocking Cloud Metadata Access #

When you run the Caddy server in a cloud provider environment (like AWS EC2, Google Cloud, or DigitalOcean), the provider offers an internal metadata endpoint accessible by VMs at the special IP address 169.254.169.254 without authentication. This endpoint contains sensitive information about IAM credentials, access tokens, and VM configuration.

If your backend application has a Server-Side Request Forgery (SSRF) security hole where users can instruct the server to make requests to custom URLs, attackers can exploit it to read that metadata data.

You can configure Caddy to act as a front shield by blocking requests trying to access that cloud metadata IP range:

# Cloud Metadata SSRF attack mitigation
example.com {
    # Detect requests trying to route to Link-Local / Metadata IPs
    @metadata_attack {
        path /proxy/* /fetch/*
        remote_ip 169.254.0.0/16
    }
    respond @metadata_attack "Access to the local network is forbidden!" 403

    reverse_proxy localhost:8080
}

Summary #

  • remote_ip Matcher: Use the remote_ip matcher to restrict access based on single IP addresses or subnets using CIDR notation.
  • Allowlist Strategy: For sensitive data (admin panel/metrics), always apply the Allowlist pattern (deny all, allow only trusted IPs) using not remote_ip.
  • Trusted Proxies Configuration: If Caddy runs behind a CDN/Load Balancer, you must configure trusted_proxies in the global options so Caddy can safely read the real visitor IP.
  • Efficient Geo-blocking: Do geographic blocking using the CF-IPCountry header if using Cloudflare, or the MaxMind GeoIP plugin for a self-hosted solution.
  • SSRF Prevention: Protect your cloud infrastructure by blocking requests targeting the internal metadata IP range 169.254.0.0/16.
  • Log Audit Trail: Record every blocked request into a separate log file (security_blocks.log) for analysis by automatic attack prevention scripts.

← Previous: Rate Limiting   Next: Security Headers →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact