Fail2ban is a daemon that scans your server log files for signs of suspicious activity, and takes action when a configured threshold is reached. A common use case is to block potentially malicious clients by IP after x failed login attempts to prevent brute force password guessing attacks.
The only thing you need to do in order to use this for protecting your Rails App is to log failed login attempts and provide a pattern to match the logfile against. Turns out that with Devise, the widely used authentication engine for Rails, that’s a surprisingly hard thing to do.
Look at Devise’s session controller and you see what I mean:
So where’s the code path for the invalid credentials case? It’s simply not there! It
all happens inside Warden, the General Rack
Authentication Framework. Warden uses Ruby’s
throw construct to bail out
of the standard control flow in case of authentication failures, so in case a user
supplied a wrong password, we never return to the
create method after calling
Brute Force Approach - Catching the :warden
Now shouldn’t it be possible to build an around filter for that method, wrapping the
original method in a
catch block, and do the logging from there? Yes it is:
What’s interesting about this
catch thing is that after the block you actually do not know
:warden symbol was thrown or not. You just end up there and need to
determine what’s up from the result of the block. That
case construct comes
straight from Warden’s Manager class where it’s used for the same purpose. Quite a
lot of code to get our simple message written.
Better: Use Warden Callbacks
When I built the above I wasn’t aware of this, but Warden provides several
Callbacks. Interesting for our
use case is the
I simply put that into
That’s much better, but I really dislike the
if statement that is necessary to
determine if there was an actual sign in failure, or if we just have an unauthorized request that’s redirected to the sign in page.
Unfortunately Warden does not differentiate between these two cases, they are simply both considered failures, and the callback is called no matter what. A callback that would only be triggered on failed sign in attempts would simplify things further, but from looking at Warden’s code it doesn’t seem to be very easy to add.
Finally, the Fail2ban Matcher
With our log message in place the rest is fairly easy. Put this into
The pattern takes into account any number of Rails’ log tags that might precede the message and stops just before the timestamp (which is consumed by Fail2ban).
Add a corresponding section to
/etc/fail2ban/jail.local and you’re done:
Obviously this log-file based approach isn’t very useful for a clustered application - in this case you should try to achieve something similar at the load balancer level. You could for example set some HTTP header on the response and log that for Fail2ban, or use it directly to throttle / refuse offending clients, i.e. with Haproxy.