In this tutorial, we are going to learn how to restrict access to WordPress login page to specific IPs with libModSecurity. libModSecurity is also known as ModSecurity version 3.0.
ModSecurity is an open source, cross-platform web application firewall (WAF) module developed by Trustwave’s SpiderLabs. Known as the “Swiss Army Knife” of WAFs, it enables web application defenders to gain visibility into HTTP(S) traffic and provides a power rules language and API to implement advanced protections.
It has a robust event-based programming language which provides protection from a range of attacks against web applications and allows for HTTP traffic monitoring, logging and real-time analysis.
What Can ModSecurity Do?
- Real-time application security monitoring and access control
- Full HTTP traffic logging
- Continuous passive security assessment
- Web application hardening
- Due to its ability to parse XML and apply XPath expressions with its ability to proxy requests, it can be used as an XML web service router.
- HTTP Protocol Protection
- Real-time Blacklist Lookups
- HTTP Denial of Service Protections
- Generic Web Attack Protection
- Error Detection and Hiding
WordPress login pages are always under brute force attacks with the malicious actors always up-to try any combination of multiple authentication credentials against your WordPress with the aim of getting access to it.
There a number of methods which you can use to protect your WordPress login page against these attacks including;
- Enabling basic web server http authentication.
- Restricting access to WordPress directories from specific IP addresses on the web server configurations
- Using secure plugins that masks the WordPress authentication URLs
- Using non-common usernames such as admin, for any user account
- Using complex and unique, non-dictionary passwords for logins.
All these methods are fine. However, you can as well be able to use ModSecurity to protect your WordPress Login pages against these attacks, and that is what this tutorial is all about.
Well, you can protect something that doesn’t exist. Therefore, you need to be having a WordPress site up and running. You can follow the link below to learn how to install and setup WordPress site with Nginx and MySQL 8 on CentOS 8 system.
In our demo, we have our WordPress running on a CentOS 8. We have also covered how to install and setup LibModSecurity with Apache or Nginx web server in our previous tutorials. Depending on the type of web server your site is hosted on, follow any of the links below to install WordPress.
Now that you have ModSecurity WAF in place, let us proceed configure it to restrict access to WordPress login page from specific IP addresses.
We have already installed the OWASP ModSecurity Core Rule Set (CRS) while setting up ModSecurity, which provides a set of generic attack detection rules.
We specified the location of these rules in our ModSecurity rules file,
Include "/etc/httpd/conf.d/modsecurity.d/modsecurity.conf" Include "/etc/httpd/conf.d/modsecurity.d/owasp-crs/crs-setup.conf" Include "/etc/httpd/conf.d/modsecurity.d/owasp-crs/rules/*.conf"
Create a custom rules configuration file;
Create a Rule to Allow Access to WordPress login page, (URI, wp-login.php) from Specific IP addresses.
ModSecurity rules are defined using the
SecRule directive. SecRule is made up of 4 parts:
- Variables – Instructs ModSecurity where to look (sometimes called Targets).
- Operators – Instructs ModSecurity when to trigger a match.
- Transformations – Instructs ModSecurity how it should normalize variable data .
- Actions – Instructs ModSecurity what to do if a rule matches
The Syntax of the rule is;
SecRule VARIABLES "OPERATOR" "TRANSFORMATIONS,ACTIONS"
Read more about this on Making ModSecurity rules.
For the configuration directives, check Reference Manual.
Therefore, our rule looks like;
SecRule REQUEST_URI "@eq /wp-login.php" \ "chain, \ id:'2000', \ phase:1, \ log, \ pass" SecRule REMOTE_ADDR "@ipMatch 192.168.57.1,192.168.57.21" \ ctl:ruleEngine=DetectionOnly
Dissecting the rule into parts;
- REQUEST_URI: This variable holds the full request URL including the query string dataREQUEST_URI without the domain part. For example, the
- REMOTE_ADDR: This variable holds the IP address of the remote client. If Apache directive HostnameLookups is set to On, then you can use REMOTE_HOST variable which holds the remote hostname resolved through DNS.
- @eq: Performs numerical comparison and returns true if the input value is equal to the provided parameter
- @ipMatch: Performs a fast ipv4 or ipv6 match of REMOTE_ADDR variable data. Multiple addresses can be separated with commas.
- chain: Chains the current rule with the rule that immediately follows it, creating a rule chain. Rule chains allow you to simulate logical AND. The disruptive actions specified in the first portion of the chained rule will be triggered only if all of the variable checks return positive hits.
- id: Assigns a unique ID to the rule or chain in which it appears. The value must be numeric. 1–99,999: reserved for local (internal) use.
- phase: Places the rule or chain into one of five available processing phases.
- Request headers (phase 1): Analyzes the request headers first.
- Request body (phase 2): The request body phase is the main request analysis phase and takes place immediately after a complete request body has been received and processed.
- Response headers (phase 3): The response headers phase takes place after response headers become available, but before a response body is read.
- Response body (phase 4): The main response analysis phase.
- Logging (phase 5): The only phase you can’t block.
- log: Indicates that a successful match of the rule needs to be logged.
- pass: Continues processing with the next rule in spite of a successful match.
- ctl: Changes ModSecurity configuration on transient, per-transaction basis. Any changes made using this action will affect only the transaction in which the action is executed. ruleEngine=DetectionOnly, changes the Operating mode for ModSecurity for this specific rule.
So basically the rule takes each HTTP request and extract the URI portion equalling to
wp-login.php, it assigns the rule a unique ID of 2001, places the rule on request header phase, logs the rule in-case of a successful match, chains the rule with the next one, continues to process the next rule which checks if the request IP address matches the specified ones, and if true, put ModSecurity in detection only, permissive, mode.
Create a Rule to Deny Any Access to WordPress login page
Next, you need to block all other access to WordPress login page. Hence, create a rule that denies access. Place the rule in the same file above, just after the above rule;
SecRule REQUEST_URI "@contains wp-login" "id:2001,phase:1,t:lowercase,log,deny,msg:'Warning, Access to WordPress Login page is Restricted'"
Our general rule now looks like;
SecRule REQUEST_URI "@eq /wp-login.php" \ "chain, \ id:'2000', \ phase:1, \ log, \ pass" SecRule REMOTE_ADDR "@ipMatch 192.168.57.1,192.168.57.21" \ ctl:ruleEngine=DetectionOnly SecRule REQUEST_URI "@contains wp-login" "id:2001,phase:1,t:lowercase,log,deny,msg:'Warning, Access to WordPress Login page is Restricted'"
Configure ModSecurity to Process Custom Rules
Now that we have the rule ready, configure ModSecurity to process this rule.
In our setup,
/etc/httpd/conf.d/modsecurity.d/rules.conf is our main ModSecurity rules configuration file.
Hence, we need to include our custom rule configuration file, in the main rules file.
echo 'Include "/etc/httpd/conf.d/modsecurity.d/custom.conf"' >> /etc/httpd/conf.d/modsecurity.d/rules.conf
Our main rules configuration file looks like in below now;
Include "/etc/httpd/conf.d/modsecurity.d/modsecurity.conf" Include "/etc/httpd/conf.d/modsecurity.d/owasp-crs/crs-setup.conf" Include "/etc/httpd/conf.d/modsecurity.d/owasp-crs/rules/*.conf" Include "/etc/httpd/conf.d/modsecurity.d/custom.conf"
To effect the changes, restart Apache;
systemctl restart httpd
Testing Access to WordPress Login
To verify if our rule works, simply try to access WordPress from a source, whose IP address is not part of the whitelisted addresses above.
In our case, we are testing this from a system whose IP address is,
If all is well, on the browser, you will get such an error;
On the logs, you should see such a log entry;
tail -f /var/log/httpd/modsec_audit.log
... ---pnERTMML---A-- [11/Jul/2020:15:33:58 +0300] 159447083849.637890 192.168.57.22 48084 wp.kifarunix-demo.com 0 ---pnERTMML---B-- GET /wp-login.php?redirect_to=http%3A%2F%2Fwp.kifarunix-demo.com%2Fwp-admin%2F&reauth=1 HTTP/1.1 Host: wp.kifarunix-demo.com User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0 Cookie: wordpress_test_cookie=WP+Cookie+check Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: keep-alive Upgrade-Insecure-Requests: 1 ---pnERTMML---D-- ---pnERTMML---F-- HTTP/1.1 403 ---pnERTMML---H-- ModSecurity: Access denied with code 403 (phase 1). Matched "Operator `Contains' with parameter `wp-login' against variable `REQUEST_URI' (Value: `/wp-login.php?redirect_to=http://wp.kifarunix-demo.com/wp-admin/&reauth=1' ) [file "/etc/httpd/conf.d/modsecurity.d/custom.conf"] [line "8"] [id "2001"] [rev ""] [msg "Warning, Access to WordPress Login page is Restricted"] [data ""] [severity "0"] [ver ""] [maturity "0"] [accuracy "0"] [hostname "wp.kifarunix-demo.com"] [uri "/wp-login.php"] [unique_id "159447083849.637890"] [ref "o1,8v4,83t:lowercase"] ...
There are other many ways of protecting WordPress against attacks. Thus blocking access to wp-login.php page helps curb the bruteforce attacks.
That marks the end of our guide on how to restrict access to WordPress login page to specific IPs with libModSecurity. We hope this was informative. Enjoy.