GeoIP at work

March 4, 2018 technews

In these months some customers ask for location-based access limitation to their application or a shared application, like PHPMyAdmin or Webmail, for their login. For this reason I’ve upgraded our FTP and Web Access infrastructure, moving to a FTP daemon that supports GeoIP and a Web Application Firewall application that supports this technology. Now I’m ready to share these enhancements in this article and explain how I protect FTP and Web application:

  • from unwanted FTP/Web login attempt from “unsafe” countries like China and Korea
  • limiting the access, for selected login, to some resources from specific locations

Prerequisites

Debian, the only Linux Distribution that I recognize as valid, delivers the MaxMind GeoIP database in two different formats:

  • a distribution-based updated database, packaged as geoip-database on main repo, that contains GeoIP (Country based) only database
  • a script-based updated database, packaged as geoip-database-contrib in contrib repo, that adds GeoLiteCity and GeoIPASNum database

I prefer the second approach and I scheduled the monthly update using crontab with:

cat <<\EOF | sudo tee /etc/cron.d/geoip-update-db
MAILTO=""
30 4 1 * * * root /usr/sbin/update-geoip-database
EOF

FTP

FTP daemons like proftpd support the MaxMind GeoIP database for location-based access list; on Debian you can install the needed software with:

apt install proftpd-mod-geoip

that installs all the needed prerequisites (proftpd daemon) and the mod-geoip additional module; I suggest to install the clamav module too, but this is out of the scope of this article. Once installed, you need to enable the module in /etc/proftpd/modules.conf:

LoadModule mod_geoip.c

and configure it. Current Debian version (1.3.5) does not support the integration with a SQL data source that the newer one (1.3.6+) does, so if you use virtual users based in SQL database you should create a script that periodically dumps your user-based restriction on the filesystem and reload the proftpd daemon. The following one is a sample basic configuration:

cat <<\EOF | sudo tee /etc/proftpd/conf.d/geoip.conf
# Whitelist local access
<Class geoip-whitelist>
   From 127.0.0.1
   From 192.168.0.0/24
</Class>
 
<IfClass geoip-whitelist>
  GeoIPEngine off
</IfClass>
<IfClass !geoip-whitelist>
  GeoIPEngine on
</IfClass>
 
GeoIPPolicy allow,deny
GeoIPTable /usr/share/GeoIP/GeoLiteCity.dat
GeoIPTable /usr/share/GeoIP/GeoIP.dat  MemoryCache UTF8
 
GeoIPDenyFilter CountryCode (UA|ID|YU|LT|EG|RO|BG|TR|RU|PK|MY|CN)
EOF

This configuration:

  • enables GeoIPEngine only for the access from the outside world, whitelisting the localhost and the local network
    loads the standard GeoIP database
  • denies access from some countries like China, Turkey or Romania; the ProFTPD daemon will refuse the connection from selected countries with a 421 error code

To apply a more-restrictive configuration to a specific user you can specify:

cat <<\EOF | sudo tee /etc/proftpd/conf.d/geoip.restriction.conf
<IfUser testuser>
GeoIPAllowFilter CountryCode (IT|CH)
GeoIPDenyFilter Continent (EU|AS|NA|AS|AF|SA|OC)
</IfUser>
<IfUser swissuser>
GeoIPAllowFilter CountryCode CH
GeoIPDenyFilter Continent (EU|AS|NA|AS|AF|SA|OC)
</IfUser>
EOF

that permits the access to testuser username from IT/CH only; to obtain this result you need to specify a IfUser block that blacklists the entire world using the Continent filter and whitelists the IT/CH country. The GeoIPPolicy that evaluates IPAllow filter before the IPDenyFilter will do the rest. If you try to connect from Romania:

root@romania-server:~# ftp ftp.mycompany.net
Connected to ftp.mycompany.net.
421 Service not available, remote server has closed connection
ftp>

and the syslog will report this:

Mar  4 12:14:26 ftp-b proftpd[5395]: ftp-b (srvXX.e-presbox.com[89.46.***.****]) - mod_geoip/0.7: Connection denied to 89.46.***.*** due to GeoIP filter/policy
Mar  4 12:14:26 ftp-b proftpd[5395]: ftp-b (srvXX.e-presbox.com[89.46.***.***]) - mod_geoip.c: error initializing session: Permission denied
Mar  4 12:14:26 ftp-b proftpd[5395]: ftp-b (srvXX.e-presbox.com[89.46.***.***]) - FTP session closed.

You can check how the GeoIP will categorize the remote IP address with the getgeoip optional program:

# geoiplookup 89.46.***.***
 
GeoIP Country Edition: RO, Romania
GeoIP City Edition, Rev 1: RO, N/A, N/A, N/A, N/A, 46.000000, 25.000000, 0, 0
GeoIP ASNum Edition: AS48874 Hostmaze Inc Srl-d

If you try to connect from Germany you can reach the login and password request but, after a valid login with testuser, you’ll obtain the same error and the log will report:

Mar  4 12:03:45 ftp-a proftpd[5395]: ftp-a (185.72.***.***[185.72.***.***]) - FTP session opened.
Mar  4 12:03:45 ftp-a proftpd[5395]: ftp-a (185.72.***.***[185.72.***.***]) - mod_geoip/0.7: Connection denied to 185.72.***.*** due to GeoIP filter/policy
Mar  4 12:03:45 ftp-a proftpd[5395]: ftp-a (185.72.***.***[185.72.***.***]) - FTP session closed.

Web

Web access can be protected using Apache 2 as Reverse Proxy and ModSecurity with Core Rule Set 3 as Application Firewall. The CRS 3 defines many common attack pattern and can be useful, once configured and tuned to the specific workload, to protect an application to the most used attack methods. Debian packages the needed software as:

apt install libapache2-mod-security2 libapache2-mod-geoip

and you can download the CRS3 from github and configure Apache to use the ModSecurity and GeoIP engine:

###########################################
# ModSecurity
###########################################
SecRuleEngine                 On
 
SecRequestBodyAccess          On
SecRequestBodyLimit           20000000
SecRequestBodyNoFilesLimit    64000
 
SecResponseBodyAccess         On
SecResponseBodyLimit          10000000
 
SecPcreMatchLimit             100000
SecPcreMatchLimitRecursion    100000
 
SecTmpSaveUploadedFiles       On
SecUploadKeepFiles            RelevantOnly
SecTmpDir                     /var/cache/modsecurity/upload/tmp/
SecUploadDir                  /var/cache/modsecurity/upload/store/
SecDataDir                    /var/cache/modsecurity/data/
 
SecAuditEngine                RelevantOnly
SecAuditLogRelevantStatus     "^(?:5|4(?!04))"
SecAuditLogParts              ABIJEFHKZ
 
SecAuditLogType               Concurrent
SecAuditLog                   /var/log/apache2/modsec_audit.log
SecAuditLogStorageDir         /var/log/apache2/audit/
 
Include             ${ApacheBasePath}/modsecurity/crs-setup.conf
IncludeOptional     ${ApacheBasePath}/modsecurity/addons/*.pre.conf
IncludeOptional     ${ApacheBasePath}/modsecurity/crs300/*.conf
IncludeOptional     ${ApacheBasePath}/modsecurity/addons/*.post.conf
 
###########################################
# GeoIP
###########################################
GeoIPEnable On

The crs-setup.conf file contains the needed configuration to use GeoIP database:

SecGeoLookupDB /usr/share/GeoIP/GeoIP.dat

and a pre-compiled rule that defines the “High Risk” country code list that blocks requests from these countries:

SecAction \
 "id:900600,\
  phase:1,\
  nolog,\
  pass,\
  t:none,\
  setvar:'tx.high_risk_country_codes=UA ID YU LT EG RO BG TR RU PK MY CN'"

You can see the same list used in ProFTPD daemon. When the request is blocked using this list the Apache’s error log displays:

Mar  4 12:34:08 waf-b-eqs apache2[121918]: [Sun Mar 04 12:34:08 2018] [error] [pid 121918] apache2_util.c(273): [client 42.236.***.***:11810] [appid PHP56] [client 42.236.***.***] ModSecurity: Warning. String match within "UA ID YU LT EG RO BG TR RU PK MY CN" at GEO:COUNTRY_CODE. [file "/opt/apache-waf/modsecurity/crs300/REQUEST-910-IP-REPUTATION.conf"] [line "71"] [id "910100"] [msg "Client IP is from a HIGH Risk Country Location."] [severity "CRITICAL"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-reputation-ip"] [hostname "www.***.com"] [uri "/"] [unique_id "WpvZsKwSeBkAAdw@P14AAAUW"]

If you want to use a specific protection for WordPress login pages you can specify a good-guys country list:

# Allowed country for MGMT
SecAction "id:10099,phase:1,nolog,pass,t:none,setvar:'tx.good_guys=IT,CH,US,FR,DE,ES'"
and protect some key resources with:

SecRule ENV:GEOIP_COUNTRY_CODE "!@within %{tx.good_guys}" "id:10110,phase:request,drop,t:none,log,tag:'Mine Ruleset',msg:'Unwanted wp-login request from bad states',chain"
SecRule REQUEST_URI            "@strmatch wp-login.php" "t:none"
 
SecRule ENV:GEOIP_COUNTRY_CODE "!@within %{tx.good_guys}" "id:10111,phase:request,drop,t:none,log,tag:'Mine Ruleset',msg:'Unwanted xmlrpc request from bad states',chain"
SecRule REQUEST_URI            "@strmatch xmlrpc.php" "t:none"

the effect will be:

Mar  4 12:40:46 waf-a-eqs apache2[82013]: [Sun Mar 04 12:40:46 2018] [error] [pid 82013] apache2_util.c(273): [client 103.51.***.***:53005] [appid PHP56] [client 103.51.***.***] ModSecurity: Access denied with connection close (phase 2). Pattern match "wp-login.php" at REQUEST_URI. [file "/opt/apache-waf/modsecurity/addons/addons.pre.conf"] [line "30"] [id "10110"] [msg "Unwanted wp-login request from bad states"] [tag "Mine Ruleset"] [hostname "www.***.it"] [uri "/wp-login.php"] [unique_id "WpvbPn8AAAEAAUBdyjcAAAAN"]

You can do the same for PHPMyAdmin, and limit the swissuser access only from a specific country:

<Virtualhost PHPMyAdmin>
SecRule REQUEST_METHOD         "POST"                         "id:30002,phase:request,t:none,deny,msg:'PMA (login) with selfee_admin outside CH',chain"
SecRule REQUEST_URI            "@rx index.php"                "t:none,chain"
SecRule ARGS:pma_username      "@streq swissuser"             "t:none,chain"
SecRule ENV:GEOIP_COUNTRY_CODE "!@within CH"                  "t:none"
</VirtualHost>

And the effect is:

Mar  3 21:08:49 waf-c-eqs apache2[10797]: [Sat Mar 03 21:08:49 2018] [error] [pid 10797] apache2_util.c(273): [client ***.***.***.***:52164] [appid PMA] [client ***.***.***.***] ModSecurity: Access denied with code 406 (phase 2). Match of "within CH" against "ENV:GEOIP_COUNTRY_CODE" required. [file "/opt/apache-waf/modsecurity/addons/pma.pre.conf"] [line "1"] [id "30001"] [msg "PMA (login) with swissuser outside IT"] [hostname "pma.***.***"] [uri "/index.php"] [unique_id "WpsA0awSeBkAACot5vYAAAAX"]

Extra features

You can also enrich the Apache Access Log file with the detected country code with:

GeoIPOutput Env
LogFormat "%V %h %{GEOIP_COUNTRY_CODE}e [....]" combined_v1

so you can see the detected country code for each request and review your white/black list, eg:

Mar  4 12:06:45 waf-c-eqs hosting: www.angeloxx.it 54.209.***.*** US - [04/Mar/2018:12:06:44 +0100] "GET / HTTP/1.1" 200 28862 "-" "Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1" ---- uuid:WpvTRKwSeBkAALtxtB0AAAAV session:- appid:PHP71-SSL proxy:****** state:200 time:697653 masi:0 maso:0 msti:1703 mapp:686494 msto:5128 bin:547 bout:35787 ssl:TLSv1.2 cipher:ECDHE-RSA-AES256-GCM-SHA384 cache:"-" notes:"-"

This is another story, but you can see that my Apache log file is a little bit weird! When you use modsecurity, you probably want to log the detected anomaly score for input and output (masi/maso) to tune your configuration and avoid false positive, the negotiated TLS protocol and cipher and some additional information like the backend server elected by the mod_balancer Apache module. All this information is logged using this LogFormat:

LogFormat "%V %h %{GEOIP_COUNTRY_CODE}e %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" ---- uuid:%{UNIQUE_ID}e session:%{SESSIONID}e appid:%{WEBAPPID}M proxy:%{BALANCER_WORKER_ROUTE}e state:%>s time:%D masi:%{ModSecAnomalyScoreIn}e maso:%{ModSecAnomalyScoreOut}e msti:%{ModSecTimeIn}e mapp:%{ApplicationTime}e msto:%{ModSecTimeOut}e bin:%I bout:%O ssl:%{SSL_PROTOCOL}x cipher:%{SSL_CIPHER}x cache:\"%{cache-status}e\" notes:\"%{notes}e\"" combined_v1

but you need to collect some additional information like:

# === ModSec timestamps at the start of each phase (ids: 90000 - 90009)
 
SecAction "id:90000,phase:1,nolog,pass,setvar:TX.ModSecTimestamp1start=%{DURATION}"
SecAction "id:90001,phase:2,nolog,pass,setvar:TX.ModSecTimestamp2start=%{DURATION}"
SecAction "id:90002,phase:3,nolog,pass,setvar:TX.ModSecTimestamp3start=%{DURATION}"
SecAction "id:90003,phase:4,nolog,pass,setvar:TX.ModSecTimestamp4start=%{DURATION}"
SecAction "id:90004,phase:5,nolog,pass,setvar:TX.ModSecTimestamp5start=%{DURATION}"
 
Include             ${ApacheBasePath}/modsecurity/crs-setup.conf
IncludeOptional     ${ApacheBasePath}/modsecurity/addons/*.pre.conf
IncludeOptional     ${ApacheBasePath}/modsecurity/crs300/*.conf
IncludeOptional     ${ApacheBasePath}/modsecurity/addons/*.post.conf
 
# === ModSec Timestamps at the End of Each Phase (ids: 90010 - 90019)
 
SecAction "id:90010,phase:1,pass,nolog,setvar:TX.ModSecTimestamp1end=%{DURATION}"
SecAction "id:90011,phase:2,pass,nolog,setvar:TX.ModSecTimestamp2end=%{DURATION}"
SecAction "id:90012,phase:3,pass,nolog,setvar:TX.ModSecTimestamp3end=%{DURATION}"
SecAction "id:90013,phase:4,pass,nolog,setvar:TX.ModSecTimestamp4end=%{DURATION}"
SecAction "id:90014,phase:5,pass,nolog,setvar:TX.ModSecTimestamp5end=%{DURATION}"
 
 
# === ModSec performance calculations and variable export (ids: 90100 - 90199)
 
SecAction "id:90100,phase:5,pass,nolog,\
  setvar:TX.perf_modsecinbound=%{PERF_PHASE1},\
  setvar:TX.perf_modsecinbound=+%{PERF_PHASE2},\
  setvar:TX.perf_application=%{TX.ModSecTimestamp3start},\
  setvar:TX.perf_application=-%{TX.ModSecTimestamp2end},\
  setvar:TX.perf_modsecoutbound=%{PERF_PHASE3},\
  setvar:TX.perf_modsecoutbound=+%{PERF_PHASE4},\
  setenv:ModSecTimeIn=%{TX.perf_modsecinbound},\
  setenv:ApplicationTime=%{TX.perf_application},\
  setenv:ModSecTimeOut=%{TX.perf_modsecoutbound},\
  setenv:ModSecAnomalyScoreIn=%{TX.anomaly_score},\
  setenv:ModSecAnomalyScoreOut=%{TX.outbound_anomaly_score}"

you can see that I surrounded the ModSecurity Rule Set with multiple SecActions that detect the begin and the end of the ModSecurity parsing for each request, calculate the time among each ModSecurity phases (see the https://www.netnea.com/cms/apache-tutorial-7_including-modsecurity-core-rules/ Netnea tutorial, the source of many golden rules about ModSecurity and Core Rule Set) and show how much time is spent by Apache in:

  • phases parsing
  • time spent by the backend application (ApplicationTime, mapp in log file) to answer
(cover image from https://www.flickr.com/photos/wallyg/4792216028)