Fail2ban is your friend

Fail2ban is your friend

January 15, 2016 8 minutes reading time Development

For over 20 years, I’ve been running public servers on the web. Except for the first year, the operating systems of the servers have always been some Linux variants (Linux is 25 years old right now). Currently, my server runs Ubuntu 12.04 LTS with Plesk as a server administration software. It’s a basic virtual server hosted by Host Europe.

As soon as I started to look at the log files, I realized that there’s a lot going on that I would not classify as “intended use” of my server. It’s a little like watching the security camera monitoring the front door of your house and seeing a steady stream of people trying to get in - sometimes by just pressing the door handle, sometimes by trying to run a giant battering ram into it. Some of them seem just confused, nevertheless, their strategies might be successful sometimes: starting at the left edge of the house, they run into the wall, turning around and trying the same one millimeter to the right, until they finally reach the right edge of the house.

Scans log files and bans malicious IPs

A while ago, I decided to do something about it and installed and configured Fail2ban on my server. The areas that I want to protect are logins via SSH, logins for sending and fetching email, FTP, DNS queries, and logins to Plesk and WordPress. There’s never a guaranteed 100% protection, but reducing server load, log file noise, and risk is worth a try.

Use a guide to learn how to harden your Ubuntu 12.04 LTS server, and read this post to learn how to install and do some basic configuration for Fail2ban. The rest of this post explains how I configured Fail2ban on my server.

Default values

The configuration of fail2ban is done in two separate places: the file /etc/fail2ban/jail.local holds it all together, defines default values, and references other configuration files (one for each scenario), while these individual configuration files are located within the directory /etc/fail2ban/filter.d/.

My configuration uses log entries from the last findtime seconds (3 hours here) to determine if there were at least maxretry failed attempts. In this case, the offending IP address is banned for bantime seconds (3 hours in my case) What is and what isn’t a failed attempt is defined in the *.conf files as a Regular Expression listed under filter for each section.

The list of values under the ignoreip entry (separated by blanks) are the IP addresses or subnetworks that I want to exclude from all the banning, because I don’t want to lock myself out: the local subnetwork is therefore excluded, as is the subnetwork of my own DSL provider (the second address). The last address is the public IP address of the server itself. All IP addresses are only examples.

[DEFAULT]
ignoreip = 127.0.0.1/8 123.45.0.0/15 67.89.12.34
bantime  = 10800  ; 3 hours
findtime  = 10800  ; 3 hours
maxretry = 3

For the following list, the first block is the section in /etc/fail2ban/jail.local that describes the scenario. The section name, e.g. ssh, will be used as the name of a jail. A jail is a container for all offending IP addresses, that are banned at any given time. The value for filter is a reference to a configuration file, e.g. filter = sshd is a reference to the configuration file /etc/fail2ban/filter.d/sshd.conf. And the content of the specific file is then presented after the configuration section of the file /etc/fail2ban/jail.local.

Before you configure a jail it’s always a good idea to test it. Fail2ban has a method to do just that. For example, if you want to test the jail in section [named], you can find the referenced log file and the configuration file and test this combination with the command fail2ban-regex -v /var/log/daemon.log /etc/fail2ban/filter.d/named-refused.conf. It will output a list of matches for this jail and some other information. All matches for the regular expressions are shown, regardless of the time they occurred.

SSH access

This is probably the service that is attacked the most, often by trying to guess the password for user root. It’s always a good idea to disable SSH login by this user.

[ssh]
enabled  = true
filter   = sshd
action   = iptables[name=ssh, port=ssh, protocol=tcp]
logpath  = /var/log/auth.log

Specific configuration file /etc/fail2ban/filter.d/sshd.conf looks like this:

[INCLUDES]
before = common.conf
[Definition]
_daemon = sshd
failregex = ^%(__prefix_line)s(?:error: PAM: )?[aA]uthentication (?:failure|error) for .* from <HOST>( via \S+)?\s*$
            ^%(__prefix_line)s(?:error: PAM: )?User not known to the underlying authentication module for .* from <HOST>\s*$
            ^%(__prefix_line)sFailed \S+ for .*? from <HOST>(?: port \d*)?(?: ssh\d*)?(: (ruser .*|(\S+ ID \S+ \(serial \d+\) CA )?\S+ %(__md5hex)s(, client user ".*", client host ".*")?))?\s*$
            ^%(__prefix_line)sROOT LOGIN REFUSED.* FROM <HOST>\s*$
            ^%(__prefix_line)s[iI](?:llegal|nvalid) user .* from <HOST>\s*$
            ^%(__prefix_line)sUser .+ from <HOST> not allowed because not listed in AllowUsers\s*$
            ^%(__prefix_line)sUser .+ from <HOST> not allowed because listed in DenyUsers\s*$
            ^%(__prefix_line)sUser .+ from <HOST> not allowed because not in any group\s*$
            ^%(__prefix_line)srefused connect from \S+ \(<HOST>\)\s*$
            ^%(__prefix_line)sReceived disconnect from <HOST>: 3: \S+: Auth fail$
            ^%(__prefix_line)sUser .+ from <HOST> not allowed because a group is listed in DenyGroups\s*$
            ^%(__prefix_line)sUser .+ from <HOST> not allowed because none of user's groups are listed in AllowGroups\s*$
            ^(?P<__prefix>%(__prefix_line)s)User .+ not allowed because account is locked<SKIPLINES>(?P=__prefix)(?:error: )?Received disconnect from <HOST>: 11: .+ \[preauth\]$
            ^(?P<__prefix>%(__prefix_line)s)Disconnecting: Too many authentication failures for .+? \[preauth\]<SKIPLINES>(?P=__prefix)(?:error: )?Connection closed by <HOST> \[preauth\]$
            ^(?P<__prefix>%(__prefix_line)s)Connection from <HOST> port \d+(?: on \S+ port \d+)?<SKIPLINES>(?P=__prefix)Disconnecting: Too many authentication failures for .+? \[preauth\]$
            ^%(__prefix_line)spam_unix\(sshd:auth\):\s+authentication failure;\s*logname=\S*\s*uid=\d*\s*euid=\d*\s*tty=\S*\s*ruser=\S*\s*rhost=<HOST>\s.*$
ignoreregex = 
[Init]
maxlines = 10
journalmatch = _SYSTEMD_UNIT=sshd.service + _COMM=sshd
# Authors: Cyril Jaquier, Yaroslav Halchenko, Petr Voralek, Daniel Black

Simple Authentication and Security Layer (SASL)

[sasl]
enabled  = true
port     = smtp,ssmtp,submission,imap2,imap3,imaps,pop3,pop3s
filter   = postfix-sasl
action   = iptables[name=sasl, port=smtp, protocol=tcp]
logpath  = /var/log/maillog

Specific configuration file /etc/fail2ban/filter.d/postfix-sasl.conf looks like this:

[INCLUDES]
before = common.conf
[Definition]
_daemon = postfix/(submission/)?smtp(d|s)
failregex = ^%(__prefix_line)swarning: [-._\w]+\[<HOST>\]: SASL ((?i)LOGIN|PLAIN|(?:CRAM|DIGEST)-MD5) authentication failed(: [ A-Za-z0-9+/:]*={0,2})?\s*$
ignoreregex = authentication failed: Connection lost to authentication server$
[Init]
journalmatch = _SYSTEMD_UNIT=postfix.service
# Author: Yaroslav Halchenko

File Transfer via FTP

[proftp]
enabled  = true
filter   = proftpd
action   = iptables[name=proftp, port=ftp, protocol=tcp]
logpath  = /var/log/auth.log

Specific configuration file /etc/fail2ban/filter.d/proftpd.conf looks like this:

[INCLUDES]
before = common.conf
[Definition]
_daemon = proftpd
__suffix_failed_login = (User not authorized for login|No such user found|Incorrect password|Password expired|Account disabled|Invalid shell: '\S+'|User in \S+|Limit (access|configuration) denies login|Not a UserAlias|maximum login length exceeded).?
failregex = ^%(__prefix_line)s%(__hostname)s \(\S+\[<HOST>\]\)[: -]+ USER .*: no such user found from \S+ \[\S+\] to \S+:\S+ *$
            ^%(__prefix_line)s%(__hostname)s \(\S+\[<HOST>\]\)[: -]+ USER .* \(Login failed\): %(__suffix_failed_login)s\s*$
            ^%(__prefix_line)s%(__hostname)s \(\S+\[<HOST>\]\)[: -]+ SECURITY VIOLATION: .* login attempted\. *$
            ^%(__prefix_line)s%(__hostname)s \(\S+\[<HOST>\]\)[: -]+ Maximum login attempts \(\d+\) exceeded *$
ignoreregex = 
# Authors: Yaroslav Halchenko, Daniel Black

Fetching email

[courier]
enabled  = true
filter   = courier-login
action   = iptables-multiport[name=courier, port="110,995,143,993", protocol=tcp]
logpath  = /var/log/maillog

Specific configuration file /etc/fail2ban/filter.d/courier-login.conf looks like this:

[INCLUDES]
before = common.conf
[Definition]
_daemon = (?:courier)?(?:imapd?|pop3d?)(?:login)?(?:-ssl)?
failregex = courier-[A-z0-9]+: LOGIN FAILED, user=.*, ip=\[<HOST>\]$
ignoreregex = 
# Author: Unknown

DNS

[named]
enabled  = true
filter   = named-refused
action   = iptables-multiport[name=named, port="53,953", protocol=tcp]
logpath  = /var/log/daemon.log

Specific configuration file /etc/fail2ban/filter.d/named-refused.conf looks like this:

[Definition]
_daemon=named
__pid_re=(?:\[\d+\])
__daemon_re=\(?%(_daemon)s(?:\(\S+\))?\)?:?
__daemon_combs_re=(?:%(__pid_re)s?:\s+%(__daemon_re)s|%(__daemon_re)s%(__pid_re)s?:)
__line_prefix=(?:\s\S+ %(__daemon_combs_re)s\s+)?
failregex = ^%(__line_prefix)s( error:)?\s*client <HOST>#\S+( \([\S.]+\))?: (view (internal|external): )?query(?: \(cache\))? '.*' denied\s*$
            ^%(__line_prefix)s( error:)?\s*client <HOST>#\S+( \([\S.]+\))?: zone transfer '\S+/AXFR/\w+' denied\s*$
            ^%(__line_prefix)s( error:)?\s*client <HOST>#\S+( \([\S.]+\))?: bad zone transfer request: '\S+/IN': non-authoritative zone \(NOTAUTH\)\s*$
\(NOTAUTH\)\s*$
ignoreregex =
# Author: Yaroslav Halchenko

Plesk access

[plesk]
enabled  = true
filter   = plesk-login
action   = iptables-multiport[name=plesk, port="80,443,8443", protocol=tcp]
logpath  = /var/log/plesk/panel.log

Specific configuration file /etc/fail2ban/filter.d/plesk-login.conf looks like this:

[INCLUDES]
before = common.conf
[Definition]
failregex = Failed login attempt with login '.*' from IP <HOST>$
ignoreregex = 
# Author: Thomas Weitzel

WordPress administration

As a prerequisite, I’ve installed a plugin on all of my WordPress sites that helps prevent brute force attacks with Fail2ban & WordPress. This plugin makes sure that failed login attempts are easily found within the log files. Additionally, login into WordPress should only be possible via HTTPS. Adding the following lines to your wp-config.php will make sure that nobody can log in without using SSL:

// Force SSL login for administrator
define('FORCE_SSL_ADMIN', true);

One little remark regarding the use of SSL certificates: since the end of 2015 I use free Let’s Encrypt certificates. Highly recommended. These certificates are trusted by most browsers.

[wordpress]
enabled  = true
port     = http,https
filter   = wordpress-auth
action   = iptables[name=wpauth, port=https, protocol=tcp]
logpath  = /var/www/vhosts/domain1.org/logs/access_ssl_log
           /var/www/vhosts/domain2.org/logs/access_ssl_log
           /var/www/vhosts/domain3.org/logs/access_ssl_log

Specific configuration file /etc/fail2ban/filter.d/wordpress-auth.conf looks like this:

[INCLUDES]
before = common.conf
[Definition]
failregex = ^<HOST>\s.*POST.*(wp-login\.php|xmlrpc\.php).* 401
ignoreregex = 
# Author: Tim Nash

Handling persistent disruptors

This is a special case: Fail2ban can monitor its own log file. This feature can be used to find repeating offenders and treat them in a unique way. In my case, if an IP address gets banned for a second time within a week (remember, I ban it only for 3 hours the first time), it gets banned for an entire week.

[recidive]
enabled  = true
filter   = recidive
action   = iptables-allports[name=recidive]
logpath  = /var/log/fail2ban.log
bantime  = 604800  ; 1 week
findtime = 604800  ; 1 week
maxretry = 2

Specific configuration file /etc/fail2ban/filter.d/recidive.conf looks like this:

[INCLUDES]
before = common.conf
[Definition]
_daemon = fail2ban\.actions\s*
_jailname = recidive
failregex = ^(%(__prefix_line)s| %(_daemon)s%(__pid_re)s?:\s+)NOTICE\s+\[(?!%(_jailname)s\])(?:.*)\]\s+Ban\s+<HOST>\s*$
ignoreregex = 
[Init]
journalmatch = _SYSTEMD_UNIT=fail2ban.service PRIORITY=5
# Authors: Tom Hendrikx, Amir Caspi 

Conclusion

While Fail2ban is not the highest hurdle to take, it handles at least most of the script kiddies very well. And these are making up the majority of my log messages. It is one of a series of steps that you should take to make your own server a little more secure.

The contents of the individual configuration files have accumulated on my server over time. They are nearly entirely the work of others, and I’ve just used their work. If you are one of the original authors: Thank you!