logo
What is IP Filtering? How to Use Allow and Deny Rules

What is IP Filtering? How to Use Allow and Deny Rules

Dec 23, 2025

Introduction

When you deploy a web application, not all traffic is created equal. Some endpoints should only be accessible from your office network. Some admin dashboards should never be accessible from the public internet. Some IP addresses are known to be malicious and should be blocked entirely.

IP filtering is a security technique that allows you to control which devices (identified by their IP address) can access your website or specific parts of it.

Think of it like a nightclub's VIP list:

  • Whitelist (Allow List): Only people on the VIP list can enter

  • Blacklist (Block List): Anyone on the "banned" list can't enter, but everyone else can

  • Route-specific filtering: Some rooms are only for VIPs, but the main floor is open to everyone

Caddy makes implementing these rules simple through a feature called "matchers" that intercept requests before they reach your application.


Understanding IP Addresses and CIDR Notation

Before we dive into the configuration, let's understand how to specify IP addresses in Caddy.

Single IP Address

203.0.113.10

This refers to one specific device. Every device connected to the internet has a unique IP address (similar to a home address).

IP Range Using CIDR Notation

Instead of listing every IP individually, you can specify ranges using CIDR notation:

10.0.0.0/8
192.168.0.0/16
172.16.0.0/12

What does /8, /16, /12 mean?

The number after the slash represents how many bits of the IP address are "fixed" (network portion) and how many can vary (host portion).

  • 10.0.0.0/8: The first 8 bits (10) are fixed, the rest (0.0.0) can be anything → covers 10.0.0.0 to 10.255.255.255 (16 million addresses)

  • 192.168.0.0/16: The first 16 bits (192.168) are fixed → covers 192.168.0.0 to 192.168.255.255 (65,536 addresses)

  • 172.16.0.0/12: The first 12 bits are fixed → covers a large private network range

Common Private IP Ranges (for internal networks)

These IP ranges are reserved for private networks and never used on the public internet:

RangeSizeCommon Use
10.0.0.0/816 million IPsLarge corporate networks, data centers
172.16.0.0/121 million IPsMedium corporate networks, Docker networks
192.168.0.0/1665,536 IPsSmall office/home networks, most common
127.0.0.1/8256 IPsLocalhost (your own machine)

Use Cases for IP Filtering

1. Protecting Admin Dashboards

Your Node.js application has an admin panel at /admin that only your team should access. Without IP filtering, anyone on the internet could find it (either by guessing the URL or through search engines).

Solution: Only allow the /admin route from your office IP address or your company's VPN.

2. Securing Internal APIs

You have internal APIs that should only be called from your backend servers or monitoring tools, not from the public internet.

Example: A /metrics endpoint that exposes server health information should only be accessible from your monitoring service.

3. Blocking Known Attackers

If you notice a specific IP address repeatedly attacking your site, you can block it immediately.

Example: An IP address is flooding your API with requests trying to guess user IDs. Block it to prevent the attack.

4. Geographic Restrictions

Some companies must comply with laws that require them to block access from certain countries. IP filtering is one tool for this.

Example: A game published in the US might need to block IPs from countries with different regulations.

5. VPN or Proxy Requirements

Force users to go through your company VPN before accessing the application.

Example: A company requires all remote employees to connect through a VPN before accessing internal tools.


How Caddy IP Filtering Works

Caddy uses a feature called matchers to intercept requests. Here's the flow:

Incoming Request
    ↓
Caddy checks matchers (@allowed_ips, @blocked_ips, etc.)
    ↓
Does request match? 
    ├─ YES → Execute the handle block
    └─ NO → Continue to next rule
    ↓
If no rules matched → Execute default handler

The key directive is remote_ip, which tells Caddy to examine the IP address of the client making the request.


Pattern 1: Whitelist (Allow Only Specific IPs)

This is the most restrictive approach: only allow traffic from IPs you explicitly specify. Everything else is blocked.

Basic Example: Allow Two Specific IPs

example.com {
    @allowed_ips {
        remote_ip 203.0.113.10 198.51.100.25
    }

    handle @allowed_ips {
        reverse_proxy localhost:3000
    }

    respond "Access denied" 403
}

How this works:

  1. Request arrives from an IP address

  2. Caddy checks: "Is this IP in my allowed list (203.0.113.10 or 198.51.100.25)?"

  3. If YES: Request goes to your Node.js app (reverse_proxy)

  4. If NO: Respond with "Access denied" (HTTP 403 status code)

Real-world example:

myapp.example.com {
    @allowed_ips {
        remote_ip 192.168.1.100 192.168.1.101 203.45.67.89
    }

    handle @allowed_ips {
        reverse_proxy localhost:3000
    }

    respond "Access denied" 403
}

Your office has two workstations (192.168.1.100 and 192.168.1.101) and a remote worker on a specific IP (203.45.67.89). Only these devices can access the app.

Allow a Range of IPs

Instead of listing IPs individually, use CIDR notation:

example.com {
    @allowed_ips {
        remote_ip 10.0.0.0/8 192.168.1.0/24
    }

    handle @allowed_ips {
        reverse_proxy localhost:3000
    }

    respond "Access denied" 403
}

What this allows:

  • 10.0.0.0/8: Any IP starting with 10 (company network)

  • 192.168.1.0/24: Any IP from 192.168.1.0 to 192.168.1.255 (small office network)

Allow Your VPN Network

Many companies use VPNs to allow remote workers secure access:

internal-app.example.com {
    @vpn_users {
        remote_ip 10.8.0.0/24
    }

    handle @vpn_users {
        reverse_proxy localhost:3000
    }

    respond "VPN access required" 403
}

Now only users connected to the company VPN (which assigns IPs in the 10.8.0.0/24 range) can access the app.

Whitelist with Error Messages

Provide helpful error messages to blocked users:

admin.example.com {
    @allowed_ips {
        remote_ip 10.0.0.0/8
    }

    handle @allowed_ips {
        reverse_proxy localhost:3000
    }

    respond "Admin access is restricted to the internal network. If you're trying to access from outside, please use the VPN." 403
}

Pattern 2: Blacklist (Block Specific IPs)

This is less restrictive: allow everyone except IPs you explicitly block. Useful for blocking known attackers.

Basic Example: Block Known Abusive IPs

example.com {
    @blocked_ips {
        remote_ip 192.0.2.50 203.0.113.99
    }

    handle @blocked_ips {
        respond "Access denied" 403
    }

    reverse_proxy localhost:3000
}

How this works:

  1. Request arrives

  2. Caddy checks: "Is this IP in my blocklist?"

  3. If YES: Respond with 403 Forbidden

  4. If NO: Allow the request through to your Node.js app

Execution order matters: The blacklist rule is checked first. If the IP is blocked, the request never reaches your application, which is more efficient.

Blocking a Range

Block an entire network range:

example.com {
    @blocked_ips {
        remote_ip 192.0.2.0/24 203.0.113.0/24
    }

    handle @blocked_ips {
        respond "Your network has been blocked due to suspicious activity" 403
    }

    reverse_proxy localhost:3000
}

This blocks two entire subnets (256 IPs each).

Blocking Known Botnet IPs

If you've identified an attacker using a specific IP range:

example.com {
    @botnet {
        remote_ip 198.51.100.0/24
    }

    handle @botnet {
        respond "Access denied" 403
    }

    # Log the attack attempt
    log {
        output stdout
        format single_line
    }

    reverse_proxy localhost:3000
}

Blacklist with Monitoring

Block and log the attack:

example.com {
    @attack_ips {
        remote_ip 192.0.2.100 203.0.113.200
    }

    handle @attack_ips {
        # Log suspicious access attempts
        log {
            output stdout
            format json
        }
        respond "Access denied" 403
    }

    reverse_proxy localhost:3000
}

Pattern 3: Route-Specific IP Filtering

Only apply IP restrictions to certain routes (URLs). Other routes remain open to everyone.

Allow /admin Route Only from Internal Network

example.com {
    handle_path /admin/* {
        @internal {
            remote_ip 10.0.0.0/8 192.168.0.0/16
        }

        handle @internal {
            reverse_proxy localhost:3000
        }

        respond "Admin access requires internal network access" 403
    }

    handle {
        reverse_proxy localhost:3000
    }
}

How this works:

  1. If the request path starts with /admin/:

    • Check if the IP is in the internal network (10.0.0.0/8 or 192.168.0.0/16)

    • If YES: Allow it

    • If NO: Respond with 403

  2. For all other paths (/): Allow everyone

Real-world example:

User at office (IP: 10.5.20.15)
    ├─ Access /admin → ALLOWED (internal IP)
    └─ Access /products → ALLOWED (public route)

User on public internet (IP: 203.45.67.89)
    ├─ Access /admin → BLOCKED (not internal IP)
    └─ Access /products → ALLOWED (public route)

Multiple Protected Routes

Protect several admin routes:

example.com {
    handle_path /admin/* {
        @internal {
            remote_ip 10.0.0.0/8
        }

        handle @internal {
            reverse_proxy localhost:3000
        }

        respond "Admin panel requires internal access" 403
    }

    handle_path /api/internal/* {
        @internal {
            remote_ip 10.0.0.0/8
        }

        handle @internal {
            reverse_proxy localhost:3000
        }

        respond "Internal API requires internal access" 403
    }

    handle_path /metrics {
        @internal {
            remote_ip 10.0.0.0/8 127.0.0.1
        }

        handle @internal {
            reverse_proxy localhost:3000
        }

        respond "Metrics endpoint requires internal access" 403
    }

    # Public routes
    handle {
        reverse_proxy localhost:3000
    }
}

This protects:

  • /admin/* - Admin dashboard

  • /api/internal/* - Internal APIs

  • /metrics - Health metrics

All other routes are public.

Subdomain-Based Access Control

Different subdomains with different IP restrictions:

# Public website - open to everyone
www.example.com {
    reverse_proxy localhost:3000
}

# Admin dashboard - internal only
admin.example.com {
    @internal {
        remote_ip 10.0.0.0/8
    }

    handle @internal {
        reverse_proxy localhost:3001
    }

    respond "Admin access requires internal network" 403
}

# API for partner integrations - partners only
api.example.com {
    @partners {
        remote_ip 203.0.113.0/24 198.51.100.0/24
    }

    handle @partners {
        reverse_proxy localhost:3002
    }

    respond "Partner API access only" 403
}

Pattern 4: Combining Whitelists and Blacklists

Apply both allow and block rules for fine-grained control.

Allow a Range, but Block Specific IPs Within It

example.com {
    # First, block known bad IPs
    @blocked_ips {
        remote_ip 10.5.20.50 10.5.20.51
    }

    handle @blocked_ips {
        respond "Your IP has been blocked" 403
    }

    # Then, allow the larger range
    @allowed_ips {
        remote_ip 10.0.0.0/8
    }

    handle @allowed_ips {
        reverse_proxy localhost:3000
    }

    # Block everyone else
    respond "Access denied" 403
}

Logic:

  1. Check if IP is in blocklist → If yes, block it

  2. Check if IP is in allowlist → If yes, allow it

  3. Block everyone else

This is useful when you have a large trusted network but need to block specific compromised devices within that network.

Allow Specific IPs, Block Others from Same Range

internal.example.com {
    @trusted_ips {
        remote_ip 192.168.1.10 192.168.1.11 192.168.1.12
    }

    @blocked_ips {
        remote_ip 192.168.1.99
    }

    handle @blocked_ips {
        respond "Your device is blocked" 403
    }

    handle @trusted_ips {
        reverse_proxy localhost:3000
    }

    respond "Access denied" 403
}

Only three specific devices (192.168.1.10, .11, .12) can access, even though they're all on the 192.168.1.0/24 network. Device .99 is explicitly blocked.


Real-World Scenarios

Scenario 1: E-Commerce Company with Admin Dashboard

shop.example.com {
    # Admin routes - internal network only
    handle_path /admin/* {
        @internal {
            remote_ip 10.0.0.0/8 203.45.67.0/24
        }

        handle @internal {
            reverse_proxy localhost:3001
        }

        respond "Admin access restricted" 403
    }

    # Known bot/crawler IPs
    @bad_bots {
        remote_ip 192.0.2.50 192.0.2.51 192.0.2.52
    }

    handle @bad_bots {
        log {
            output stdout
        }
        respond "Automated access not allowed" 403
    }

    # Everything else - public shop
    handle {
        reverse_proxy localhost:3000
    }
}

Scenario 2: SaaS Application with Team Access

app.example.com {
    # Team members on VPN
    @team {
        remote_ip 10.8.0.0/24
    }

    # Specific partner company
    @partners {
        remote_ip 203.0.113.0/25
    }

    # Block known attackers
    @blocked {
        remote_ip 192.0.2.100 192.0.2.101
    }

    handle @blocked {
        respond "Access denied" 403
    }

    handle @team {
        reverse_proxy localhost:3000
    }

    handle @partners {
        reverse_proxy localhost:3000
    }

    respond "Access requires VPN or partner credentials" 403
}

Scenario 3: API with Different Access Tiers

api.example.com {
    # Premium tier - restricted to premium client IPs
    handle_path /api/v1/premium/* {
        @premium {
            remote_ip 198.51.100.0/24
        }

        handle @premium {
            reverse_proxy localhost:3001
        }

        respond "Premium API requires premium subscription" 403
    }

    # Standard tier - broader access
    handle_path /api/v1/standard/* {
        @authorized {
            remote_ip 203.0.113.0/24 198.51.100.0/24 172.16.0.0/16
        }

        handle @authorized {
            reverse_proxy localhost:3002
        }

        respond "Standard API access restricted" 403
    }

    # Public tier - anyone
    handle_path /api/v1/public/* {
        reverse_proxy localhost:3003
    }

    respond "Not found" 404
}

Common Mistakes to Avoid

Mistake 1: Not Accounting for Proxies and Load Balancers

When your traffic goes through a proxy, Caddy might see the proxy's IP, not the real user's IP.

# ❌ WRONG - If you're behind Cloudflare, you'll block Cloudflare's IPs!
example.com {
    @allowed {
        remote_ip 192.168.1.0/24
    }

    handle @allowed {
        reverse_proxy localhost:3000
    }

    respond "Access denied" 403
}

Solution: Tell Caddy to look at the X-Forwarded-For header that proxies add:

# ✅ CORRECT
example.com {
    @allowed {
        header X-Forwarded-For 192.168.1.0/24
    }

    handle @allowed {
        reverse_proxy localhost:3000
    }

    respond "Access denied" 403
}

Or if using Cloudflare:

# ✅ CORRECT for Cloudflare
example.com {
    @allowed {
        header CF-Connecting-IP 192.168.1.0/24
    }

    handle @allowed {
        reverse_proxy localhost:3000
    }

    respond "Access denied" 403
}

Mistake 2: Wrong CIDR Notation

# ❌ WRONG - Invalid CIDR
@internal {
    remote_ip 10.0.0.0/33
}

CIDR numbers must be between 0-32. A /33 doesn't exist.

Solution: Use valid CIDR ranges:

# ✅ CORRECT
@internal {
    remote_ip 10.0.0.0/8
    remote_ip 192.168.0.0/16
}

Mistake 3: Forgetting That Order Matters

Rules are evaluated in order. If you have a blocking rule after an allowing rule, the blocking rule might never execute.

# ❌ WRONG - Whitelist will match first, blacklist never checked
example.com {
    @allow_all {
        remote_ip 10.0.0.0/8
    }

    handle @allow_all {
        reverse_proxy localhost:3000
    }

    @blocked_ips {
        remote_ip 10.0.0.50
    }

    handle @blocked_ips {
        respond "Blocked" 403
    }
}

Solution: Put blocklists before allowlists:

# ✅ CORRECT
example.com {
    @blocked_ips {
        remote_ip 10.0.0.50
    }

    handle @blocked_ips {
        respond "Blocked" 403
    }

    @allow_all {
        remote_ip 10.0.0.0/8
    }

    handle @allow_all {
        reverse_proxy localhost:3000
    }
}

Mistake 4: Blocking Yourself During Testing

When you deploy IP filtering, make sure you don't accidentally block yourself!

# ❌ WRONG - If your office IP is 192.168.1.100, you'll be locked out!
example.com {
    @internal {
        remote_ip 10.0.0.0/8
    }

    handle @internal {
        reverse_proxy localhost:3000
    }

    respond "Access denied" 403
}

Solution:

  1. Find your real IP address:

    curl ifconfig.me
    
  2. Include it in the allow list before deploying:

    @internal {
        remote_ip 192.168.1.100 10.0.0.0/8
    }
    
  3. Test from a different IP to verify blocking works:

    # Test with a different IP using a proxy or VPN
    curl -H "X-Forwarded-For: 203.0.113.1" https://example.com
    

Mistake 5: Not Updating Rules Regularly

IP addresses for cloud services, CDNs, and partners change. Your IP filtering rules become outdated.

Solution:

  • Keep a comment with the date you last reviewed the rules:

    # Last reviewed: 2025-01-15
    @partner_ips {
        remote_ip 203.0.113.0/24  # Partner A's office
        remote_ip 198.51.100.0/24  # Partner B's office
    }
    
  • Subscribe to IP range updates from partners

  • Regularly audit your rules (monthly or quarterly)


Testing IP Filtering Rules

Test 1: Check Your Own IP

Find your current IP address:

curl ifconfig.me

Output: 203.45.67.89

Test 2: Verify Blocking Works

Using curl with a spoofed IP header:

# Simulate request from blocked IP
curl -H "X-Forwarded-For: 192.0.2.50" https://example.com

# Expected response: 403 Forbidden

Test 3: Verify Allowing Works

# Simulate request from allowed IP
curl -H "X-Forwarded-For: 10.0.0.50" https://example.com

# Expected response: 200 OK (or your app's response)

Test 4: Check Caddy Logs

View Caddy logs to see IP filtering in action:

# If Caddy is running as a service
sudo journalctl -u caddy -f

# Or if running in Docker
docker logs -f caddy-container-name

Look for access logs showing allowed/denied requests.


Complete Production-Ready Examples

Admin Dashboard Setup

# Main public site
example.com {
    reverse_proxy localhost:3000
}

# Admin dashboard - internal only
admin.example.com {
    @internal {
        remote_ip 10.0.0.0/8 203.45.67.100
    }

    handle @internal {
        reverse_proxy localhost:3001
    }

    log {
        output stdout
        format json
    }

    respond "Admin access requires internal network access" 403
}

Multi-Tier API

api.example.com {
    # Block known attackers first
    @attackers {
        remote_ip 192.0.2.0/24
    }

    handle @attackers {
        log { output stdout }
        respond "Access denied" 403
    }

    # Premium customers
    handle_path /premium/* {
        @premium {
            remote_ip 198.51.100.0/24
        }

        handle @premium {
            reverse_proxy localhost:3001
        }

        respond "Premium access required" 403
    }

    # Standard customers
    handle_path /standard/* {
        @standard {
            remote_ip 203.0.113.0/24 198.51.100.0/24
        }

        handle @standard {
            reverse_proxy localhost:3002
        }

        respond "Standard access required" 403
    }

    # Public API
    handle_path /public/* {
        reverse_proxy localhost:3003
    }

    respond "Not found" 404
}

Key Takeaways

  1. IP filtering is a critical security tool for protecting sensitive routes and admin panels.

  2. Understand CIDR notation to efficiently manage IP ranges instead of listing individual IPs.

  3. Use whitelists (allow lists) for maximum security when protecting sensitive resources.

  4. Use blacklists (block lists) for reactive blocking of known attackers.

  5. Order matters: Put blacklists before whitelists so blockers execute first.

  6. Test thoroughly before deploying, especially when using whitelists, to avoid locking yourself out.

  7. Account for proxies - if your traffic goes through a proxy or CDN, use the appropriate headers (X-Forwarded-For, CF-Connecting-IP).

  8. Update regularly - IP addresses change, so review your rules monthly.

  9. Combine with other security - IP filtering is one layer, use it alongside other protections like authentication and authorization.

  10. Monitor and log - Enable logging to track blocked requests and identify attack patterns.


Further Reading