388 lines
20 KiB
HTML
388 lines
20 KiB
HTML
<?xml version="1.0" encoding="utf-8"?>
|
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
|
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
|
|
<head>
|
|
<!-- 2026-02-21 Sat 20:52 -->
|
|
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Failed2ban - A Real Test In The Trenches</title>
|
|
<meta name="author" content="Buckwheat" />
|
|
<meta name="generator" content="Org Mode" />
|
|
<style type="text/css">
|
|
#content { max-width: 60em; margin: auto; }
|
|
.title { text-align: center;
|
|
margin-bottom: .2em; }
|
|
.subtitle { text-align: center;
|
|
font-size: medium;
|
|
font-weight: bold;
|
|
margin-top:0; }
|
|
.todo { font-family: monospace; color: red; }
|
|
.done { font-family: monospace; color: green; }
|
|
.priority { font-family: monospace; color: orange; }
|
|
.tag { background-color: #eee; font-family: monospace;
|
|
padding: 2px; font-size: 80%; font-weight: normal; }
|
|
.timestamp { color: #bebebe; }
|
|
.timestamp-kwd { color: #5f9ea0; }
|
|
.org-right { margin-left: auto; margin-right: 0px; text-align: right; }
|
|
.org-left { margin-left: 0px; margin-right: auto; text-align: left; }
|
|
.org-center { margin-left: auto; margin-right: auto; text-align: center; }
|
|
.underline { text-decoration: underline; }
|
|
#postamble p, #preamble p { font-size: 90%; margin: .2em; }
|
|
p.verse { margin-left: 3%; }
|
|
pre {
|
|
border: 1px solid #e6e6e6;
|
|
border-radius: 3px;
|
|
background-color: #f2f2f2;
|
|
padding: 8pt;
|
|
font-family: monospace;
|
|
overflow: auto;
|
|
margin: 1.2em;
|
|
}
|
|
pre.src {
|
|
position: relative;
|
|
overflow: auto;
|
|
}
|
|
pre.src:before {
|
|
display: none;
|
|
position: absolute;
|
|
top: -8px;
|
|
right: 12px;
|
|
padding: 3px;
|
|
color: #555;
|
|
background-color: #f2f2f299;
|
|
}
|
|
pre.src:hover:before { display: inline; margin-top: 14px;}
|
|
/* Languages per Org manual */
|
|
pre.src-asymptote:before { content: 'Asymptote'; }
|
|
pre.src-awk:before { content: 'Awk'; }
|
|
pre.src-authinfo::before { content: 'Authinfo'; }
|
|
pre.src-C:before { content: 'C'; }
|
|
/* pre.src-C++ doesn't work in CSS */
|
|
pre.src-clojure:before { content: 'Clojure'; }
|
|
pre.src-css:before { content: 'CSS'; }
|
|
pre.src-D:before { content: 'D'; }
|
|
pre.src-ditaa:before { content: 'ditaa'; }
|
|
pre.src-dot:before { content: 'Graphviz'; }
|
|
pre.src-calc:before { content: 'Emacs Calc'; }
|
|
pre.src-emacs-lisp:before { content: 'Emacs Lisp'; }
|
|
pre.src-fortran:before { content: 'Fortran'; }
|
|
pre.src-gnuplot:before { content: 'gnuplot'; }
|
|
pre.src-haskell:before { content: 'Haskell'; }
|
|
pre.src-hledger:before { content: 'hledger'; }
|
|
pre.src-java:before { content: 'Java'; }
|
|
pre.src-js:before { content: 'Javascript'; }
|
|
pre.src-latex:before { content: 'LaTeX'; }
|
|
pre.src-ledger:before { content: 'Ledger'; }
|
|
pre.src-lisp:before { content: 'Lisp'; }
|
|
pre.src-lilypond:before { content: 'Lilypond'; }
|
|
pre.src-lua:before { content: 'Lua'; }
|
|
pre.src-matlab:before { content: 'MATLAB'; }
|
|
pre.src-mscgen:before { content: 'Mscgen'; }
|
|
pre.src-ocaml:before { content: 'Objective Caml'; }
|
|
pre.src-octave:before { content: 'Octave'; }
|
|
pre.src-org:before { content: 'Org mode'; }
|
|
pre.src-oz:before { content: 'OZ'; }
|
|
pre.src-plantuml:before { content: 'Plantuml'; }
|
|
pre.src-processing:before { content: 'Processing.js'; }
|
|
pre.src-python:before { content: 'Python'; }
|
|
pre.src-R:before { content: 'R'; }
|
|
pre.src-ruby:before { content: 'Ruby'; }
|
|
pre.src-sass:before { content: 'Sass'; }
|
|
pre.src-scheme:before { content: 'Scheme'; }
|
|
pre.src-screen:before { content: 'Gnu Screen'; }
|
|
pre.src-sed:before { content: 'Sed'; }
|
|
pre.src-sh:before { content: 'shell'; }
|
|
pre.src-sql:before { content: 'SQL'; }
|
|
pre.src-sqlite:before { content: 'SQLite'; }
|
|
/* additional languages in org.el's org-babel-load-languages alist */
|
|
pre.src-forth:before { content: 'Forth'; }
|
|
pre.src-io:before { content: 'IO'; }
|
|
pre.src-J:before { content: 'J'; }
|
|
pre.src-makefile:before { content: 'Makefile'; }
|
|
pre.src-maxima:before { content: 'Maxima'; }
|
|
pre.src-perl:before { content: 'Perl'; }
|
|
pre.src-picolisp:before { content: 'Pico Lisp'; }
|
|
pre.src-scala:before { content: 'Scala'; }
|
|
pre.src-shell:before { content: 'Shell Script'; }
|
|
pre.src-ebnf2ps:before { content: 'ebfn2ps'; }
|
|
/* additional language identifiers per "defun org-babel-execute"
|
|
in ob-*.el */
|
|
pre.src-cpp:before { content: 'C++'; }
|
|
pre.src-abc:before { content: 'ABC'; }
|
|
pre.src-coq:before { content: 'Coq'; }
|
|
pre.src-groovy:before { content: 'Groovy'; }
|
|
/* additional language identifiers from org-babel-shell-names in
|
|
ob-shell.el: ob-shell is the only babel language using a lambda to put
|
|
the execution function name together. */
|
|
pre.src-bash:before { content: 'bash'; }
|
|
pre.src-csh:before { content: 'csh'; }
|
|
pre.src-ash:before { content: 'ash'; }
|
|
pre.src-dash:before { content: 'dash'; }
|
|
pre.src-ksh:before { content: 'ksh'; }
|
|
pre.src-mksh:before { content: 'mksh'; }
|
|
pre.src-posh:before { content: 'posh'; }
|
|
/* Additional Emacs modes also supported by the LaTeX listings package */
|
|
pre.src-ada:before { content: 'Ada'; }
|
|
pre.src-asm:before { content: 'Assembler'; }
|
|
pre.src-caml:before { content: 'Caml'; }
|
|
pre.src-delphi:before { content: 'Delphi'; }
|
|
pre.src-html:before { content: 'HTML'; }
|
|
pre.src-idl:before { content: 'IDL'; }
|
|
pre.src-mercury:before { content: 'Mercury'; }
|
|
pre.src-metapost:before { content: 'MetaPost'; }
|
|
pre.src-modula-2:before { content: 'Modula-2'; }
|
|
pre.src-pascal:before { content: 'Pascal'; }
|
|
pre.src-ps:before { content: 'PostScript'; }
|
|
pre.src-prolog:before { content: 'Prolog'; }
|
|
pre.src-simula:before { content: 'Simula'; }
|
|
pre.src-tcl:before { content: 'tcl'; }
|
|
pre.src-tex:before { content: 'TeX'; }
|
|
pre.src-plain-tex:before { content: 'Plain TeX'; }
|
|
pre.src-verilog:before { content: 'Verilog'; }
|
|
pre.src-vhdl:before { content: 'VHDL'; }
|
|
pre.src-xml:before { content: 'XML'; }
|
|
pre.src-nxml:before { content: 'XML'; }
|
|
/* add a generic configuration mode; LaTeX export needs an additional
|
|
(add-to-list 'org-latex-listings-langs '(conf " ")) in .emacs */
|
|
pre.src-conf:before { content: 'Configuration File'; }
|
|
|
|
table { border-collapse:collapse; }
|
|
caption.t-above { caption-side: top; }
|
|
caption.t-bottom { caption-side: bottom; }
|
|
td, th { vertical-align:top; }
|
|
th.org-right { text-align: center; }
|
|
th.org-left { text-align: center; }
|
|
th.org-center { text-align: center; }
|
|
td.org-right { text-align: right; }
|
|
td.org-left { text-align: left; }
|
|
td.org-center { text-align: center; }
|
|
dt { font-weight: bold; }
|
|
.footpara { display: inline; }
|
|
.footdef { margin-bottom: 1em; }
|
|
.figure { padding: 1em; }
|
|
.figure p { text-align: center; }
|
|
.equation-container {
|
|
display: table;
|
|
text-align: center;
|
|
width: 100%;
|
|
}
|
|
.equation {
|
|
vertical-align: middle;
|
|
}
|
|
.equation-label {
|
|
display: table-cell;
|
|
text-align: right;
|
|
vertical-align: middle;
|
|
}
|
|
.inlinetask {
|
|
padding: 10px;
|
|
border: 2px solid gray;
|
|
margin: 10px;
|
|
background: #ffffcc;
|
|
}
|
|
#org-div-home-and-up
|
|
{ text-align: right; font-size: 70%; white-space: nowrap; }
|
|
textarea { overflow-x: auto; }
|
|
.linenr { font-size: smaller }
|
|
.code-highlighted { background-color: #ffff00; }
|
|
.org-info-js_info-navigation { border-style: none; }
|
|
#org-info-js_console-label
|
|
{ font-size: 10px; font-weight: bold; white-space: nowrap; }
|
|
.org-info-js_search-highlight
|
|
{ background-color: #ffff00; color: #000000; font-weight: bold; }
|
|
.org-svg { }
|
|
</style>
|
|
<link rel="stylesheet" type="text/css" href="../assets/tokyonight.css" />
|
|
</head>
|
|
<body>
|
|
<div id="content" class="content">
|
|
<div align=center>
|
|
<p>
|
|
<a href="https://buckwheat.neocities.org/blog/posts">Click here to return to blog posts menu</a>
|
|
</p>
|
|
</div>
|
|
|
|
<div id="table-of-contents" role="doc-toc">
|
|
<h2>Table of Contents</h2>
|
|
<div id="text-table-of-contents" role="doc-toc">
|
|
<ul>
|
|
<li><a href="#org5b47158">Intro</a></li>
|
|
<li><a href="#org469da47">The Battlefield</a></li>
|
|
<li><a href="#orge1d45a7">Enter Fail2ban</a></li>
|
|
<li><a href="#orgaf007e6">Failed2ban At All</a></li>
|
|
<li><a href="#orgd5f6c85">Get Blacklisted!</a></li>
|
|
<li><a href="#org8172923">Try It Yourself!</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div align=center>
|
|
|
|
<div id="org89365cc" class="figure">
|
|
<p><img src="./images/failed2ban.png" alt="failed2ban.png" style="height:100%;width:100%;object-fit:contain;" />
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div align=center>
|
|
<div id="outline-container-org5b47158" class="outline-2">
|
|
<h2 id="org5b47158">Intro</h2>
|
|
<div class="outline-text-2" id="text-org5b47158">
|
|
<p>
|
|
Hey! It's been a while since there's been activity, huh? Well, worth first saying (even if almost 2 months late) Happy New Years to all my readers.
|
|
</p>
|
|
|
|
<p>
|
|
I recently became a founding member of a multinational group of FOSS Developers and Advocates called Neo-Sekai Club (more about that in a future post of mine!), and I'm in charge of many, many tasks, System Administration and Network Engineer being two of these tasks. In this blogpost, I go over real boots-on-the-ground experience of trying Fail2ban on production FreeBSD servers used by Neo-Sekai Club to see whether the thing everyone uses is really always the thing that works!
|
|
</p>
|
|
</div>
|
|
|
|
<div align=center>
|
|
</div>
|
|
</div>
|
|
<div id="outline-container-org469da47" class="outline-2">
|
|
<h2 id="org469da47">The Battlefield</h2>
|
|
<div class="outline-text-2" id="text-org469da47">
|
|
<p>
|
|
It goes without saying that the moment your server touches the internet, you're going to start having script kiddies and botnets hammering on your SSH, firing off port scans, and all sorts of other things. This totally sucks, and this is why we use firewalls. At Neo-Sekai Club, we deploy all of our servers using only BSDs. In this on-the-ground test, this is from real experience using a FreeBSD 14.3 server that we host our web services from.
|
|
</p>
|
|
|
|
<p>
|
|
We use Packet Filter (pf) as our firewall, which is a fantastically simple firewall ported over from OpenBSD and is just as much of a breeze to use over on FreeBSD. Packets get scrubbed in on our interface, logged out to the pflog, and if that's not a port we've whitelisted in the firewall, we block it. Pretty simple system to use! However, that's just not enough in a real-world scenario when it comes to SSH bruteforce attempts. If this was a Linux server, here's now where we're getting into using a system like Fail2ban. So… what exactly is Fail2ban?
|
|
</p>
|
|
</div>
|
|
|
|
<div align=center>
|
|
</div>
|
|
</div>
|
|
<div id="outline-container-orge1d45a7" class="outline-2">
|
|
<h2 id="orge1d45a7">Enter Fail2ban</h2>
|
|
<div class="outline-text-2" id="text-orge1d45a7">
|
|
<p>
|
|
Fail2ban is a Python-based security system that integrates into your firewall and provides an IP blacklist based on a number of failed SSH attempts within an alloted timeframe, and then issues bans on IPs who've failed SSH enough times for a period specified by the System Administrator. In the World of Linux, this is a very common anti-bruteforce system that System Administrators use to stop bruteforce attacks in their tracks, and it integrates quite nicely into Linux's firewall system iptables. Here's the thing however, we're not on Linux, and we don't get to use iptables on FreeBSD… so what's it like to use Fail2ban with pf on FreeBSD?
|
|
</p>
|
|
|
|
<p>
|
|
Firstly, Fail2ban uses a jail system, not the same as the jail system provided in FreeBSD, in which IPs can no longer access a service monitored by Fail2ban once their IP is banned. However, there's almost no good information out there to use Fail2ban with pf on FreeBSD! Sure, our SysAdmin Team found setup guides and we followed those to the absolute T, but boy was it a headache for us to do it… Okay, great, now we've got the Fail2ban up and running, added as a service to rc… And nobody's getting blocked. Why?
|
|
</p>
|
|
</div>
|
|
|
|
<div align=center>
|
|
</div>
|
|
</div>
|
|
<div id="outline-container-orgaf007e6" class="outline-2">
|
|
<h2 id="orgaf007e6">Failed2ban At All</h2>
|
|
<div class="outline-text-2" id="text-orgaf007e6">
|
|
<p>
|
|
We weren't really able to conclusively figure out as to what was going wrong with Fail2ban, but our presumption had to do with pf somehow, or with our Fail2ban configuration. We'd found before in FreeBSD's forums<sup><a id="fnr.1" class="footref" href="#fn.1" role="doc-backlink">1</a></sup> a link to using Fail2ban bans with pf on a fairly helpful FreeBSD-centric blog<sup><a id="fnr.2" class="footref" href="#fn.2" role="doc-backlink">2</a></sup> and, huh… Fail2ban was trying to use iptables. What? That's so weird! We did almost everything identically to this blog's config too! We even overrid the Fail2ban config defaults to say "Hey, when you're banning an IP, PLEASE use pf to do this! Thanks!" But alas, IPs were not being banned. It worked when we sat there and added these IPs to the banlist ourselves using Fail2ban manually, but not automatically. Hm, odd.
|
|
</p>
|
|
|
|
<p>
|
|
This was a massive problem that we immediately had to take to the CISO. We have a bruteforce prevention system, and it isn't even working after we set it up correctly, this is not good! It was decided among the SysAdmins and Network Engineers that we were going to completely migrate over to blacklistd to properly enforce our IP bans, or that was the hope. In all honesty, we were afraid that this system wouldn't work either, or it'd be esoteric. This was our security we were gambling on here! But, did we really have a reason to be afraid?
|
|
</p>
|
|
</div>
|
|
|
|
<div align=center>
|
|
</div>
|
|
</div>
|
|
<div id="outline-container-orgd5f6c85" class="outline-2">
|
|
<h2 id="orgd5f6c85">Get Blacklisted!</h2>
|
|
<div class="outline-text-2" id="text-orgd5f6c85">
|
|
<p>
|
|
So what even is blacklistd? Well, in versions of FreeBSD prior to 15, blacklistd is a similar service to Fail2ban but provided natively in FreeBSD to do the same task. While Fail2ban is in Python, blacklistd is in C, which has far superior performance with a much smaller resource footprint, something we absolutely loved! A lot of the code in blacklistd also comes from NetBSD's blocklistd, and blocklistd is now what the daemon is called in FreeBSD 15; and surprisingly this was a hell of a simple system for us to setup. Firstly, the config is dead simple in contrast to Fail2ban, we just added an anchor for it to our <code>pf.conf</code> and modified the <code>blacklistd.conf</code> to increase the bantime and change the maximum number of tries. The OS even provided a fantastic and sane example config that required very little work from us to tweak! After that, we enabled it in the init system, enabled it in our <code>sshd_config</code>, and restarted everything.
|
|
</p>
|
|
|
|
<p>
|
|
Immediately, blacklistd got to work. Specifically, unlike Fail2ban, IPs got <b>banned</b>. Shot down right then and there after meeting blacklistd's ban criteria! We started to see immediate results. I really do mean <b>immediate</b>. IPs were already starting to get shot down by blacklistd and added to the pf table. It was a serious boost to our peace of mind, and quite wild how little work it took to actually start blacklisting IPs trying to bruteforce SSH. Now of course, it goes without saying that blacklistd works at the socket level, it's a system that works between Layers 3 and 4 upon direct socket data whereas Fail2ban merely greps logfiles and acts upon events, meaning it's acting upon Layer 7 data to then make Layer 3 and 4 decisions.<sup><a id="fnr.1.1" class="footref" href="#fn.1" role="doc-backlink">1</a></sup> There are cases where Fail2ban might be the choice and blacklistd isn't, such as services running in Jails that blacklistd then cannot monitor if you're attempting to run a singular firewall system. It does go without saying though, that blacklistd as an SSH intrusion prevention system is phenomenally simpler on FreeBSD with pf than Fail2ban is. How simple is simple though?
|
|
</p>
|
|
</div>
|
|
|
|
<div align=center>
|
|
</div>
|
|
</div>
|
|
<div id="outline-container-org8172923" class="outline-2">
|
|
<h2 id="org8172923">Try It Yourself!</h2>
|
|
<div class="outline-text-2" id="text-org8172923">
|
|
<p>
|
|
It's amazing how quick it is to get blacklistd with a simple enough configuration running on a FreeBSD system using pf. First, let's say that your <code>/etc/pf.conf</code> looks something like this:
|
|
</p>
|
|
|
|
<div class="org-src-container">
|
|
<pre class="src src-nil">set block-policy drop
|
|
set skip on lo0
|
|
scrub in
|
|
|
|
block in log on iface0 all
|
|
pass in quick log on iface0 proto tcp from any to any 22 keep state
|
|
pass in quick on iface0 inet proto icmp icmp-type { echoreq, unreach, timex } keep state
|
|
pass out quick log on iface0 proto { tcp, udp, icmp } from any to any keep state
|
|
</pre>
|
|
</div>
|
|
|
|
<p>
|
|
So we've got our SSH port open in pf, but inherently that is not going to simply stop bruteforcing. This just means every other port is going to have packets sent to it dropped (since the block-policy is set to drop). We have to first add in the anchor for blacklistd into our <code>/etc/pf.conf</code>:
|
|
</p>
|
|
|
|
<div class="org-src-container">
|
|
<pre class="src src-nil">set block-policy drop
|
|
set skip on lo0
|
|
scrub in
|
|
|
|
anchor "blacklistd/*" in on iface0
|
|
|
|
block in log on iface0 all
|
|
pass in quick log on iface0 proto tcp from any to any 22 keep state
|
|
pass in quick on iface0 inet proto icmp icmp-type { echoreq, unreach, timex } keep state
|
|
pass out quick log on iface0 proto { tcp, udp, icmp } from any to any keep state
|
|
</pre>
|
|
</div>
|
|
|
|
<p>
|
|
Great, now we can go setup our <code>/etc/blacklistd.conf</code>. There is surprisingly not a whole ton we need to do, because FreeBSD 14.3 out the box gives us a great starting point for our blacklistd rules, we can just change some of the ban lengths and attempts:
|
|
</p>
|
|
|
|
<div class="org-src-container">
|
|
<pre class="src src-nil">#
|
|
# Blacklist rule
|
|
# adr/mask:port type proto owner name nfail disable
|
|
[local]
|
|
ssh stream tcp * * 3 24h
|
|
</pre>
|
|
</div>
|
|
|
|
<p>
|
|
The nfail option is just specifying how many failed login attempts until blacklistd gets triggered, and the disable option is how long the ban lasts. Any rules set under <code>[local]</code> are in regards to locally ran services from the system and thus how they get handled by blacklistd.<sup><a id="fnr.3" class="footref" href="#fn.3" role="doc-backlink">3</a></sup> Now that we've created our blacklistd rules, let's go into <code>/etc/ssh/sshd_config</code> and set <code>UseBlacklist yes</code> to start using blacklistd, then add <code>blacklistd_enable</code>"YES"= to our <code>/etc/rc.conf</code>. Now restart <code>sshd</code>, start <code>blacklistd</code>, and run <code>pfctl -f /etc/pf.conf</code> to reload your new firewall rules and you've got blacklistd running! Surprisingly simpler than using Fail2ban, and trust us… we jumped through too many hoops with that…
|
|
</p>
|
|
|
|
<p>
|
|
I don't expect this to have been an eye-opening or profound entry, but it's a worthwhile writeup for any aspiring FreeBSD SysAdmins and Engineers to look at when using real FreeBSD systems in real production environments when weighing out how best to handle their security posture. Until then, stay safe on the internet and always better to be safe rather than sorry!
|
|
</p>
|
|
</div>
|
|
|
|
<div align=center>
|
|
</div>
|
|
</div>
|
|
<div id="footnotes">
|
|
<h2 class="footnotes">Footnotes: </h2>
|
|
<div id="text-footnotes">
|
|
|
|
<div class="footdef"><sup><a id="fn.1" class="footnum" href="#fnr.1" role="doc-backlink">1</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
|
|
<a href="https://forums.freebsd.org/threads/fail2ban-1-1-x-with-pf-need-help-before-i-lose-it-where-to-find-docs-or-examples.98862/">The FreeBSD Forums Post</a>
|
|
</p></div></div>
|
|
|
|
<div class="footdef"><sup><a id="fn.2" class="footnum" href="#fnr.2" role="doc-backlink">2</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
|
|
<a href="https://dbdemon.com/pf_and_fail2ban/">dbdemon's Blog Post About Fail2ban and pf</a>
|
|
</p></div></div>
|
|
|
|
<div class="footdef"><sup><a id="fn.3" class="footnum" href="#fnr.3" role="doc-backlink">3</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
|
|
<a href="https://man.freebsd.org/cgi/man.cgi?query=blacklistd.conf&apropos=0&sektion=5&manpath=FreeBSD+14.3-RELEASE&format=html">FreeBSD 14.3 Man Page for blacklistd.conf</a>
|
|
</p>
|
|
</div></div></div>
|
|
|
|
|
|
</div>
|
|
</div></div>
|
|
</body>
|
|
</html>
|