Caddy MaxMind GeoIP Access Control: Whitelist and Blacklist Modes

Why GeoIP Access Control

If your service is mainly for users in specific regions, traffic from elsewhere often includes malicious requests:

  • A blog targeting Chinese readers; European IPs may be scanners
  • An admin panel that only needs access from your country, or an API service that needs to limit abuse from specific regions

GeoIP restriction can’t be your only defense. VPNs can bypass IP geolocation checks. It works better as a way to cut down malicious traffic and log noise, with some relief for server load too.

caddy-maxmind-geolocation Plugin

My custom build already includes this plugin (see the xcaddy build article). It uses MaxMind’s GeoLite2 database for IP geolocation lookups.

Each database record contains:

  • IP range start and end, plus the country code (ISO 3166-1 alpha-2, e.g., CN, US, JP)
  • Optional region and city info (City edition; this article uses the Country edition)

Two Restriction Modes

I use two snippets for different scenarios:

Whitelist Mode: Allow Only Specified Countries

(geo_only_allow_countries) {
    @only_allow_countries_geofilter {
        not {
            maxmind_geolocation {
                db_path "/config/geodb/GeoLite2-Country.mmdb"
                allow_countries {args[:]}
            }
        }
    }

    respond @only_allow_countries_geofilter "You are blocked" 403
}

This defines a named matcher, @only_allow_countries_geofilter, for requests not in the specified country list, then returns 403 for those requests.

Usage:

example.com {
    import geo_only_allow_countries CN JP
    reverse_proxy 127.0.0.1:3004
}

This only allows Chinese and Japanese IPs through.

Blacklist Mode: Block Specified Countries

(geo_not_allow_countries) {
    @not_allow_countries_geofilter {
        maxmind_geolocation {
            db_path "/config/geodb/GeoLite2-Country.mmdb"
            allow_countries {args[:]}
        }
    }

    respond @not_allow_countries_geofilter "You are blocked" 403
}

This directly matches requests from the specified country list and returns 403.

Usage:

example.com {
    import geo_not_allow_countries RU
    reverse_proxy 127.0.0.1:8080
}

This blocks Russian IPs.

GeoLite2 Database Auto-Update

MaxMind’s GeoLite2 database changes as IP allocations move around, usually 1-2 times per month. If the database gets stale, geolocation results for some IPs can be wrong.

My container handles updates through update_geodb.sh, which is already covered in the xcaddy article.

Limitations

  1. VPN/proxy bypass: Users can switch their exit IP through a VPN, and GeoIP can’t stop that
  2. Database freshness and CDN origin requests: IP ranges get reassigned, so old databases may cause misclassification; when requests pass through Cloudflare, Caddy sees Cloudflare’s origin IP, so trusted_proxies must be configured correctly to get the real client IP

GeoIP fits better as a way to narrow the attack surface, not as the only security barrier.

In practice, the setup is just a few lines of Caddyfile. Whitelist mode fits admin panels and other tightly controlled entry points; blacklist mode is a better fit for blocking known malicious traffic from specific regions. Once GeoIP database updates are automated, there is not much ongoing maintenance.

References