{"id":851,"date":"2026-08-10T23:24:25","date_gmt":"2026-08-10T23:24:25","guid":{"rendered":"https:\/\/virtualserversvps.com\/blog\/?p=851"},"modified":"2026-09-23T22:03:58","modified_gmt":"2026-09-23T22:03:58","slug":"fail2ban-setup-vps-ssh-bruteforce-protection","status":"publish","type":"post","link":"https:\/\/virtualserversvps.com\/blog\/fail2ban-setup-vps-ssh-bruteforce-protection\/","title":{"rendered":"Custom Fail2ban Filters and Recidive Jails for VPS Bruteforce Defense"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">The default <code>sshd<\/code> jail in Fail2ban bans an IP after repeated failures \u2014 but the moment an attacker rotates source addresses, a per-IP ban is worthless. This article focuses on what most tutorials skip: writing custom filters for services that log in non-standard formats, using <code>recidive<\/code> to catch repeat offenders across jails, and testing filters before they go live. Set the base protections up first, then extend with the jails below.<\/p>\n\n\n<!-- wp:heading level=\"2\" -->\n<h2 class=\"wp-block-heading\">Diagnose which jail is doing the work<\/h2>\n<!-- \/wp:post-content -->\n\n<!-- wp:paragraph -->\n<p class=\"wp-block-paragraph\">Before tuning blind, confirm what is actually being banned. The default <code>sshd<\/code> jail catches password guesses, but if your box only allows key auth, most failures are pre-auth probes that still count. Look at the distribution of banned addresses over a week.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:code -->\n<pre class=\"wp-block-code\"><code>grep \" Ban \" \/var\/log\/fail2ban.log | awk '{print $NF}' | sort | uniq -c | sort -rn | head\nfail2ban-client status sshd<\/code><\/pre>\n<!-- \/wp:code -->\n\n<!-- wp:paragraph -->\n<p class=\"wp-block-paragraph\">If the same source subnet keeps reappearing after short bans, the default <code>bantime<\/code> is too short for that source. That is the exact case the escalating and recidive jails are built for.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading level=\"2\" -->\n<h2 class=\"wp-block-heading\">Confirm what is actually being banned<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p class=\"wp-block-paragraph\">Before adding jails, check the numbers. On a default instance the log tells the truth; a grep over the last day shows the real attack surface.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:code -->\n<pre class=\"wp-block-code\"><code>journalctl -u ssh --since yesterday | grep -c \"Failed password\"\nfail2ban-client status sshd<\/code><\/pre>\n<!-- \/wp:code -->\n\n<!-- wp:paragraph -->\n<p class=\"wp-block-paragraph\">If <code>status sshd<\/code> shows a healthy ban count but failures keep climbing, the noise is distributed across many single-shot IPs. That is the signal to move to aggregated bans.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading level=\"2\" -->\n<h2 class=\"wp-block-heading\">Write a custom filter for a non-standard log<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p class=\"wp-block-paragraph\">Fail2ban ships filters for common services only. For anything else, point a filter at the log and give it a regex with a named <code>&lt;HOST&gt;<\/code> group. Example for an app that writes JSON lines to its own log:<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:code -->\n<pre class=\"wp-block-code\"><code># \/etc\/fail2ban\/filter.d\/myapp.conf\n[Definition]\nfailregex = ^.*\"ip\":\"&lt;HOST&gt;\".*\"event\":\"auth_fail\".*$\nignoreregex =<\/code><\/pre>\n<!-- \/wp:code -->\n\n<!-- wp:heading level=\"2\" -->\n<h2 class=\"wp-block-heading\">Test the filter before enabling it<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p class=\"wp-block-paragraph\">A wrong regex bans innocent traffic. Always dry-run against real log lines.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:code -->\n<pre class=\"wp-block-code\"><code>fail2ban-regex \/var\/log\/myapp\/access.log \/etc\/fail2ban\/filter.d\/myapp.conf<\/code><\/pre>\n<!-- \/wp:code -->\n\n<!-- wp:paragraph -->\n<p class=\"wp-block-paragraph\">The output shows matched and missed lines. Only wire the filter into a jail once the match count is non-zero and the missed lines are genuinely unrelated traffic.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading level=\"2\" -->\n<h2 class=\"wp-block-heading\">Catch repeat offenders with recidive<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p class=\"wp-block-paragraph\">Single-IP bans expire. The <code>recidive<\/code> jail watches Fail2ban&#8217;s own log for IPs that trigger multiple bans, then applies a long ban. That is what turns a nuisance filter into a real deterrent.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:code -->\n<pre class=\"wp-block-code\"><code># \/etc\/fail2ban\/jail.d\/recidive.local\n[recidive]\nenabled  = true\nlogpath  = \/var\/log\/fail2ban.log\nbanaction = iptables-allports\nbantime  = 1w\nfindtime = 1d\nmaxretry = 3<\/code><\/pre>\n<!-- \/wp:code -->\n\n<!-- wp:table -->\n<figure class=\"wp-block-table\"><table><thead><tr><th>Setting<\/th><th>Nuisance jail<\/th><th>Recidive jail<\/th><\/tr><\/thead><tbody><tr><td>bantime<\/td><td>1h<\/td><td>1w<\/td><\/tr><tr><td>findtime<\/td><td>10m<\/td><td>1d<\/td><\/tr><tr><td>maxretry<\/td><td>5<\/td><td>3<\/td><\/tr><tr><td>Scope<\/td><td>Single service<\/td><td>All ports<\/td><\/tr><\/tbody><\/table><\/figure>\n<!-- \/wp:table -->\n\n<!-- wp:heading level=\"2\" -->\n<h2 class=\"wp-block-heading\">Whitelist before you widen the net<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p class=\"wp-block-paragraph\">Aggressive jails ban everything they see. Protect your own access and monitoring before enabling them.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:code -->\n<pre class=\"wp-block-code\"><code># \/etc\/fail2ban\/jail.d\/ignoreips.local\n[DEFAULT]\nignoreip = 127.0.0.1\/8 ::1 203.0.113.7 198.51.100.0\/24<\/code><\/pre>\n<!-- \/wp:code -->\n\n<!-- wp:list -->\n<ul class=\"wp-block-list\"><li>Include your office\/static IP and any monitoring or uptime prober that hits protected endpoints.<\/li><li>Reload, do not restart, to apply without dropping existing bans: <code>fail2ban-client reload<\/code>.<\/li><li>Verify the active rule set with <code>iptables -S | grep f2b<\/code> after each change.<\/li><\/ul>\n<!-- \/wp:list -->\n\n<!-- wp:heading level=\"2\" -->\n<h2 class=\"wp-block-heading\">Why per-IP bans are not enough<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p class=\"wp-block-paragraph\">A single-source ban assumes the attacker uses one address. Distributed credential-stuffing spreads attempts across thousands of addresses, each sending only a handful of failures, so no individual IP ever trips the retry threshold. The defence has two parts: lower your thresholds for the services that matter, and aggregate across jails so that an address banned once is treated as suspect system-wide. The <code>recidive<\/code> jail does the second; per-service filters with tighter <code>maxretry<\/code> do the first.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading level=\"2\" -->\n<h2 class=\"wp-block-heading\">Drop the noisy traffic at the firewall first<\/h2>\n<!-- wp:heading -->\n\n<!-- wp:paragraph -->\n<p class=\"wp-block-paragraph\">Fail2ban reacts to events; a firewall drop rule prevents them. A rule that drops known-bad address ranges before they reach sshd keeps your ban list short and your logs readable.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:code -->\n<pre class=\"wp-block-code\"><code>iptables -I INPUT -m conntrack --ctstate NEW -p tcp --dport 22 -m hashlimit   --hashlimit-name ssh --hashlimit-above 6\/min --hashlimit-burst 6   --hashlimit-mode srcip --hashlimit-htable-expire 60000 -j DROP<\/code><\/pre>\n<!-- \/wp:code -->\n\n<!-- wp:paragraph -->\n<p class=\"wp-block-paragraph\">This limits any single source to six new SSH connections per minute. Legitimate users never notice; credential-stuffing scripts hit the wall immediately.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading level=\"2\" -->\n<h2 class=\"wp-block-heading\">Tune the ban lifecycle for your threat model<\/h2>\n<!-- wp:heading -->\n\n<!-- wp:table -->\n<figure class=\"wp-block-table\"><table><thead><tr><th>Parameter<\/th><th>Effect<\/th><th>Sensible start<\/th><\/tr><\/thead><tbody><tr><td>bantime<\/td><td>How long an IP stays blocked<\/td><td>3600 s<\/td><\/tr><tr><td>findtime<\/td><td>Window over which retries are counted<\/td><td>600 s<\/td><\/tr><tr><td>maxretry<\/td><td>Failures allowed before ban<\/td><td>4<\/td><\/tr><tr><td>maxretry (recidive)<\/td><td>Jail triggers before long ban<\/td><td>3<\/td><\/tr><tr><td>ignoreip<\/td><td>Never ban these<\/td><td>Your admin + monitor IPs<\/td><\/tr><\/tbody><\/table><\/figure>\n<!-- \/wp:table -->\n\n<!-- wp:paragraph -->\n<p class=\"wp-block-paragraph\">Use incremental banning so repeat offenders escalate: first offence one hour, then one day, then a week. It keeps accidental lockouts short while making persistent attacks expensive.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:code -->\n<pre class=\"wp-block-code\"><code># \/etc\/fail2ban\/jail.d\/escalate.local\n[DEFAULT]\nbantime.increment = true\nbantime.factor = 24\nbantime.maxtime = 4w<\/code><\/pre>\n<!-- \/wp:code -->\n\n<!-- wp:heading level=\"2\" -->\n<h2 class=\"wp-block-heading\">What to monitor after the fact<\/h2>\n<!-- wp:heading -->\n\n<!-- wp:list -->\n<ul class=\"wp-block-list\"><li>Watch the total banned-IP count over time; a sudden climb usually means a new campaign against your range.<\/li><li>Alert if the number of currently banned IPs spikes above ten times your daily average.<\/li><li>Re-check <code>fail2ban-client status<\/code> after every reload to confirm all jails loaded \u2014 a syntax error in one jail file silently drops it.<\/li><\/ul>\n<!-- \/wp:list -->\n\n<!-- wp:paragraph -->\n<p class=\"wp-block-paragraph\">Handle the base SSH jail with the standard recipe, then let custom filters and recidive carry the load. A hardening pass runs best on a clean system image \u2014 see the <a href=\"https:\/\/virtualserversvps.com\/cloud-vps-benefits\/\">cloud VPS benefits<\/a> walkthrough for how the base environment is configured, or browse <a href=\"https:\/\/virtualserversvps.com\/\">VPS tutorials<\/a> for the rest of the hardening series.<\/p>\n<!-- \/wp:paragraph -->","protected":false},"excerpt":{"rendered":"<p>The default sshd jail in Fail2ban bans an IP after repeated failures \u2014 but the moment an attacker rotates source addresses, a per-IP ban is worthless. This article focuses on&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"iawp_total_views":2,"footnotes":""},"categories":[4],"tags":[],"class_list":["post-851","post","type-post","status-publish","format-standard","hentry","category-security-compliance"],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v26.1 (Yoast SEO v26.1) - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Custom Fail2ban Filters and Recidive Jails for VPS Bruteforce Defense - Virtual Servers VPS Blog<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/virtualserversvps.com\/blog\/fail2ban-setup-vps-ssh-bruteforce-protection\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Custom Fail2ban Filters and Recidive Jails for VPS Bruteforce Defense\" \/>\n<meta property=\"og:description\" content=\"Custom Fail2ban Filters and Recidive Jails for VPS Bruteforce Defense\" \/>\n<meta property=\"og:url\" content=\"https:\/\/virtualserversvps.com\/blog\/fail2ban-setup-vps-ssh-bruteforce-protection\/\" \/>\n<meta property=\"og:site_name\" content=\"Virtual Servers VPS Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-08-10T23:24:25+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-09-23T22:03:58+00:00\" \/>\n<meta name=\"author\" content=\"Virtual-Servers-Vps-Editor\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Virtual-Servers-Vps-Editor\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"4 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/fail2ban-setup-vps-ssh-bruteforce-protection\/\",\"url\":\"https:\/\/virtualserversvps.com\/blog\/fail2ban-setup-vps-ssh-bruteforce-protection\/\",\"name\":\"Custom Fail2ban Filters and Recidive Jails for VPS Bruteforce Defense - Virtual Servers VPS Blog\",\"isPartOf\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#website\"},\"datePublished\":\"2026-08-10T23:24:25+00:00\",\"dateModified\":\"2026-09-23T22:03:58+00:00\",\"author\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0\"},\"breadcrumb\":{\"@id\":\"https:\/\/virtualserversvps.com\/blog\/fail2ban-setup-vps-ssh-bruteforce-protection\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/virtualserversvps.com\/blog\/fail2ban-setup-vps-ssh-bruteforce-protection\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/fail2ban-setup-vps-ssh-bruteforce-protection\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/virtualserversvps.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Custom Fail2ban Filters and Recidive Jails for VPS Bruteforce Defense\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#website\",\"url\":\"https:\/\/virtualserversvps.com\/blog\/\",\"name\":\"Virtual Servers VPS Blog\",\"description\":\"\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/virtualserversvps.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0\",\"name\":\"Virtual-Servers-Vps-Editor\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g\",\"caption\":\"Virtual-Servers-Vps-Editor\"},\"sameAs\":[\"https:\/\/virtualserversvps.com\/blog\"],\"url\":\"https:\/\/virtualserversvps.com\/blog\/author\/virtualserversvps\/\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Custom Fail2ban Filters and Recidive Jails for VPS Bruteforce Defense - Virtual Servers VPS Blog","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/virtualserversvps.com\/blog\/fail2ban-setup-vps-ssh-bruteforce-protection\/","og_locale":"en_US","og_type":"article","og_title":"Custom Fail2ban Filters and Recidive Jails for VPS Bruteforce Defense","og_description":"Custom Fail2ban Filters and Recidive Jails for VPS Bruteforce Defense","og_url":"https:\/\/virtualserversvps.com\/blog\/fail2ban-setup-vps-ssh-bruteforce-protection\/","og_site_name":"Virtual Servers VPS Blog","article_published_time":"2026-08-10T23:24:25+00:00","article_modified_time":"2026-09-23T22:03:58+00:00","author":"Virtual-Servers-Vps-Editor","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Virtual-Servers-Vps-Editor","Est. reading time":"4 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/virtualserversvps.com\/blog\/fail2ban-setup-vps-ssh-bruteforce-protection\/","url":"https:\/\/virtualserversvps.com\/blog\/fail2ban-setup-vps-ssh-bruteforce-protection\/","name":"Custom Fail2ban Filters and Recidive Jails for VPS Bruteforce Defense - Virtual Servers VPS Blog","isPartOf":{"@id":"https:\/\/virtualserversvps.com\/blog\/#website"},"datePublished":"2026-08-10T23:24:25+00:00","dateModified":"2026-09-23T22:03:58+00:00","author":{"@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0"},"breadcrumb":{"@id":"https:\/\/virtualserversvps.com\/blog\/fail2ban-setup-vps-ssh-bruteforce-protection\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/virtualserversvps.com\/blog\/fail2ban-setup-vps-ssh-bruteforce-protection\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/virtualserversvps.com\/blog\/fail2ban-setup-vps-ssh-bruteforce-protection\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/virtualserversvps.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Custom Fail2ban Filters and Recidive Jails for VPS Bruteforce Defense"}]},{"@type":"WebSite","@id":"https:\/\/virtualserversvps.com\/blog\/#website","url":"https:\/\/virtualserversvps.com\/blog\/","name":"Virtual Servers VPS Blog","description":"","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/virtualserversvps.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/82a299a8284a66ff49f97c74684724a0","name":"Virtual-Servers-Vps-Editor","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/virtualserversvps.com\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/d820b15f1cd028e97610d9adf536df7be5cb6423869967037d468d5355fa003f?s=96&d=mm&r=g","caption":"Virtual-Servers-Vps-Editor"},"sameAs":["https:\/\/virtualserversvps.com\/blog"],"url":"https:\/\/virtualserversvps.com\/blog\/author\/virtualserversvps\/"}]}},"_links":{"self":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/851","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/comments?post=851"}],"version-history":[{"count":3,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/851\/revisions"}],"predecessor-version":[{"id":1207,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/posts\/851\/revisions\/1207"}],"wp:attachment":[{"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/media?parent=851"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/categories?post=851"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/virtualserversvps.com\/blog\/wp-json\/wp\/v2\/tags?post=851"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}