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.
1 2 |
sudo apt-get update sudo apt-get install -y build-essential |
Now, to install all of the prerequisities libraries we’ll need to compile NGINX:
1 |
sudo apt-get install -y libpcre3-dev zlib1g-dev libssl-dev libxslt1-dev |
Using the GeoIP2 database with NGINX requires the ngx_http_geoip2_module and requires the MaxMind development packages from MaxMind:
1 2 3 |
sudo add-apt-repository -y ppa:maxmind/ppa sudo apt-get update sudo apt-get install -y libmaxminddb-dev |
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:
1 2 3 |
cd /usr/local/src/ sudo wget https://nginx.org/download/nginx-1.15.3.tar.gz sudo tar -xzvf nginx-1.15.3.tar.gz |
We also need the source for the GeoIP2 NGINX module:
1 2 |
sudo wget https://github.com/leev/ngx_http_geoip2_module/archive/3.0.tar.gz sudo tar -xzvf 3.0.tar.gz |
Now, to configure and compile.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
cd nginx-1.15.3 sudo ./configure \ --with-cc-opt='-g -O2 -fPIE -fstack-protector-strong -Wformat -Werror=format-security -fPIC -Wdate-time -D_FORTIFY_SOURCE=2' \ --with-ld-opt='-Wl,-Bsymbolic-functions -fPIE -pie -Wl,-z,relro -Wl,-z,now -fPIC' \ --prefix=/usr/share/nginx \ --conf-path=/etc/nginx/nginx.conf \ --http-log-path=/var/log/nginx/access.log \ --error-log-path=/var/log/nginx/error.log \ --lock-path=/var/lock/nginx.lock \ --pid-path=/run/nginx.pid \ --modules-path=/usr/lib/nginx/modules \ --http-client-body-temp-path=/var/lib/nginx/body \ --http-fastcgi-temp-path=/var/lib/nginx/fastcgi \ --http-proxy-temp-path=/var/lib/nginx/proxy \ --http-scgi-temp-path=/var/lib/nginx/scgi \ --http-uwsgi-temp-path=/var/lib/nginx/uwsgi \ --with-debug \ --with-pcre-jit \ --with-http_ssl_module \ --with-http_stub_status_module \ --with-http_realip_module \ --with-http_auth_request_module \ --with-http_v2_module \ --with-http_dav_module \ --with-http_slice_module \ --with-threads \ --with-http_addition_module \ --with-http_gunzip_module \ --with-http_gzip_static_module \ --with-http_sub_module \ --with-http_xslt_module=dynamic \ --with-stream=dynamic \ --with-stream_ssl_module \ --with-stream_ssl_preread_module \ --with-mail=dynamic \ --with-mail_ssl_module \ --add-dynamic-module=/usr/local/src/ngx_http_geoip2_module-3.0 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
configuring additional dynamic modules adding module in /usr/local/src/ngx_http_geoip2_module-3.0/ checking for MaxmindDB library ... found + ngx_geoip2_module was configured checking for PCRE library ... found checking for PCRE JIT support ... found checking for OpenSSL library ... found checking for zlib library ... found checking for libxslt ... found checking for libexslt ... found creating objs/Makefile Configuration summary + using threads + using system PCRE library + using system OpenSSL library + using system zlib library nginx path prefix: "/usr/share/nginx" nginx binary file: "/usr/share/nginx/sbin/nginx" nginx modules path: "/usr/lib/nginx/modules" nginx configuration prefix: "/etc/nginx" nginx configuration file: "/etc/nginx/nginx.conf" nginx pid file: "/run/nginx.pid" nginx error log file: "/var/log/nginx/error.log" nginx http access log file: "/var/log/nginx/access.log" nginx http client request body temporary files: "/var/lib/nginx/body" nginx http proxy temporary files: "/var/lib/nginx/proxy" nginx http fastcgi temporary files: "/var/lib/nginx/fastcgi" nginx http uwsgi temporary files: "/var/lib/nginx/uwsgi" nginx http scgi temporary files: "/var/lib/nginx/scgi" |
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
1 2 3 4 5 |
cd /usr/sbin sudo ln -s /usr/share/nginx/sbin/nginx nginx cd /usr/share/nginx/ sudo ln -s /usr/lib/nginx/modules modules sudo mkdir -p /var/lib/nginx/body |
For the Systemd service file, place the following in /lib/systemd/system/nginx.service
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[Unit] Description=The NGINX HTTP and reverse proxy server After=syslog.target network.target remote-fs.target nss-lookup.target [Service] Type=forking PIDFile=/run/nginx.pid ExecStartPre=/usr/sbin/nginx -t ExecStart=/usr/sbin/nginx ExecReload=/usr/sbin/nginx -s reload ExecStop=/bin/kill -s QUIT $MAINPID PrivateTmp=true [Install] WantedBy=multi-user.target |
and reload systemd
with sudo systemctl daemon-reload
. You should now be able to check the status of nginx
:
1 2 3 4 |
sudo systemctl status nginx nginx.service - The NGINX HTTP and reverse proxy server Loaded: loaded (/lib/systemd/system/nginx.service; disabled; vendor preset: enabled) Active: inactive (dead) |
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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
sudo geoipupdate -v geoipupdate 3.1.0 Opened License file /etc/GeoIP.conf Insert edition_id GeoLite2-Country Insert edition_id GeoLite2-City Read in license key /etc/GeoIP.conf Number of edition IDs 2 url: https://updates.maxmind.com/app/update_getfilename?product_id=GeoLite2-Country md5hex_digest: 00000000000000000000000000000000 url: https://updates.maxmind.com/geoip/databases/GeoLite2-Country/update?db_md5=00000000000000000000000000000000 Uncompress file /usr/share/GeoIP/GeoLite2-Country.mmdb.gz to /usr/share/GeoIP/GeoLite2-Country.mmdb.test Rename /usr/share/GeoIP/GeoLite2-Country.mmdb.test to /usr/share/GeoIP/GeoLite2-Country.mmdb url: https://updates.maxmind.com/app/update_getfilename?product_id=GeoLite2-City md5hex_digest: 00000000000000000000000000000000 url: https://updates.maxmind.com/geoip/databases/GeoLite2-City/update?db_md5=00000000000000000000000000000000 Uncompress file /usr/share/GeoIP/GeoLite2-City.mmdb.gz to /usr/share/GeoIP/GeoLite2-City.mmdb.test Rename /usr/share/GeoIP/GeoLite2-City.mmdb.test to /usr/share/GeoIP/GeoLite2-City.mmdb |
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:
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
Well spotted, and fixed!
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