---
author:
    email: mail@petermolnar.net
    image: https://petermolnar.net/favicon.jpg
    name: Peter Molnar
    url: https://petermolnar.net
copies:
- http://web.archive.org/web/20150524052355/https://petermolnar.eu/linux-tech-coding/secure-wordpress-with-nginx-and-fail2ban/
lang: en
published: '2015-04-07T14:40:57+00:00'
summary: WPScan with Metasploit can easily hack a WordPress site - unless you automatically
    block their access to the PHP level.
tags:
- WordPress
title: How to make WordPress secure with nginx and fail2ban

---

## Why

I've recently came across a little presentation on how easy is to hack
WordPress[^1]. I'd known it's pretty easy, but unfortunately there are
tools making this possible for nearly anyone.

To test how problematic my sites are, I've downloaded WPScan[^2] and ran
it against my sites. The result was surprising: The website is
unreachable.

After the initial panic that (*I've killed my own site!*) I realized
that my long forgotten nginx rules in combination with fail2ban banned
the testing IP within the first 2 seconds of the scanning attempt.

You may want to introduce something similar to block scanners on your
WordPress. *( Or ask you hosting provider to do so. )*

I've not written all of these rules myself but unfortunately I cannot
recall all the sources I've used during the years to collect this,
therefore my thanks go to everyone who ever did anything I've read on
this.

**Please bear in mind that security is never bullet-proof - this setup
will not save you from someone really attacking your site. There are
many layers you need to use and always keep an eye out, revisit the
logs, supervise what is happening with your sites.**

## nginx[^3] {#nginx3}

In nginx you need to add a new log format. This will contain all the
blocked requests, including the IP address; **this should go into the
http {} block.**

`/etc/nginx/nginx.conf`, `http` section:

``` {.apache}
log_format blocked '$time_local: Blocked request from $remote_addr $request';
```

You'll also need the following rules **in the server {} block** for the
site you're setting the protection for:

``` {.apache}
# note: if you have posts with title matching these, turn them off or fine-tune
# them to exclude those

## Block SQL injections
location ~* union.*select.*\( {
    access_log /var/log/nginx/blocked.log blocked;
    deny all;
}
location ~* union.*all.*select.* {
    access_log /var/log/nginx/blocked.log blocked;
    deny all;
}
location ~* concat.*\( {
    access_log /var/log/nginx/blocked.log blocked;
    deny all;
}

## Block common exploits
location ~* (<|%3C).*script.*(>|%3E) {
    access_log /var/log/nginx/blocked.log blocked;
    deny all;
}
location ~* base64_(en|de)code\(.*\) {
    access_log /var/log/nginx/blocked.log blocked;
    deny all;
}
location ~* (%24&x) {
    access_log /var/log/nginx/blocked.log blocked;
    deny all;
}
location ~* (%0|%A|%B|%C|%D|%E|%F|127\.0) {
    access_log /var/log/nginx/blocked.log blocked;
    deny all;
}
location ~* \.\.\/  {
    access_log /var/log/nginx/blocked.log blocked;
    deny all;
}
location ~* ~$ {
    access_log /var/log/nginx/blocked.log blocked;
    deny all;
}
location ~* proc/self/environ {
    access_log /var/log/nginx/blocked.log blocked;
    deny all;
}
location ~* /\.(htaccess|htpasswd|svn) { log_not_found off;
    access_log /var/log/nginx/blocked.log blocked;
    deny all;
}

## Block file injections
location ~* [a-zA-Z0-9_]=(\.\.//?)+ {
    access_log /var/log/nginx/blocked.log blocked;
    deny all;
}
location ~* [a-zA-Z0-9_]=/([a-z0-9_.]//?)+ {
    access_log /var/log/nginx/blocked.log blocked;
    deny all;
}

## Block access to internal WordPress assets that isn't queried under normal
## circumstances
location ~* wp-config.php {
    access_log /var/log/nginx/blocked.log blocked;
    deny all;
}
location ~* wp-admin/includes {
    access_log /var/log/nginx/blocked.log blocked;
    deny all;
}
location ~* wp-app\.log {
    access_log /var/log/nginx/blocked.log blocked;
    deny all;
}
location ~* (licence|readme|license)\.(html|txt) {
    access_log /var/log/nginx/blocked.log blocked;
    deny all;
}

## In case you have anything using sqlite as database you probably want to block
## direct access to those as well
location ~* \.sqlite$ {
    access_log /var/log/nginx/blocked.log blocked;
    deny all;
}
location ~* ^/(SQLite|sqlite) {
    access_log /var/log/nginx/blocked.log blocked;
    deny all;
}
location ~* \.sqlite-journal$ {
    access_log /var/log/nginx/blocked.log blocked;
    deny all;
}
```

## fail2ban[^4] {#fail2ban4}

Add this jail to the jails config:

`/etc/fail2ban/jail.conf`

``` {.ini}
[nginx-blocked]
enabled = true
port = 80,443
filter = nginx-blocked
logpath = /var/log/nginx/blocked.log
bantime  = 3600
maxretry = 3
backend = auto
findtime = 86400
banaction = iptables-multiport
protocol = tcp
chain = INPUT
```

Note: the logpath accepts \* as wildcard, in case you have more blocked
logs.

And also add the filter

`/etc/fail2ban/filter.d/nginx-blocked.conf`

``` {.ini}
[Definition]
failregex = ^.* Blocked request from <HOST>.*$
ignoreregex =
```

For the `nat banaction`, please see fail2ban for NAT hosts[^5].

### Optional: syslog

In case you have more than one webserver, you probably want to
centralize this. The easiest way is to have fail2ban on the
loadbalancer, nginx logging into syslog, syslog pushed to the
loadbalancer's syslog. **Rsyslog is a deep topic, so please keep in mind
that you'll most probably need to read more about it. This is an example
for those who are familiar with syslog.** You can replace the
`access_log` lines in the collection above with something similar to:

    access_log syslog:server=unix:/dev/log,facility=local7,tag=nginx,severity=warn blocked;

in the nginx.conf and:

    local7.warn /var/log/nginx/blocked.log

in you rsyslog.conf.

## Troubleshooting

aaronpk[^6] started to use this setup, but had some troubles verifying
it's running and happy.

**Note: these all should be run as root.**

### Check if fail2ban is running

`ps aux | grep fail2ban` should show something like
`/usr/bin/python /usr/bin/fail2ban-server` (or similar).

### `bantime` and `findtime`

In my example, bantime is set for an hour and findtime for a day. You
may need to tune this according to your needs and traffic.

### verify your regex

If you run `fail2ban-regex -v [path-to-log] [path-to-filter-rule]` it
should output something like this:

    Running tests
    =============

    Use   failregex file : /etc/fail2ban/filter.d/nginx-blocked.conf
    Use         log file : /var/log/nginx.blocked.log


    Results
    =======

    Failregex: 1452 total
    |-  #) [# of hits] regular expression
    |   1) [1452] ^.*?Blocked request from <HOST> .*$
    |      51.255.65.41  Sun Feb 19 06:31:05 2017

    [ ... lots of IPs here ...]

    |      157.55.39.20  Thu Feb 23 12:30:23 2017
    `-

    Ignoreregex: 0 total

    Date template hits:
    |- [# of hits] date format
    |  [2125] ISO 8601
    |  [2] Year/Month/Day Hour:Minute:Second
    |  [0] WEEKDAY MONTH Day Hour:Minute:Second[.subsecond] Year
    |  [0] WEEKDAY MONTH Day Hour:Minute:Second Year
    |  [0] WEEKDAY MONTH Day Hour:Minute:Second
    |  [0] MONTH Day Hour:Minute:Second
    |  [0] Day/Month/Year Hour:Minute:Second
    |  [0] Day/Month/Year2 Hour:Minute:Second
    |  [0] Day/MONTH/Year:Hour:Minute:Second
    |  [0] Month/Day/Year:Hour:Minute:Second
    |  [0] Year-Month-Day Hour:Minute:Second[,subsecond]
    |  [0] Year-Month-Day Hour:Minute:Second
    |  [0] Year.Month.Day Hour:Minute:Second
    |  [0] Day-MONTH-Year Hour:Minute:Second[.Millisecond]
    |  [0] Day-Month-Year Hour:Minute:Second
    |  [0] Month-Day-Year Hour:Minute:Second[.Millisecond]
    |  [0] TAI64N
    |  [0] Epoch
    |  [0] Hour:Minute:Second
    |  [0] <Month/Day/Year@Hour:Minute:Second>
    |  [0] YearMonthDay Hour:Minute:Second
    |  [0] Month-Day-Year Hour:Minute:Second
    `-

    Lines: 2127 lines, 0 ignored, 1452 matched, 675 missed

If not, the regex is not working.

### Query fail2ban

Fail2ban offers a program names `fail2ban-client` to ask for status, so:

`fail2ban-client status nginx-blocked`

    Status for the jail: nginx-blocked
    |- filter
    |  |- File list:    /var/rlog/mekare.petermolnar.eu/nginx.blocked.log
    |  |- Currently failed: 68
    |  `- Total failed: 586
    `- action
       |- Currently banned: 6
       |  `- IP list:   122.167.147.40 176.93.112.88 193.154.116.218 203.118.160.75 79.122.16.39 157.55.39.20
       `- Total banned: 139

If this is returning some error, it's either the client not being able
to talk to the server (check the socket, maybe add `-s [socketpath]` to
the client), or the service/jail is not running'.

### Dump iptables

When you're certain an IP should have been or is blocked, you can also
check iptables with `iptables-save`.

[^1]: <http://www-personal.umich.edu/~markmont/awp/>

[^2]: <http://wpscan.org/>

[^3]: <http://nginx.org/>

[^4]: <http://www.fail2ban.org/wiki/index.php/Main_Page>

[^5]: <https://petermolnar.net/linux-tech-coding/fail2ban-nat-hosts/>

[^6]: <http://aaronparecki.com/>