Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / Apache

Block IP using ModSecurity

5.00/5 (1 vote)
9 Apr 2013CPOL5 min read 52.1K  
Block any number of IP addresses using mod_security. We can do that dynamically without having to restart the Apache web server every time.

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.

Bash
# Blocks the specified IP
iptables -A INPUT -s 0.0.0.1 -j DROP

# Blocks the specified IP range
iptables -A INPUT -m iprange --src-range 0.0.0.0-0.0.0.255 -j DROP

# Same as above, but using CIDR notaion
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.

Bash
#
# Adding / Removing an ip to Dynamic Whitelist & Blacklist - allowed only for localhost
#

# Remove from Dynamic Whitelist & Blacklist - remove allowed variable
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"
#
# Add to Dynamic Whitelist - allowed value is 1
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"
#
# Add to Dynamic Blacklist - allowed value is 2
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"



# Allow any request from localhost
SecRule REMOTE_ADDR "^127.0.0.1$" 
"phase:1,t:none,allow,nolog,ctl:ruleEngine=off"



#
# Initialize IP Collection using the IP address obtained from x-forwarded-for or remote_addr
#
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}"



#
# Process Dynamic Whitelist & Blacklist
#
# Allow if IP is present in Dynamic Whitelist
SecRule IP:ALLOWED "@eq 1" "phase:1,t:none,allow,nolog,ctl:ruleEngine=off"
#
# Drop if IP is present in Dynamic Blacklist
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.

Bash
# Blacklist few IPs
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

# Whitelist few IPs
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

# Remove IP from both the lists if present
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)