Symfony & Fail2Ban IP blocking
Fail2ban is a great service that can help prevent brute force attacks by automatically altering your firewall to block certain IP addresses found in log files you specify in rules. Typically this may be used to block IP addresses trying to brute force SSH logins or repeatedly trying to login to email accounts. We can however also make use of this service in our Symfony projects.
In previous articles i've looked at logging logins to a database with Symfony and blocking access to an IP with Symyfony itself at the application level. This tutorial goes one step further and locks the IP address out from accessing the server at all.
In this example we're going to allow a user a maximum of 3 retries at logging into our site before adding them to the fail2ban block list which will lock them out from accessing the site entirely for 600 seconds (10 minutes)
Our first task is to create an authentication failure handler class which extends DefaultAuthenticationFailureHandler. There's only two methods we need to implement, a constructor that will take in a logger object and a request object, and the onAuthenticationFailure method. In this method all that we're going to do is get the users IP address and write it to our log file as Symfony doesn't log this by default.
# src/AppBundle/EventListener/AuthenticationListener.php
namespace AppBundle\EventListener;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Class AuthenticationListener
*/
class AuthenticationListener
{
/**
* @var LoggerInterface
*/
private $logger;
/**
* @var RequestStack
*/
private $request;
/**
* @param LoggerInterface $logger
* @param RequestStack $request
*/
public function __construct(LoggerInterface $logger, RequestStack $request)
{
$this->logger = $logger;
$this->request = $request;
}
/**
* onAuthenticationFailure
*/
public function onAuthenticationFailure()
{
$ipAddress = $this->request->getCurrentRequest()->getClientIp();
$this->logger->error('Authentication failed for IP: ' . $ipAddress);
}
}
Now that our class is complete we need to set this up as a Symfony service which is done in the services.yml file. We ensure our logger and the request stack are passed to our class as arguments. We then tag it is an kernel event listener on the security.authentication.failure event.
#services.yml
services:
app.security.authentication_event_listener:
class: AppBundle\EventListener\AuthenticationListener
arguments: ["@logger","@request_stack"]
tags:
- { name: kernel.event_listener, event: security.authentication.failure, method: onAuthenticationFailure }
We create a new filter file for fail2ban (on CentOS atleast the location of this should be in the /etc/fail2ban/filter.d/ folder, this may potentially vary on different Linux distrubtions) and set the fail regular expression to look for the string of text we use in our log error message.
#/etc/fail2ban/filter.d/symfony.conf
[Definition]
failregex = Authentication failed for IP: <HOST>
We edit our jail.local file to include our new rule. You'll need to change the logpath to the location of your Symfony prod.log file. You can also play around with some of the other variables such as the time a user is banned for or the number of retries a user can have before being locked out (i.e the number of times the IP address is found in the log with our authentication failure log message).
#/etc/fail2ban/jail.local
[symfony]
enabled = true
filter = symfony
logpath = /var/www/app/logs/prod.log
port = http,https
bantime = 600
banaction = iptables-multiport
maxretry = 3
All that remains to do is to restart fail2ban and you should then be able to test it all works.
systemctl restart fail2ban
The above uses the standard prod.log (and dev.log) as a demonstration, you may wish to extend the code above further so that authentication failures are written to a seperate log file (and update the jail.local file appropriately).