Sadly, there’s a ton of malicious activity going on all the time on the Internet. One thing that can be done to reduce some of this traffic is simply to limit your website to users coming from specified countries. My approach is to deny by default and whitelist a small number of countries. I’m looking for a better server-wide solution that works with Linux, but here’s something I found that works for individual websites using Apache’s configuration files and MaxMind’s products/services. Important: if you access your web server “locally” (on the same LAN, or even same computer) read the notes at the end.
1. For simplicity, assume all command are run with root privileges:
su -
2. You’ll need some development stuff to build the MaxMind software:
yum -y install gcc-c++ httpd-devel wget
3. Create a temporary directory to make cleanup simpler later:
mkdir ~/tmp
4. Download the geographical database that relates IP addresses to countries (you’ll need to periodically update this as IP address locations change). It used to be you could just download this data. Now they ask for a bunch of personal and contact data to download it, so I’m not sure I’ll keep using this service. I’ll test it out for the time being though just to update my notes. The main download page can be found here: http://dev.maxmind.com/geoip/geoip2/geolite2/. This eventually led me here: https://www.maxmind.com/en/accounts/473295/geoip/downloads (after being forced to divulge an email address, etc.). I downloaded a file for “GeoLite2 Country” named “GeoLite2-Country_20201229.tar.gz”. Again, this used to be an easy thing, so I’m skeptically moving forward…
cd ~/tmp tar xf GeoLite2-Country_20201229.tar.gz cd GeoLite2-Country_20201229 mkdir -p /opt/geoip && mv GeoLite2-Country.mmdb /opt/geoip/
5. Download, build, and install the MaxMind database library. The main download page can be found here: https://github.com/maxmind/libmaxminddb/releases
cd ~/tmp wget https://github.com/maxmind/libmaxminddb/releases/download/1.4.3/libmaxminddb-1.4.3.tar.gz tar xf libmaxminddb-1.4.3.tar.gz cd libmaxminddb-1.4.3/ ./configure make make install sh -c "echo /usr/local/lib >> /etc/ld.so.conf.d/local.conf" ldconfig
6. Download, build, and install the MaxMind Apache module. The main download page can be found here: https://github.com/maxmind/mod_maxminddb/releases
cd ~/tmp wget<a href="https://github.com/maxmind/mod_maxminddb/releases/download/1.2.0/mod_maxminddb-1.2.0.tar.gz"> https://github.com/maxmind/mod_maxminddb/releases/download/1.2.0/mod_maxminddb-1.2.0.tar.gz</a> tar -xzf mod_maxminddb-1.2.0.tar.gz cd mod_maxminddb-1.2.0 ./configure make make install
7. When the Apache module was installed, it attempted to update the apache/httpd configuration. This worked for me on one server and failed on another, so I would check /etc/httpd/conf/httpd.conf to ensure that the following line is present:
LoadModule maxminddb_module /usr/lib64/httpd/modules/mod_maxminddb.so
8. Restart Apache:
/bin/systemctl restart httpd.service
9. Now let’s use this on a website. Modify the .htaccess for your website to add the following configuration to only allow whitelisted country codes (the example just allows the US and Canada):
<IfModule maxminddb_module> ErrorDocument 403 "403" Order Deny,Allow Deny From All MaxMindDBEnable On MaxMindDBFile DB /opt/geoip/GeoLite2-Country.mmdb MaxMindDBEnv MM_COUNTRY_CODE DB/country/iso_code SetEnvIf MM_COUNTRY_CODE ^(US|CA) AllowCountry Allow from env=AllowCountry </IfModule>
Notes
- To test this, I recommend using something like the privateinternetaccess.com service. By setting different PIA servers you can attempt to access your website from different countries.
- If you are testing on the same network as your server (using LAN IP addresses), your local addresses are not routable over the Internet and therefore will not be recognized by the geo database. Using the approach I have recommended here will essentially disable access to your website from your LAN. You can simply append “Allow from 192.168.1.0/24” to you .htaccess file for local access (replace 192.168.1.0/24 with whatever your LAN uses). If you access the website from the same machine, you may need to add “Allow from 127.0.0.1”.
- If you prefer to put the website’s MacMind DB config in the httpd.conf file (instead of the .htaccess file), just put the same block of code inside a directory container (see complete example below).
Complete example using httpd.conf (no changes to .htaccess)
<VirtualHost *:80> ServerName www.domain.com ServerAlias domain.com DocumentRoot /var/www/domain.com/web ErrorLog /var/www/domain.com/log/error.log RewriteEngine on RewriteCond %{HTTP_HOST} !^www\. RewriteRule ^ http://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L] ServerAdmin cj@domain.com <Directory "/var/www/domain.com/web"> AllowOverride All Require all granted <IfModule maxminddb_module> ErrorDocument 403 "403" Order Deny,Allow Deny From All MaxMindDBEnable On MaxMindDBFile DB /opt/geoip/GeoLite2-Country.mmdb MaxMindDBEnv MM_COUNTRY_CODE DB/country/iso_code SetEnvIf MM_COUNTRY_CODE ^(US|CA) AllowCountry Allow from env=AllowCountry Allow from 10.1.10.0/24 </IfModule> </Directory> </VirtualHost> <VirtualHost *:443> ServerName www.domain.com ServerAlias domain.com DocumentRoot /var/www/domain.com/web ErrorLog /var/www/domain.com/log/error.log RewriteEngine on RewriteCond %{HTTP_HOST} !^www\. RewriteCond %{HTTPS}s ^on(s)| RewriteRule ^ http%1://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L] ServerAdmin cj@domain.com <Directory "/var/www/domain.com/web"> AllowOverride All Require all granted <IfModule maxminddb_module> ErrorDocument 403 "403" Order Deny,Allow Deny From All MaxMindDBEnable On MaxMindDBFile DB /opt/geoip/GeoLite2-Country.mmdb MaxMindDBEnv MM_COUNTRY_CODE DB/country/iso_code SetEnvIf MM_COUNTRY_CODE ^(US|CA) AllowCountry Allow from env=AllowCountry Allow from 10.1.10.0/24 </IfModule> </Directory> <IfModule mod_ssl.c> SSLEngine on SSLProtocol All -SSLv2 -SSLv3 SSLCertificateFile /var/www/domain.com/ssl/www.domain.com.cert.pem SSLCertificateKeyFile /var/www/domain.com/ssl/www.domain.com.key.pem </IfModule> </VirtualHost>
Another Example – Using Redirects Instead
This might be a little friendlier approach. This example “allows” by default and redirects users from the specified countries to a web page (replace “domain.com” and replace “XX|YY|ZZ” with whatever country codes irk you).
# Block list of countries codes <IfModule maxminddb_module> Order Allow,Deny MaxMindDBEnable On MaxMindDBFile DB /opt/geoip/GeoLite2-Country.mmdb MaxMindDBEnv MM_COUNTRY_CODE DB/country/iso_code RewriteEngine on RewriteCond %{ENV:MM_COUNTRY_CODE} ^(XX|YY|ZZ)$ RewriteCond %{REQUEST_FILENAME} !some_web_page.html RewriteCond %{REQUEST_FILENAME} !some_image_used_in_web_page.png RewriteRule ^(.*)$ http://www.domain.com/some_web_page.html [L] Allow from All </IfModule>
Getting a little fancier with PHP
I wanted to see how this could be used in a PHP file (to get geo data and do something interesting with it). Here’s what I did…
(Most of this came from https://github.com/maxmind/MaxMind-DB-Reader-php)
PHP library:
curl -sS https://getcomposer.org/installer | php php composer.phar require geoip2/geoip2:~2.0
C/C++ library (makes things a little faster):
svn co https://github.com/maxmind/MaxMind-DB-Reader-php cd MaxMind-DB-Reader-php/trunk/ext yum -y install php-devel phpize ./configure make make install
Edit /etc/php.ini to include “extension=maxminddb.so”. I restarted httpd after this (pretty sure that’s necessary, but I haven’t verified yet).
Download the DBs:
mkdir -p /opt/geoip/ && cd /opt/geoip/ wget http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz wget http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.mmdb.gz
I changed the owner of the DBs to apache (not sure if that was really needed). Next, I put the code to use in a PHP file. Here’s an example:
<?php require 'vendor/autoload.php'; use MaxMind\Db\Reader; $ipAddress = $_SERVER["REMOTE_ADDR"]; $databaseFile = '/opt/geoip/GeoLite2-City.mmdb'; $reader = new Reader($databaseFile); $city = $reader->get($ipAddress)[city][names][en]; $country = $reader->get($ipAddress)[country][names][en]; $subdivisions = $reader->get($ipAddress)[subdivisions][0][names][en]; $reader->close() ?><html> <head> <title>Hello<?php if ($country != "") echo " " . $country; ?>!</title> <style> a:link { color: #ffffff; } a:visited { color: #ffffff; } a:hover { color: #ffffff; } a:active { color: #ffffff; } </style> </head> <body style="background-color:#000000; color:#ffffff"> <br /><br /> <div style="text-align:center"> <h1>Hello<?php if ($country != "") echo " " . $country; ?>!</h1> <hr /> <?php if ($city != "" || $subdivisions != "" || $country != "") { if ($city != "") echo "<br />City: " . $city; if ($country != "") echo "<br />Country: " . $country; } ?> </div> </body> </html>