Introduction

  • GeoIP filtering is a security technique that controls access to a website, server or online resource based on the geographical location of the incoming IP address. This method is often used to restrict access from certain countries or regions associated with high levels of suspicious or malicious activity, such as spam, hacking attempts or unauthorised access.
  • Today I’m going to explain how to set up GeoIP filtering with Caddy and FirewallD.
  • I’ve explained how to set up Caddy in my previous articles: Setting Up Caddy Web Server

Step 1: Select a GeoIP Database for Caddy

  • I prefer the GeoLite2 database for Caddy. If you have a Maxmind membership you can use it too. If you don’t have a Maxmind membership, you can download the GeoLite2 database from the following link: https://github.com/P3TERX/GeoLite.mmdb

    Information

    This database is updated every 3 days.

  • Copy the GeoLite2-country.mmdb URL from the releases page and download the GeoLite2-country.mmdb file to your server.

    sudo wget -O /etc/caddy/GeoLite2-Country.mmdb <copied_url>

    photo


Step 2: Install maxmind_geolocation Plugin for Caddy

  • To use the maxmind geolocation module with Caddy, you need to download the module with the following command:

    sudo caddy add-package github.com/porech/caddy-maxmind-geolocation

Step 3: Set Up Caddy for GeoIP Filtering

  • Open the Caddyfile with your preferred text editor.

    sudo vim /etc/caddy/Caddyfile
  • Add the following lines to the Caddyfile:

    (geofilter) {
        import /etc/caddy/Geofilter
    }

    photo

  • Create a new file named Geofilter in the /etc/caddy directory.

    sudo vim /etc/caddy/Geofilter
  • Then add the countries you want to allow to the Geofilter file. We do not want to block requests from local addresses. So we add local addresses to the Geofilter file as shown below. For example:

    @geofilter {
        not maxmind_geolocation {
            db_path "/etc/caddy/GeoLite2-Country.mmdb"
            allow_countries DE TR           # Germany and Turkey
        }
        not remote_ip 127.0.0.1             # Localhost
        not remote_ip ::1                   # Localhost
        not remote_ip 10.0.0.0/8            # PrivateNetwork
        not remote_ip 100.64.0.0/10         # PrivateNetwork
        not remote_ip 127.0.0.0/8           # PrivateNetwork
        not remote_ip 172.16.0.0/12         # PrivateNetwork
        not remote_ip 192.168.0.0/16        # PrivateNetwork
    }

    photo

    Note

    If you are using CloudFlare proxy, you should add the CloudFlare IP ranges to the Geofilter file.
    For example:

        not remote_ip 103.21.244.0/22       # CloudFlare
        not remote_ip 103.22.200.0/22       # CloudFlare
        not remote_ip 103.31.4.0/22         # CloudFlare
        not remote_ip 104.16.0.0/13         # CloudFlare
        not remote_ip 104.24.0.0/14         # CloudFlare
        not remote_ip 108.162.192.0/18      # CloudFlare
        not remote_ip 131.0.72.0/22         # CloudFlare
        not remote_ip 141.101.64.0/18       # CloudFlare
        not remote_ip 162.158.0.0/15        # CloudFlare
        not remote_ip 172.64.0.0/13         # CloudFlare
        not remote_ip 173.245.48.0/20       # CloudFlare
        not remote_ip 188.114.96.0/20       # CloudFlare
        not remote_ip 190.93.240.0/20       # CloudFlare
        not remote_ip 197.234.240.0/22      # CloudFlare
        not remote_ip 198.41.128.0/17       # CloudFlare

    photo

  • If you want to display a custom error page for blocked countries, you can add the following lines to the end of the Geofilter file

    header  @geofilter Content-Type text/html
    respond @geofilter <<HTML
        <html>
        <head><title>Blocked</title></head>
        <body><img src="https://http.cat/401" alt="blocked"/></body>
        </html>
        HTML 401

    photo

  • Then save this file and open the Caddyfile again.

    sudo vim /etc/caddy/Caddyfile
  • Add the following line to the Caddyfile:

    yourdomain.com {
        ...
        import geofilter                   # Import the Geofilter
        ...
    }

    photo

  • Then save this file and restart Caddy.

    sudo systemctl restart caddy

Step 4: Test the GeoIP Filtering for Caddy

  • If you have a another server in a blocked country or if you have a VPN, you can test the GeoIP filtering by accessing your domain. I tested it with this tool.

  • Result: photo


Step 5: Set Up Cronjob for GeoIP Database Update

  • We can create a cronjob to automatically update the GeoLite2 database.

  • Create a new file named geoupdate.sh in the /usr/local/bin directory.

    sudo vim /usr/local/bin/geoupdate.sh
  • Add the following lines to the file.

    #!/bin/bash
    wget -O /etc/caddy/GeoLite2-Country.mmdb https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb
    [[ "$?" == "0" ]] && systemctl restart caddy

    photo

  • Then save the file and make it executable.

    sudo chmod +x /usr/local/bin/geoupdate.sh
  • Test the script by running it manually.

    sudo /usr/local/bin/geoupdate.sh
  • Then check the GeoLite2 database file.

    stat /etc/caddy/GeoLite2-Country.mmdb

    photo

  • If the script runs successfully, we can add it to the crontab.

    sudo crontab -e
  • Add the following line to the crontab.

    0 0 * * SUN /usr/local/bin/geoupdate.sh

    Tip
    This line will update the GeoLite2 database every Sunday at midnight.


Step 6: Select countries to Allow With FirewallD

  • The methods described up to this step apply to HTTPS connections. However, we cannot always use HTTPS connections. For example, if we are using a DNS server, we need to use port 53 independently of the Caddy. Or we need to use port 22 for SSH connections. We can use the blocking method through firewalld to block requests to the server port.

  • I prefer the ipdeny database this step.

  • Copy the links to the database files for the countries you want to authorise. Then download these databases using the following command:

    Note

    I will use the Deutschland and Türkiye databases as an example.

    wget <copied_url>

    photo


Step 7: Set Up FirewallD for GeoIP Filtering

  • We don’t want to refuse connections from local addresses. So we create a new file called local.zone.

    vim local.zone
  • Then we add the following lines to the local.zone file.

    10.0.0.0/8
    100.64.0.0/10
    127.0.0.0/8
    172.16.0.0/12
    192.168.0.0/16
  • Then we create a new ipset for the allowed countries.

    sudo firewall-cmd --permanent --new-ipset='ALLOWLIST' --type=hash:net --option=family=inet --option=hashsize=4096 --option=maxelem=200000
  • We add the local addresses to the ALLOWLIST ipset.

    sudo firewall-cmd --permanent --ipset='ALLOWLIST' --add-entries-from-file=local.zone
    sudo firewall-cmd --permanent --ipset='ALLOWLIST' --add-entries-from-file=de.zone
    sudo firewall-cmd --permanent --ipset='ALLOWLIST' --add-entries-from-file=tr.zone
  • Then we create a new rich rule to allow a server port for the ALLOWLIST ipset.

    # SSH
    sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source ipset=ALLOWLIST port port="22" protocol="tcp" accept'
     
    # DNS
    sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source ipset=ALLOWLIST port port="53" protocol="tcp" accept'
    sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source ipset=ALLOWLIST port port="53" protocol="udp" accept'
     
    # QUIC
    sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source ipset=ALLOWLIST port port="853" protocol="tcp" accept'
    sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source ipset=ALLOWLIST port port="853" protocol="udp" accept'
  • Then we reload the firewall.

    sudo firewall-cmd --reload
  • We can test these rules by sending a telnet command to another server that is located in a country that is not allowed (or we can use a VPN).

    telnet <server_ip> 22           # SSH
    telnet <server_ip> 53           # DNS

    photo


What is the difference between blocking between Caddy and FirewallD?

  • When using FirewallD for blocking, iptables works in the background and handles the blocking process at Layer 4 (transport layer). This means that the request is intercepted and blocked before it reaches the server’s application layer, providing a performance benefit by reducing the load on higher layers.

  • In contrast, when blocking with Caddy, the request proceeds through Layer 4 and reaches Layer 7 (Application Layer), where the application itself decides whether to block the request. Blocking at Layer 7 may consume slightly more resources, but it allows for more granular control, as the decision can be based on application-specific rules. However, it typically requires fewer performance resources for fine-grained control than Layer 4 blocking, especially for certain types of content filtering or geo-restriction.


Conclusion

  • By implementing GeoIP filtering, organisations and administrators can limit the risks associated with untrusted regions and better protect their networks from cyber threats. This technology is often used in industries where data security is a priority, such as finance, healthcare and government. GeoIP filtering can be configured on firewalls, routers and specific applications, making it a flexible solution for securing network access based on geographical boundaries.

References