We can dynamically add any IP to a blacklist (or whitelist) using the Apache module mod_security. It was really surprising that we could not find any existing solution after searching the Internet, but this facility to block any IP is very important because just like every other website, we also get a large number of unwanted HTTP requests from few IPs on a daily basis. Hence, we went ahead and implemented a solution to block such IPs and it is our pleasure to be sharing the solution with you.
Normally in Linux, iptables
is used to block any unwanted IP or IP range. Some example usage of using iptables
to block IPs is given below.
iptables -A INPUT -s 0.0.0.1 -j DROP
iptables -A INPUT -m iprange --src-range 0.0.0.0-0.0.0.255 -j DROP
iptables -A INPUT -s 0.0.0.0/24 -j DROP
iptables
is the most convenient and can be used if the machine is directly connected to the Internet without having a proxy or load balancer in front. But at hudku.com, we have Elastic Load Balancer in the front as our website runs as AWS Elastic Beanstalk application, running Apache on Linux. So the IP we get is always the IP of the load balancer and only in the HTTP header “X-Forwarded-For
“, we can get the IP of the requesting client. iptables
can only work with IP and we cannot make it use the values from an HTTP header.
Thus, we were forced to find alternative solutions and we decided to implement the facility using mod_security
. The logic is implemented by making use of the “Collections” provided by mod_security
. Collections are nothing but a HashMap
. The IP address is the key and the value is an integer. If the value is 1
, then we interpret it as white listed IP and if the value is 2
, then we consider the IP as blacklisted. If the key is not present, then IP is neither in whitelist nor in blacklist and hence gets treated normally.
If we find an IP in whitelist, no further checking gets done and the request gets honoured.
If we find an IP in blacklist, we drop the connection and log it in the modsecurity
log file.
To add an IP to the blacklist, we access a specific URL of our application, passing the IP as a parameter. It is as simple as www.example.com/ip/blacklist?ip=0.0.0.1. Similarly for whitelisting an IP, it is www.example.com/ip/whitelist?ip=0.0.0.2. For removing an IP from both whiltelist and blacklist, the command is www.example.com/ip/remove?ip=0.0.0.3 which removes the IP from the Collection (Map) if it exists.
Then, the most important thing is to ensure that these commands are honoured only if issued from the current machine, i.e., localhost.
When mod_security
is installed, it creates a directory called “modsecurity.d” and loads all the files with “.conf” extension present in that directory.
Armed with all the above information, now we are ready to look at the source code contained in the file my_rules.conf present in modsecurity.d directory.
SecRule REQUEST_FILENAME "^/ip/remove$"
"chain,phase:1,t:none,deny,nolog,status:200"
SecRule REMOTE_ADDR "^127.0.0.1$" "chain,t:none"
SecRule ARGS:ip "^\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b"
"t:none,initcol:ip=%{args.ip},setvar:!ip.allowed"
SecRule REQUEST_FILENAME "^/ip/whitelist$"
"chain,phase:1,t:none,deny,nolog,status:200"
SecRule REMOTE_ADDR "^127.0.0.1$" "chain,t:none"
SecRule ARGS:ip "^\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b"
"t:none,initcol:ip=%{args.ip},setvar:ip.allowed=1"
SecRule REQUEST_FILENAME "^/ip/blacklist$"
"chain,phase:1,t:none,deny,nolog,status:200"
SecRule REMOTE_ADDR "^127.0.0.1$" "chain,t:none"
SecRule ARGS:ip "^\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b"
"t:none,initcol:ip=%{args.ip},setvar:ip.allowed=2"
SecRule REMOTE_ADDR "^127.0.0.1$"
"phase:1,t:none,allow,nolog,ctl:ruleEngine=off"
SecRule REQUEST_HEADERS:x-forwarded-for "^\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b"
"phase:1,t:none,pass,nolog,capture,setvar:tx.client_ip=%{tx.1}"
SecRule &TX:CLIENT_IP "@eq 0"
"phase:1,t:none,pass,nolog,setvar:tx.client_ip=%{remote_addr}"
SecRule &TX:CLIENT_IP "!@eq 0"
"phase:1,t:none,pass,nolog,initcol:ip=%{tx.client_ip}"
SecRule IP:ALLOWED "@eq 1" "phase:1,t:none,allow,nolog,ctl:ruleEngine=off"
SecRule IP:ALLOWED "@eq 2" "phase:1,t:none,drop,log,logdata:'Dynamic Blacklist'"
The first block of code processes the “remove” request to remove an IP from both whitelist and blacklist. The second line is where the important check is made to ensure that all these requests are honoured only if issued from localhost (127.0.0.1). Then, in the third line, the IP is removed from the collection. The name of the collection is “ip
” and the name of the variable or counter we are using is “allowed
“.
Similarly, the next block processes the request for inclusion in the whitelist and the subsequent one handles the blacklist. All these blocks first initialize the collection “ip
” and then set the value of the variable “allowed
“.
Then, we make a simple check to allow all requests coming from localhost without applying any further rules.
The next section actually initializes the modsecurity
collection. We first extract the IP from the HTTP header
X-Forwarded-For
and store it in TX:CLIENT_IP
. This collection “TX
” belongs to modsecurity
and is normally available.
If X-Forwarded-For
did not contain any IP, then we try to use the value available in the remote_addr
variable.
The next line initializes our collection whose name is “ip
” using the mod_security
operator “initcol” and supplying the current IP address.
In the final block, the first line checks the value of the variable “allowed
“. If it is 1
, then it is a white-listed IP and hence is allowed to continue with the HTTP request without any further checking by turning off the Rule Engine.
The last line checks if it is a blacklisted IP. If so, the connection is dropped and the information is logged.
Here is the example usage of adding an IP to a blacklist, whitelist or removing from both the lists. We use the Linux curl
command to access the URL and we are only interested in the HTTP response code which we can get by using “http_code
” option.
curl -s -o /dev/null -I -w "%{http_code}" localhost/ip/blacklist?ip=0.0.0.1
curl -s -o /dev/null -I -w "%{http_code}" localhost/ip/blacklist?ip=0.0.0.2
curl -s -o /dev/null -I -w "%{http_code}" localhost/ip/blacklist?ip=0.0.0.3
curl -s -o /dev/null -I -w "%{http_code}" localhost/ip/whitelist?ip=0.0.1.1
curl -s -o /dev/null -I -w "%{http_code}" localhost/ip/whitelist?ip=0.0.1.2
curl -s -o /dev/null -I -w "%{http_code}" localhost/ip/whitelist?ip=0.0.1.3
curl -s -o /dev/null -I -w "%{http_code}" localhost/ip/remove?ip=127.0.0.1
The changes made to the whitelist or blacklist persist even after a reboot because we are using persistent collections of mod_security
. It stores this data in the directory specified in the directive SecDataDir. But if we need to maintain that list in a database, then we could have some bash scripts do the job before or after executing the curl
command.
That’s all folks. We can now successfully block any number of IP addresses using mod_security
. We can do that dynamically without having to restart the Apache web server every time.
If you find this post useful, please "share" using the social buttons.