{"id":3508,"date":"2018-09-02T16:25:17","date_gmt":"2018-09-02T21:25:17","guid":{"rendered":"https:\/\/dev.iachieved.it\/iachievedit\/?p=3508"},"modified":"2019-02-16T13:59:18","modified_gmt":"2019-02-16T19:59:18","slug":"geoip2-and-nginx","status":"publish","type":"post","link":"https:\/\/dev.iachieved.it\/iachievedit\/geoip2-and-nginx\/","title":{"rendered":"GeoIP2 and NGINX"},"content":{"rendered":"<p>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 <a href=\"https:\/\/www.maxmind.com\/en\/home\">MaxMind<\/a> GeoIP databases to look up a web client&#8217;s country-of-origin and have the web server respond accordingly is a popular technique.<\/p>\n<p>There are a number of <a href=\"https:\/\/www.nginx.com\/\">NGINX<\/a> tutorials on how to use the <a href=\"https:\/\/dev.maxmind.com\/geoip\/legacy\/\">legacy GeoIP database<\/a> and the <a href=\"https:\/\/nginx.org\/en\/docs\/http\/ngx_http_geoip_module.html\">ngx_http_geoip_module<\/a>, and as it happens the default Ubuntu <code>nginx<\/code> package includes the <code>ngx_http_geoip_module<\/code>.  Unfortunately the GeoIP databases will no longer be updated, and MaxMind has migrated to <a href=\"https:\/\/dev.maxmind.com\/geoip\/\">GeoIP2<\/a>.  Moreover, after January 2, 2019, the GeoIP databases <a href=\"https:\/\/support.maxmind.com\/geolite-legacy-discontinuation-notice\/\">will no longer be available<\/a>.<\/p>\n<p>This leaves us in a bind.  Luckily, while the Ubuntu distribution of NGINX doesn&#8217;t come with GeoIP2 support, we can add it by building from source.  Which is exactly what we&#8217;ll do!  In this tutorial we&#8217;re going to build <code>nginx<\/code> from the ground up, modeling its configuration options after those that are used by the canonical <code>nginx<\/code> packages available from Ubuntu 16.04.  You&#8217;ll want to go through this tutorial on a fresh installation of Ubuntu 16.04 or later; we&#8217;ll be using an EC2 instance created from the AWS Quick Start <em>Ubuntu Server 16.04 LTS (HVM), SSD Volume Type<\/em> AMI.<\/p>\n<blockquote><p>If you&#8217;re a fan of NGINX and hosting secure webservers, check out our latest post on <a href=\"https:\/\/dev.iachieved.it\/iachievedit\/tls-1-3-with-nginx-and-ubuntu-18-10\/\">configuring NGINX with support for TLS 1.3<\/a><\/p><\/blockquote>\n<h2>Getting Started<\/h2>\n<p>Since we&#8217;re going to be building binaries, we&#8217;ll need the <code>build-essential<\/code> package which is a metapackage that installs applications such as <code>make<\/code>, <code>gcc<\/code>, etc.<\/p>\n<pre class=\"lang:sh decode:true \">\nsudo apt-get update\nsudo apt-get install -y build-essential\n<\/pre>\n<p>Now, to install all of the prerequisities libraries we&#8217;ll need to compile NGINX:<\/p>\n<pre class=\"lang:sh\">\nsudo apt-get install -y libpcre3-dev zlib1g-dev libssl-dev libxslt1-dev\n<\/pre>\n<p>Using the GeoIP2 database with NGINX requires the <a href=\"https:\/\/github.com\/leev\/ngx_http_geoip2_module\">ngx_http_geoip2_module<\/a> and requires the MaxMind development packages from MaxMind:<\/p>\n<pre class=\"lang:sh\">\nsudo add-apt-repository -y ppa:maxmind\/ppa\nsudo apt-get update\nsudo apt-get install -y libmaxminddb-dev\n<\/pre>\n<h2>Getting the Sources<\/h2>\n<p>Now let&#8217;s go and download NGINX.  We&#8217;ll be using the latest dot-release of the 1.15 series, 1.15.3.  I prefer to compile things in <code>\/usr\/local\/src<\/code>, so:<\/p>\n<pre class=\"lang:sh\">\ncd \/usr\/local\/src\/\nsudo wget https:\/\/nginx.org\/download\/nginx-1.15.3.tar.gz\nsudo tar -xzvf nginx-1.15.3.tar.gz\n<\/pre>\n<p>We also need the source for the GeoIP2 NGINX module:<\/p>\n<pre class=\"lang:sh\">\nsudo wget https:\/\/github.com\/leev\/ngx_http_geoip2_module\/archive\/3.0.tar.gz\nsudo tar -xzvf 3.0.tar.gz\n<\/pre>\n<p>Now, to configure and compile.<\/p>\n<pre class=\"lang:sh nums:false\">\n\ncd nginx-1.15.3\nsudo .\/configure \\\n--with-cc-opt='-g -O2 -fPIE -fstack-protector-strong -Wformat -Werror=format-security -fPIC -Wdate-time -D_FORTIFY_SOURCE=2' \\\n--with-ld-opt='-Wl,-Bsymbolic-functions -fPIE -pie -Wl,-z,relro -Wl,-z,now -fPIC' \\\n--prefix=\/usr\/share\/nginx                 \\\n--conf-path=\/etc\/nginx\/nginx.conf         \\\n--http-log-path=\/var\/log\/nginx\/access.log \\\n--error-log-path=\/var\/log\/nginx\/error.log \\\n--lock-path=\/var\/lock\/nginx.lock          \\\n--pid-path=\/run\/nginx.pid                 \\\n--modules-path=\/usr\/lib\/nginx\/modules     \\\n--http-client-body-temp-path=\/var\/lib\/nginx\/body \\\n--http-fastcgi-temp-path=\/var\/lib\/nginx\/fastcgi  \\\n--http-proxy-temp-path=\/var\/lib\/nginx\/proxy      \\\n--http-scgi-temp-path=\/var\/lib\/nginx\/scgi        \\\n--http-uwsgi-temp-path=\/var\/lib\/nginx\/uwsgi      \\\n--with-debug                     \\\n--with-pcre-jit                  \\\n--with-http_ssl_module           \\\n--with-http_stub_status_module   \\\n--with-http_realip_module        \\\n--with-http_auth_request_module  \\\n--with-http_v2_module            \\\n--with-http_dav_module           \\\n--with-http_slice_module         \\\n--with-threads                   \\\n--with-http_addition_module      \\\n--with-http_gunzip_module        \\\n--with-http_gzip_static_module   \\\n--with-http_sub_module           \\\n--with-http_xslt_module=dynamic  \\\n--with-stream=dynamic            \\\n--with-stream_ssl_module         \\\n--with-stream_ssl_preread_module \\\n--with-mail=dynamic              \\\n--with-mail_ssl_module           \\\n--add-dynamic-module=\/usr\/local\/src\/ngx_http_geoip2_module-3.0\n<\/pre>\n<p>You will want to make sure that the ngx_http_geoip2_module will be compiled, and should see <code>nginx_geoip2_module was configured<\/code> in the end of the <code>configure<\/code> output.<\/p>\n<pre>\nconfiguring additional dynamic modules\nadding module in \/usr\/local\/src\/ngx_http_geoip2_module-3.0\/\nchecking for MaxmindDB library ... found\n + ngx_geoip2_module was configured\nchecking for PCRE library ... found\nchecking for PCRE JIT support ... found\nchecking for OpenSSL library ... found\nchecking for zlib library ... found\nchecking for libxslt ... found\nchecking for libexslt ... found\ncreating objs\/Makefile\n\nConfiguration summary\n  + using threads\n  + using system PCRE library\n  + using system OpenSSL library\n  + using system zlib library\n\n  nginx path prefix: \"\/usr\/share\/nginx\"\n  nginx binary file: \"\/usr\/share\/nginx\/sbin\/nginx\"\n  nginx modules path: \"\/usr\/lib\/nginx\/modules\"\n  nginx configuration prefix: \"\/etc\/nginx\"\n  nginx configuration file: \"\/etc\/nginx\/nginx.conf\"\n  nginx pid file: \"\/run\/nginx.pid\"\n  nginx error log file: \"\/var\/log\/nginx\/error.log\"\n  nginx http access log file: \"\/var\/log\/nginx\/access.log\"\n  nginx http client request body temporary files: \"\/var\/lib\/nginx\/body\"\n  nginx http proxy temporary files: \"\/var\/lib\/nginx\/proxy\"\n  nginx http fastcgi temporary files: \"\/var\/lib\/nginx\/fastcgi\"\n  nginx http uwsgi temporary files: \"\/var\/lib\/nginx\/uwsgi\"\n  nginx http scgi temporary files: \"\/var\/lib\/nginx\/scgi\"\n<\/pre>\n<p>Now, run <code>sudo make<\/code>.  NGINX, for all its power, is a compact and light application, and compiles in under a minute.  If everything compiles properly, you can run <code>sudo make install<\/code>.<\/p>\n<p>A few last things to complete our installation:<\/p>\n<ul>\n<li>creating a symlink from <code>\/usr\/sbin\/nginx<\/code> to <code>\/usr\/share\/nginx\/sbin\/nginx<\/code><\/li>\n<li>creating a symlink from <code>\/usr\/share\/nginx\/modules<\/code> to <code>\/usr\/lib\/nginx\/modules<\/code> <\/li>\n<li>creating the <code>\/var\/lib\/nginx\/body<\/code> directory<\/li>\n<li>installing an NGINX <a href=\"https:\/\/www.nginx.com\/resources\/wiki\/start\/topics\/examples\/systemd\/\">systemd service file<\/a><\/li>\n<\/ul>\n<pre class=\"lang:sh nums:false\">\ncd \/usr\/sbin\nsudo ln -s \/usr\/share\/nginx\/sbin\/nginx nginx\ncd \/usr\/share\/nginx\/\nsudo ln -s \/usr\/lib\/nginx\/modules modules\nsudo mkdir -p \/var\/lib\/nginx\/body\n<\/pre>\n<p>For the Systemd service file, place the following in <code>\/lib\/systemd\/system\/nginx.service<\/code>:<\/p>\n<pre class=\"lang:none nums:false\">\n[Unit]\nDescription=The NGINX HTTP and reverse proxy server\nAfter=syslog.target network.target remote-fs.target nss-lookup.target\n\n[Service]\nType=forking\nPIDFile=\/run\/nginx.pid\nExecStartPre=\/usr\/sbin\/nginx -t\nExecStart=\/usr\/sbin\/nginx\nExecReload=\/usr\/sbin\/nginx -s reload\nExecStop=\/bin\/kill -s QUIT $MAINPID\nPrivateTmp=true\n\n[Install]\nWantedBy=multi-user.target\n<\/pre>\n<p>and reload <code>systemd<\/code> with <code>sudo systemctl daemon-reload<\/code>.  You should now be able to check the status of <code>nginx<\/code>:<\/p>\n<pre class=\"lang:sh nums:false\">\nsudo systemctl status nginx\n   nginx.service - The NGINX HTTP and reverse proxy server\n   Loaded: loaded (\/lib\/systemd\/system\/nginx.service; disabled; vendor preset: enabled)\n   Active: inactive (dead)\n<\/pre>\n<p>We&#8217;ll be starting it momentarily!<\/p>\n<h2>Testing<\/h2>\n<p>On to testing!  We&#8217;re going to use HTTP (rather than HTTPS) in this example.<\/p>\n<p>While we&#8217;ve installed the libraries that interact with the GeoIP2 database, we haven&#8217;t yet installed the database itself.  This can be accomplished by installing the <code>geoipupdate<\/code> package from the MaxMind PPA:<\/p>\n<p>[code lang=text]<br \/>\n# sudo apt-get install -y geoipupdate<br \/>\n[\/code]<\/p>\n<p>Now run <code>sudo geoipupdate -v<\/code>:<\/p>\n<pre class=\"lang:sh nums:false\">\nsudo geoipupdate -v\ngeoipupdate 3.1.0\nOpened License file \/etc\/GeoIP.conf\nInsert edition_id GeoLite2-Country\nInsert edition_id GeoLite2-City\nRead in license key \/etc\/GeoIP.conf\nNumber of edition IDs 2\nurl: https:\/\/updates.maxmind.com\/app\/update_getfilename?product_id=GeoLite2-Country\nmd5hex_digest: 00000000000000000000000000000000\nurl: https:\/\/updates.maxmind.com\/geoip\/databases\/GeoLite2-Country\/update?db_md5=00000000000000000000000000000000\nUncompress file \/usr\/share\/GeoIP\/GeoLite2-Country.mmdb.gz to \/usr\/share\/GeoIP\/GeoLite2-Country.mmdb.test\nRename \/usr\/share\/GeoIP\/GeoLite2-Country.mmdb.test to \/usr\/share\/GeoIP\/GeoLite2-Country.mmdb\nurl: https:\/\/updates.maxmind.com\/app\/update_getfilename?product_id=GeoLite2-City\nmd5hex_digest: 00000000000000000000000000000000\nurl: https:\/\/updates.maxmind.com\/geoip\/databases\/GeoLite2-City\/update?db_md5=00000000000000000000000000000000\nUncompress file \/usr\/share\/GeoIP\/GeoLite2-City.mmdb.gz to \/usr\/share\/GeoIP\/GeoLite2-City.mmdb.test\nRename \/usr\/share\/GeoIP\/GeoLite2-City.mmdb.test to \/usr\/share\/GeoIP\/GeoLite2-City.mmdb\n<\/pre>\n<p>It&#8217;s a good idea to periodically update the GeoIP2 databases with <code>geoipupdate<\/code>.  This is typically accomplished with a <code>cron<\/code> job like:<\/p>\n<p>[code lang=text]<br \/>\n# crontab -l<br \/>\n30 0 * * 6 \/usr\/bin\/geoipupdate -v | \/usr\/bin\/logger<br \/>\n[\/code]<\/p>\n<p>Note:  Use of <code>logger<\/code> here is optional, we just like to see the output of the <code>geoipupdate<\/code> invocation in <code>\/var\/log\/syslog<\/code>.<\/p>\n<h2>Nginx Configuration<\/h2>\n<p>Now that <code>nginx<\/code> is built and installed, we have a GeoIP2 database in <code>\/usr\/share\/GeoIP<\/code>, we can finally get to the task of restricting access to our website.  Here is our basic <code>nginx.conf<\/code>:<\/p>\n<p>[code lang=text]<br \/>\nload_module modules\/ngx_http_geoip2_module.so;<\/p>\n<p>worker_processes auto;<\/p>\n<p>events {<br \/>\n  worker_connections  1024;<br \/>\n}<\/p>\n<p>http {<br \/>\n  sendfile      on;<br \/>\n  include       mime.types;<br \/>\n  default_type  application\/octet-stream;<br \/>\n  keepalive_timeout  65;<\/p>\n<p>  geoip2 \/usr\/share\/GeoIP\/GeoLite2-Country.mmdb {<br \/>\n    $geoip2_data_country_code country iso_code;<br \/>\n  }<\/p>\n<p>  map $geoip2_data_country_code $allowed_country {<br \/>\n    default no;<br \/>\n    US yes;<br \/>\n  }<\/p>\n<p>  server {<br \/>\n    listen       80;<br \/>\n    server_name  localhost;<\/p>\n<p>    if ($allowed_country = no) {<br \/>\n      return 403;<br \/>\n    }<\/p>\n<p>    location \/ {<br \/>\n        root   html;<br \/>\n        index  index.html index.htm;<br \/>\n    }<\/p>\n<p>    error_page   500 502 503 504  \/50x.html;<br \/>\n    location = \/50x.html {<br \/>\n        root   html;<br \/>\n    }<br \/>\n  }<br \/>\n}<br \/>\n[\/code]<\/p>\n<p>Let&#8217;s walk through the relevant directives one at a time.<\/p>\n<p><code>load_module modules\/ngx_http_geoip2_module.so;<\/code><\/p>\n<p>Since we built <code>nginx<\/code> with <code>ngx_http_geopip2_module<\/code> as a <i>dynamic<\/i> module, we need to load it explicitly with the <code>load_module<\/code> directive.<\/p>\n<p>Looking up the <a href=\"https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1\">ISO country code<\/a> from the GeoIP2 database utilizes our <code>geoip2<\/code> module:<\/p>\n<p>[code lang=text]<br \/>\ngeoip2 \/usr\/share\/GeoIP\/GeoLite2-Country.mmdb {<br \/>\n  $geoip2_data_country_code country iso_code;<br \/>\n}<br \/>\n[\/code]<\/p>\n<p>The country code of the client IP address will be placed in the NGINX variable <code>$geoip2_data_country_code<\/code>.  From this value we determine what to set <code>$allowed_country<\/code> to:<\/p>\n<p>[code lang=text]<br \/>\nmap $geoip2_data_country_code $allowed_country {<br \/>\n  default no;<br \/>\n  US yes;<br \/>\n}<br \/>\n[\/code]<\/p>\n<p><code>map<\/code> in the NGINX configuration file is a bit like a <code>switch<\/code> statement (I&#8217;ve chosen the Swift syntax of <code>switch<\/code>):<\/p>\n<p>[code lang=text]<br \/>\nswitch geoip2_data_country_code {<br \/>\n  case &#039;US&#039;:<br \/>\n    allowed_country = &quot;yes&quot;<br \/>\n  default:<br \/>\n    allowed_country = &quot;no&quot;<br \/>\n}<br \/>\n[\/code]<\/p>\n<p>If we wanted to allow IPs from the United States, Mexico, and Canada the <code>map<\/code> directive would look like:<\/p>\n<p>[code lang=text]<br \/>\nmap $geoip2_data_country_code $allowed_country {<br \/>\n  default no;<br \/>\n  US yes;<br \/>\n  MX yes;<br \/>\n  CA yes;<br \/>\n}<br \/>\n[\/code]<\/p>\n<p><code>geoip2<\/code> and <code>map<\/code> by themselves do not restrict access to the site.  This is accomplished through the <code>if<\/code> statement which is located in the <code>server<\/code> block:<\/p>\n<p>[code lang=text]<br \/>\nif ($allowed_country = no) {<br \/>\n  return 403;<br \/>\n}<br \/>\n[\/code]<\/p>\n<p>This is pretty self-explanatory.  If <code>$allowed_country<\/code> is <code>no<\/code> then return a <a href=\"https:\/\/httpstatuses.com\/403\">403 Forbidden<\/a>.<\/p>\n<p>If you haven&#8217;t done so already, start <code>nginx<\/code> with <code>systemctl start nginx<\/code> and give the configuration a go.  It&#8217;s quite easy to test your <code>nginx<\/code> configuration by disallowing your country, restarting <code>nginx<\/code> (<code>systemctl restart nginx<\/code>), and trying to access your site.<\/p>\n<h2>Credits and Disclaimers<\/h2>\n<p>NGINX and associated logos belong to NGINX Inc.  MaxMind, GeoIP, minFraud, and related trademarks belong to MaxMind, Inc.<\/p>\n<p>The following resources were invaluable in developing this tutorial:<\/p>\n<ul>\n<li><a href=\"https:\/\/www.howtoforge.com\/tutorial\/how-to-use-geoip-with-nginx-on-ubuntu-16.04\/\">How to use GeoIP with Nginx on Ubuntu 16.04<\/a>\n<li><a href=\"https:\/\/github.com\/leev\/ngx_http_geoip2_module\">GeoIP2 NGINX Module<\/a>\n<li><a href=\"https:\/\/kenkogeek.com\/en\/english-maxmind-geoip2-and-nginx-on-ubuntu-16-04\/\">Maxmind GeoIp2 and Nginx on Ubuntu 16.04<br \/>\n<\/a>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>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&#8217;s country-of-origin and have the web server respond accordingly is a popular technique. [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":3547,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[21,72,71],"tags":[73],"class_list":["post-3508","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops","category-nginx","category-ubuntu","tag-nginx-geoip2"],"_links":{"self":[{"href":"https:\/\/dev.iachieved.it\/iachievedit\/wp-json\/wp\/v2\/posts\/3508"}],"collection":[{"href":"https:\/\/dev.iachieved.it\/iachievedit\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/dev.iachieved.it\/iachievedit\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/dev.iachieved.it\/iachievedit\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/dev.iachieved.it\/iachievedit\/wp-json\/wp\/v2\/comments?post=3508"}],"version-history":[{"count":46,"href":"https:\/\/dev.iachieved.it\/iachievedit\/wp-json\/wp\/v2\/posts\/3508\/revisions"}],"predecessor-version":[{"id":3689,"href":"https:\/\/dev.iachieved.it\/iachievedit\/wp-json\/wp\/v2\/posts\/3508\/revisions\/3689"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/dev.iachieved.it\/iachievedit\/wp-json\/wp\/v2\/media\/3547"}],"wp:attachment":[{"href":"https:\/\/dev.iachieved.it\/iachievedit\/wp-json\/wp\/v2\/media?parent=3508"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/dev.iachieved.it\/iachievedit\/wp-json\/wp\/v2\/categories?post=3508"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/dev.iachieved.it\/iachievedit\/wp-json\/wp\/v2\/tags?post=3508"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}