Skip to main content

IP Geo blocking

NGINX

Qool geographic IPrestricts access restriction

Purpose

Restrict an NGINX website so that only client IP addresses geolocated to the Netherlands (NL)using andNGINX explicitly configured exception addresses can access it. All other clients receive 403 Forbidden.GeoIP2.

This

1. procedure targets Ubuntu or Debian and uses the MaxMind GeoLite2 Country database withInstall the NGINX GeoIP2 module.

Location

The relevant server paths are:

  • /etc/GeoIP.conf: MaxMind download credentials and database settings.
  • /usr/share/GeoIP/GeoLite2-Country.mmdb: downloaded country database.
  • /etc/nginx/conf.d/country-access.conf: reusable geographic access rules.
  • /etc/nginx/sites-available/<site>: the protected virtual host.

Replace example domains, proxy networks, and IP addresses before applying the configuration.

Inputs and outputs

Required inputs:

  • Root or sudo access to the NGINX server.
  • A free MaxMind account, Account ID, and license key.
  • The public IPv4 and, when applicable, IPv6 address that must be allowed as an exception.
  • The trusted proxy networks when NGINX is behind a CDN, load balancer, or reverse proxy.

The result is an NGINX access decision:

  • When the client country is NL, then access is allowed.
  • When the client IP matches a configured exception, then access is allowed.
  • When neither condition matches, then NGINX returns 403 Forbidden.
  • When the GeoIP database has no match, then access is denied by default.

Flow and behavior

1. Record the exception address

Run the following command from the exceptional connection:

curl -4 https://ifconfig.me

The examples use the reserved documentation address 203.0.113.42. Replace it with the real public address. If the connection uses IPv6, record and configure that address separately.

2. Install the required packagesmodule

sudo apt update
sudo apt install libnginx-mod-http-geoip2 geoipupdate mmdb-bin

CheckUbuntu whethershould theautomatically module is loaded:create:

sudo nginx -T 2>&1 | grep geoip2

On Ubuntu, installing libnginx-mod-http-geoip2 normally creates a module loader under /etc/nginx/modules-enabled/50-mod-http-geoip2.conf. If it does not, add

With the following directive at the top level of /etc/nginx/nginx.conf, outside the http {} block:contents:

load_module modules/ngx_http_geoip2_module.so;

Do

2. not add the directive if the module is already loaded.

3. Create the MaxMind credentials

  1. Create a free MaxMind GeoLite2 account.
  2. Accept the GeoLite2 terms.
  3. Find the MaxMind Account ID.
  4. Generate a license key for geoipupdate.
  5. Store the license key securely; treat it as a password.

4. Configure and downloadDownload the country database

EditDB-IP /etc/GeoIP.conf:publishes its free country database monthly in MMDB format. Select the current release from the official DB-IP download page. The free database requires attribution under CC BY 4.0.

For the November 2026 release:

AccountID YOUR_MAXMIND_ACCOUNT_ID
LicenseKey YOUR_MAXMIND_LICENSE_KEY
EditionIDs GeoLite2-Country
DatabaseDirectorycd /usr/share/GeoIPtmp

curl

Protect-fL the\ credentialshttps://download.db-ip.com/free/dbip-country-lite-2026-11.mmdb.gz and\ download-o thedbip-country-lite.mmdb.gz database:

gzip
sudo-dc chowndbip-country-lite.mmdb.gz root:root\
  /etc/GeoIP.conf> dbip-country-lite.mmdb

sudo chmod 600 /etc/GeoIP.conf
sudo mkdirinstall -pd -o root -g root -m 0755 /usr/share/GeoIP

sudo geoipupdate

Confirm that the database exists:

sudo lsinstall -lho root -g root -m 0644 \
  dbip-country-lite.mmdb \
  /usr/share/GeoIP/GeoLite2-Country.dbip-country-lite.mmdb

TestDB-IP adocuments lookup:that Lite databases are updated monthly, so the YYYY-MM value must periodically be updated. DB-IP documentation

mmdblookup

3. \Define --file /usr/share/GeoIP/GeoLite2-Country.mmdb \ --ip 8.8.8.8 \the country iso_code

The example address should return US.

5. Configure the reusable NGINX rulespolicy

Create /etc/nginx/conf.d/nl-country-access.map.conf:

# Determine the client's country from its IP address.
geoip2 /usr/share/GeoIP/GeoLite2-Country.dbip-country-lite.mmdb {
    auto_reload 1h;
    $geoip2_country_codevisitor_country_code default=ZZ country iso_code;
}

# Explicit IP exceptions. Individual addresses and CIDR ranges are supported.
geomap $manual_ip_exceptionremote_addr $trusted_ip {
    default 0;

    # Replace this reserved example address with theyour realown exception.trusted public IP.
    203.0.113.42/32 1;

    # Optional IPv6 exception:
    # 2001:db8:1234::1/12810 1;
}

# Allow Dutch addresses.
map "$geoip2_country_codevisitor_country_code:$trusted_ip" $country_allowed {
    default 0;

    NL# Allow clients classified as Dutch.
    ~^NL: 1;

    # Allow explicitly trusted source addresses.
    ~:1$ 1;
}

4. Create the enforcement snippet

Create /etc/nginx/snippets/nl-only.conf:

# Reusable Netherlands-only access policy.
# AccessRequires is$nl_country_allowed allowed whenin the countryHTTP orcontext.
theif explicit($country_allowed exception= matches.
map "$country_allowed:$manual_ip_exception" $geographic_access_allowed0) {
    defaultreturn 0;
    "1:0"   1;
    "1:1"   1;
    "0:1"   1;403;
}

UseUsing /32 for one IPv4 address and /128 for one IPv6 address. Only use a broader CIDR range when the entire range is intentionally trusted.

6. Configure the real client IP when using a proxy

Skip this step when clients connect directly to NGINX.

When NGINX is behind a reverse proxy, $remote_addr can contain the proxy address instead of the visitor address. Configure the real-IP module with only the actual trusted proxy networks. For example:

set_real_ip_from 192.0.2.0/24;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

Replace 192.0.2.0/24if with thea real proxy network. Never trustsimple X-Forwarded-Forreturn fromis everyappropriate internet address because clients could forge it.here.

For

5. Cloudflare,Enable configureit allfor currenta Cloudflare proxy ranges as trusted and use:

real_ip_header CF-Connecting-IP;
website

VerifyInside the resulting client address before enabling the restriction.

7. Apply the restriction to the virtual host

Add the access check inside the relevant HTTPSapplicable server {} block:

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name example.com;

    ifinclude ($geographic_access_allowed = 0) {
        return 403;
    }snippets/nl-only.conf;

    # ExistingRemaining website configuration follows.
    location / {
        proxy_pass http://127.0.0.1:8080;configuration...
}
}

Using if only to return an HTTP status is appropriate here. Add the same check to the port 80 virtual host when HTTP must also be protected:

server {
    listen 80;
    listen [::]:80;
    server_name example.com;

    if ($geographic_access_allowed = 0) {
        return 403;
    }

    return 301 https://$host$request_uri;
}

8. Validate and reload NGINX

Keep the existing SSH session open so the change can be reversed if necessary.6. Test thebefore complete NGINX configuration:

reloading
sudo nginx -t

Only afterwhen athat successful test, reload NGINX:succeeds:

sudo systemctl reload nginx

9. Test all access paths

Test from three different network locations:from:

    • From aA Dutch connection, expectwhich should receive the normal website response.website.
    • From the explicit exception address, expect the normal website response.
    • From aA non-Dutch connectionVPN orendpoint, VPN,which expectshould receive HTTP 403 Forbidden.
  • Your trusted IP, which should work regardless of its GeoIP classification.
  • Use:

    You
    curl -I https://example.com
    

    For temporary diagnostics, add these headers inside the virtual host:

    add_header X-Debug-Country $geoip2_country_code always;
    add_header X-Debug-IP-Exception $manual_ip_exception always;
    

    Reload NGINX andcan inspect theNGINX’s responseeffective headers.configuration Remove the diagnostic headers after testing because they expose internal access-decision information.

    10. Schedule database updates

    Check whether the package installed a timer:

    systemctl list-timers --all | grep geoip
    

    When no timer is present, edit the root crontab:with:

    sudo crontabnginx -e
    T

    Add

    Important a weekly update:

    17 4 * * 3 /usr/bin/geoipupdate
    

    The auto_reload 1h setting makes the NGINX module check for a replaced database without requiring a manual NGINX reload.

    Edge cases and failure modes

    considerations
    • GeoIPIf dataNGINX is notbehind perfect.Cloudflare, Corporateanother VPNs,reverse mobileproxy, networks,or foreigna exitload nodes,balancer, and$remote_addr privacymay relays can cause Dutch users to appear outsidebe the Netherlands.proxy address. Configure NGINX’s real-IP module correctly before applying this restriction. Do not blindly trust client-supplied forwarding headers.
    • A proxyserver-level or CDN configuration that does not restore the real client addressrestriction can classifyalso everyblock visitor using the proxy address and country.
    • Let'Let’s Encrypt HTTP-01 validationvalidation. canIf originateCertbot outsideuses theHTTP-01, Netherlandsprovide anda beseparate blocked. Prefer DNS-01 validation, or deliberately exempt only theunrestricted ACME challenge location.virtual host/location or use DNS-01 validation.
    • ExternalGeoIP monitoringis not an absolute security boundary. VPNs, proxies, database inaccuracies, and healthstale checksrecords originatingcan outside the Netherlands need an explicit exceptionbypass or aincorrectly deliberatelytrigger unprotected endpoint.it.
    • The exceptionqool addressserver cancurrently changehas whenno itdetected automatic database updater. A monthly replacement of the MMDB file should therefore be scheduled if you want current classifications.
    • SSH access is assignedunaffected; dynamicallythis byconfiguration anonly ISP.restricts Confirmwebsites whetherserved through the exceptionalapplicable connection has a static IP.
    • IPv4 and IPv6 are evaluated independently. Add both exception addresses when the exceptional client can use either protocol.
    • An unavailable or unmatched GeoIP record is denied by default. This is fail-closed behavior.
    • This restriction operates at the HTTP layer. A firewall or upstream WAF can provide another layer, but correct real-client-IP handling remains necessary.

    Rollback

    If valid users are unexpectedly blocked:

    1. Comment out or remove the if ($geographic_access_allowed = 0) block from the affectedNGINX virtual host.
    2. Run sudo

      --

      nginx

       -t.

    3. Run

       sudo systemctl reload nginx only when the test succeeds.

    4. Investigate the real client address, GeoIP lookup, exception range, and proxy configuration before re-enabling the restriction.