GeoIP2 and NGINX

Categories:

There are times when you want to configure your website to explicitly disallow access from certain countries, or only allow access from a given set of countries. While not completely precise, use of the MaxMind GeoIP databases to look up a web client’s country-of-origin and have the web server respond accordingly is a popular technique.

There are a number of NGINX tutorials on how to use the legacy GeoIP database and the ngx_http_geoip_module, and as it happens the default Ubuntu nginx package includes the ngx_http_geoip_module. Unfortunately the GeoIP databases will no longer be updated, and MaxMind has migrated to GeoIP2. Moreover, after January 2, 2019, the GeoIP databases will no longer be available.

This leaves us in a bind. Luckily, while the Ubuntu distribution of NGINX doesn’t come with GeoIP2 support, we can add it by building from source. Which is exactly what we’ll do! In this tutorial we’re going to build nginx from the ground up, modeling its configuration options after those that are used by the canonical nginx packages available from Ubuntu 16.04. You’ll want to go through this tutorial on a fresh installation of Ubuntu 16.04 or later; we’ll be using an EC2 instance created from the AWS Quick Start Ubuntu Server 16.04 LTS (HVM), SSD Volume Type AMI.

If you’re a fan of NGINX and hosting secure webservers, check out our latest post on configuring NGINX with support for TLS 1.3

Getting Started

Since we’re going to be building binaries, we’ll need the build-essential package which is a metapackage that installs applications such as make, gcc, etc.

Now, to install all of the prerequisities libraries we’ll need to compile NGINX:

Using the GeoIP2 database with NGINX requires the ngx_http_geoip2_module and requires the MaxMind development packages from MaxMind:

Getting the Sources

Now let’s go and download NGINX. We’ll be using the latest dot-release of the 1.15 series, 1.15.3. I prefer to compile things in /usr/local/src, so:

We also need the source for the GeoIP2 NGINX module:

Now, to configure and compile.

You will want to make sure that the ngx_http_geoip2_module will be compiled, and should see nginx_geoip2_module was configured in the end of the configure output.

Now, run sudo make. NGINX, for all its power, is a compact and light application, and compiles in under a minute. If everything compiles properly, you can run sudo make install.

A few last things to complete our installation:

  • creating a symlink from /usr/sbin/nginx to /usr/share/nginx/sbin/nginx
  • creating a symlink from /usr/share/nginx/modules to /usr/lib/nginx/modules
  • creating the /var/lib/nginx/body directory
  • installing an NGINX systemd service file

For the Systemd service file, place the following in /lib/systemd/system/nginx.service:

and reload systemd with sudo systemctl daemon-reload. You should now be able to check the status of nginx:

We’ll be starting it momentarily!

Testing

On to testing! We’re going to use HTTP (rather than HTTPS) in this example.

While we’ve installed the libraries that interact with the GeoIP2 database, we haven’t yet installed the database itself. This can be accomplished by installing the geoipupdate package from the MaxMind PPA:

[code lang=text]
# sudo apt-get install -y geoipupdate
[/code]

Now run sudo geoipupdate -v:

It’s a good idea to periodically update the GeoIP2 databases with geoipupdate. This is typically accomplished with a cron job like:

[code lang=text]
# crontab -l
30 0 * * 6 /usr/bin/geoipupdate -v | /usr/bin/logger
[/code]

Note: Use of logger here is optional, we just like to see the output of the geoipupdate invocation in /var/log/syslog.

Nginx Configuration

Now that nginx is built and installed, we have a GeoIP2 database in /usr/share/GeoIP, we can finally get to the task of restricting access to our website. Here is our basic nginx.conf:

[code lang=text]
load_module modules/ngx_http_geoip2_module.so;

worker_processes auto;

events {
worker_connections 1024;
}

http {
sendfile on;
include mime.types;
default_type application/octet-stream;
keepalive_timeout 65;

geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {
$geoip2_data_country_code country iso_code;
}

map $geoip2_data_country_code $allowed_country {
default no;
US yes;
}

server {
listen 80;
server_name localhost;

if ($allowed_country = no) {
return 403;
}

location / {
root html;
index index.html index.htm;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
[/code]

Let’s walk through the relevant directives one at a time.

load_module modules/ngx_http_geoip2_module.so;

Since we built nginx with ngx_http_geopip2_module as a dynamic module, we need to load it explicitly with the load_module directive.

Looking up the ISO country code from the GeoIP2 database utilizes our geoip2 module:

[code lang=text]
geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {
$geoip2_data_country_code country iso_code;
}
[/code]

The country code of the client IP address will be placed in the NGINX variable $geoip2_data_country_code. From this value we determine what to set $allowed_country to:

[code lang=text]
map $geoip2_data_country_code $allowed_country {
default no;
US yes;
}
[/code]

map in the NGINX configuration file is a bit like a switch statement (I’ve chosen the Swift syntax of switch):

[code lang=text]
switch geoip2_data_country_code {
case 'US':
allowed_country = "yes"
default:
allowed_country = "no"
}
[/code]

If we wanted to allow IPs from the United States, Mexico, and Canada the map directive would look like:

[code lang=text]
map $geoip2_data_country_code $allowed_country {
default no;
US yes;
MX yes;
CA yes;
}
[/code]

geoip2 and map by themselves do not restrict access to the site. This is accomplished through the if statement which is located in the server block:

[code lang=text]
if ($allowed_country = no) {
return 403;
}
[/code]

This is pretty self-explanatory. If $allowed_country is no then return a 403 Forbidden.

If you haven’t done so already, start nginx with systemctl start nginx and give the configuration a go. It’s quite easy to test your nginx configuration by disallowing your country, restarting nginx (systemctl restart nginx), and trying to access your site.

Credits and Disclaimers

NGINX and associated logos belong to NGINX Inc. MaxMind, GeoIP, minFraud, and related trademarks belong to MaxMind, Inc.

The following resources were invaluable in developing this tutorial:

3 thoughts on “GeoIP2 and NGINX”

  1. Great article, helped me a lot, thanks man.

    Found a small typo just under Testing step,
    “Now run sudo geoipdate -v:”
    should say geoipupdate

    Cheers

  2. good article, thank you for your share.
    can you please explain more detail about the section of installing an NGINX systemd service file
    I have a problem when install the nginx with GeoIP2 moudle.
    — Unit nginx.service has begun starting up.
    Mar 05 19:04:47 ubuntu systemd[17894]: nginx.service: Failed at step EXEC spawning /usr/sbin/nginx: No such file or directory

    thanks

Leave a Reply

Your email address will not be published. Required fields are marked *