Geoblocking with NGINX on CentOS
Last week we mentioned the use of geoblocking as an additional layer of protection from specific threats. The benefits of geoblocking are rarely worth the efforts (and risks) but it can be useful in certain instances. This is true when absolutely certain that access to a service can be restricted to a particular geographic location, even for remote / mobile users. At the very least, geoblocking can reduce significant levels of "noise" caused mostly by bots and automated scanners. (Geoblocking is also useful for some non-security purposes, such as when licensing agreements or content regulations require it).
State or city based geoblocking is possible but IP database accuracy at that level becomes questionable, especially given the way modern routing works. It's also difficult to think of a plausible use case for this.
A few warnings to keep in mind:
- Attackers can come from anywhere and easily bypass geoblocks
- Most organizations need to keep services available globally
- Mistakes are easy, especially if IP databases are not updated regularly
The Tutorial
Geoblocking is most effective on the perimeter and best implemented on a network firewall or a WAF. Implementation directly on a webserver is helpful for situations where the webserver is connected directly to the Internet. It is also useful for rapid deployment or when the ability to modify firewalls is limited, or when geoblocking is only one part of a more complex access control configuration.
This tutorial assumes a working implementation of nginx on CentOS, though similar configurations should work on most other variations of Linux without much modification. Default file locations for CentOS will be assumed and should be altered as necessary. All changes will be made in the default configuration file /etc/ngninx/nginx.conf. However, some installations may include configuration-specific files in /etc/nginx/conf.d/ which will need to be altered instead.
Prerequisites
Install the packages listed below using the yum command.
yum install GeoIP geoipupdate nginx-module-geoip
The nginx repo should already be configured (there is no need for the epel repo as indicated in some older tutorials online). Note that some package dependencies may be required depending on what has already been installed on the system.
We will utilize the commercial geoip databases from Maxmind. Maxmind also offers a free GeoLite2 database that is less accurate. Both require an account for automatic updates (see this article on how CCPA has affected the free download).
Name | Description | Repo |
GeoIP | The Maxmind IP database and C libraries for lookups | @base |
geoipupdate | Tool for automatically updating IP geo database | @base |
nginx-module-geoip | Nginx modules for dynamic GeoIP lookups | @ngninx |
Enable the Modules
Add the lines below to the nginx configuration file /etc/nginx/nginx.conf. Make sure it appears above any http directives. Note that some installations allow you to include the directives in a separate file in the /usr/share/nginx/modules directory.
load_module modules/ngx_http_geoip_module.so;
load_module modules/ngx_stream_geoip_module.so;
Make the Decision
Add the following lines inside the http directive in /etc/nginx/nginx.conf and modify the countries as desired. To see a list of all country codes supported, check the ISO 3166 standard or see the Maxmind specific documentation.
geoip_country /usr/share/GeoIP/GeoIP.dat;
map $geoip_country_code $allowed_country {
default no;
US yes; #USA
CA yes; #Canada
}
This will check the source IP and assign a value of yes or no to the variable $allowed_country based on the rules defined within the block. (In the example above, only users in the US and Canada will be allowed).
Like all access controls, geoblocking works best with a deny default. To block only a defined set of specific countries, change the default to "yes" and switch the value next to the specified countries.
Restricting Access
Add the following lines inside the server directive in /etc/nginx/nginx.conf. For intances with multiple server directives, use this on all servers where geoblocking is required.
if ($allowed_country = no) {
return 444;
}
Any http error code can be used, but in the configuration above error code 444 is leveraged. This is a nonstandard nginx specific error code which simply closes the connection without actually sending a response to the browser. It will appear in server error logs, however, and can be useful in analyzing how often geoblocking has been used.
Restart nginx for changes to take effect.
systemctl restart nginx
Allowing Local Lan Access
To allow addresses that are not listed in the IP database, such as local LAN addresses, the following can be added to the country block. Note that this will allow any IP that is not listed in the geo database.
'' yes; #allow unknowns
A better way to achieve the same is by coupling a geo directive with an additional if statement.
#insert into http section
geo $lan-ip {
default no;
192.168.1.0/24 yes;
}
#insert into server section, above 'if ($allowed_country = no)'
if ($lan-ip = yes) {
set $allowed_country yes;
}
Automatic Updates
This section assumes that automatic updates will be performed under the context of the root user. However, it is possible (and recommended) to alter file permissions and/or locations in such a way that a dedicated, non-root service account performs the updates.
To enable automatic updates to the IP database sign up for an account on the Maxmind website and obtain an account ID and license key.
Edit these AccountID and LicenseKey directives to match those associated with your account in the file /etc/GeoIP.conf. Additionally, ensure the DatabaseDirectory directive matches the entry for geoip_country in /etc/nginx/nginx.conf (/usr/share/GeoIP by default). Note that additional configurations might be necessary if the web server needs access through a proxy or firewall.
Test the implementation by running the geoipupdate command. If successful, use crontab -e to add the command to crontab. Be mindful that download frequency limitations may apply, even to paid accounts. The example below will update the GeoIP database nightly at 2AM.
0 2 * * * /usr/bin/geoipupdate