Centraliserat Fail2Ban

- SCAB - > Bloggen > Cybersäkerhet > Centraliserat Fail2Ban
Centralized Fail2Ban

Fail2Ban är ett av de mest använda systemen för att stoppa attacker mot Linux servers genom att scanna loggfiler och från resultaten blockera IP adresser i brandväggen. Fail2Ban är dock designat för att köras lokalt på en enda server, men kan enkelt byggas ut till ett centraliserat Fail2Ban system där alla servers i en grupp kan dela en gemensam lista på IP adresser som behöver blockeras.

I stället för att vara reaktiv får man ett proaktivt system, vilket är till stor nytta då samma angripare ofta angriper många servers.

Ett centraliserat Fail2Ban system är lätt att bygga för den händige. Det vi behöver är en centralt belägen databas, något som triggar en uppdatering av databasen, och något som hämtar data och blockerar i brandväggen. På ett säkert sätt i alla delar.

I det exempel som beskrivs här används en RHEL 8 server (vilket också skall fungera med alla kloner baserade på RHEL) och en MySQL databas, exemplet bör dock inte vara något större problem att “översätta” till andra system. Det som beskrivs för Fail2Ban bör vara generellt för alla Linux distributioner.

OBS! Det som beskrivs här är inte ett komplett nyckeln-i-handen system, du behöver själv hantera all säkerhet runt detta och även anpassa exempelkod mm för din specifika miljö och dina specifika behov. Om du har SELinux aktiverat (vilket du absolut bör ha i dina produktionssystem) så behöver du sannolikt göra justeringar där med för att få allt att fungera.

Ps. Vi är experter på säkerhet, kontakta oss när du behöver hjälp!

1. Skapa en databas (endast på master servern)

Installera MySQL enligt smak och tycke, skapa databas, tabell, användare samt schemaläggare enligt nedan, ändra namn och lösenord enligt det du vill använda:

CREATE DATABASE fail2ban;

CREATE TABLE `fail2ban` (
  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `hostname` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `created` datetime NOT NULL,
  `name` text COLLATE utf8_unicode_ci NOT NULL,
  `protocol` varchar(16) COLLATE utf8_unicode_ci NOT NULL,
  `port` varchar(32) COLLATE utf8_unicode_ci NOT NULL,
  `ip` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
  `client` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
  INDEX `hostname` (`hostname`,`ip`),
  INDEX `client` (`client`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

CREATE USER 'fail2ban'@'localhost' IDENTIFIED BY 'fail2ban';
GRANT ALL PRIVILEGES ON fail2ban.* TO 'fail2ban'@'localhost';
CREATE USER 'fail2ban'@'%' IDENTIFIED BY 'fail2ban';
GRANT ALL PRIVILEGES ON fail2ban.* TO 'fail2ban'@'%';
FLUSH PRIVILEGES;

SET GLOBAL event_scheduler = ON;

SHOW VARIABLES WHERE VARIABLE_NAME = 'event_scheduler';

CREATE EVENT `cleaning` ON SCHEDULE EVERY 1 DAY ON COMPLETION PRESERVE ENABLE
  DO
  DELETE FROM fail2ban WHERE `created` < NOW() - INTERVAL 1 MONTH;

Schemaläggaren används för att hålla databasen någorlunda städad genom att radera gamla obehövliga inlägg som sparas för tex statistik eller funktionskontroll, ändra enligt eget behov.

2. Uppdatera FirewallD

Försäkra dig om att inte bli utlåst av misstag genom att skapa en vitlistad zon med din(a) IP du använder för att logga in i systemen. Gör detta på samtliga servers som skall ingå i Fail2Ban gruppen.

firewall-cmd --permanent --new-zone=000-trusted
firewall-cmd --set-target=ACCEPT --zone=000-trusted --permanent
firewall-cmd --permanent --zone=000-trusted --add-source=123.123.123.123
firewall-cmd --reload
firewall-cmd --zone=000-trusted --list-all

Öppna en speciell port för att ta emot uppdateringar och hämtning av data. Endast på master servern.

firewall-cmd --permanent --add-port=49153/tcp
firewall-cmd --reload

3. Skapa en virtuell webbplats (endast på master servern)

Uppdateringar och hämtning av data sker via en speciell webbplats som lyssnar på port 49153. Det spelar ingen roll vilken webbserver eller domänadress som används så länge den är säkrad med certifikat och har stöd för dynamiskt innehåll. I detta exempel använder vi PHP.

VIKTIGT! Du bör också skapa en accesslista i webbplatsens konfiguration för att enbart tillåta servers i Fail2Ban gruppen (inklusive master servern) och hålla resten av världen utanför. Gör så här i Apache och i Nginx.

index.php

Tar emot uppdateringar. För att skydda blockering av egna IP adresser läggs varje server inklusive master hosten in i arrayen $whitelisted[]. Denna array måste uppdateras om servers läggs till eller tas bort ur gruppen. Vi tillåter endast anrop med POST av säkerhetsskäl.

<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

// block registration array of own hosts used in group
$whitelisted[] = '123.123.123.123';	// host1.my.domain

$client = (!empty ($_SERVER['REMOTE_ADDR'])) ? addslashes($_SERVER['REMOTE_ADDR']) : '?';
$name = (!empty($_POST['name'])) ? addslashes($_POST['name']) : die('no valid input');				// jail name
$protocol = (!empty($_POST['protocol'])) ? addslashes($_POST['protocol']) : die('no valid input');	// blocked service
$port = (!empty($_POST['port'])) ? addslashes($_POST['port']) : die('no valid input');				// blocked port
$ip = (!empty($_POST['ip'])) ? addslashes($_POST['ip']) : die('no valid input');					// blocked IPv4
$hostname = (!empty($_POST['hostname'])) ? addslashes($_POST['hostname']) : die('no valid input');	// blocked at host

if (in_array($ip, $whitelisted)) { die('whitelisted ip'); }

$conn = new mysqli('localhost', 'fail2ban', 'fail2ban', 'fail2ban');
if ($conn->connect_error) {
  die("Connection failed: " . $conn->connect_error);
}

$query = 'INSERT INTO `fail2ban` set hostname="'.$hostname.'", name="'.$name.'", protocol="'.$protocol.'", port="'.$port.'", ip="'.$ip.'", client="'.$client.'", created=NOW()';
$result = $conn->query($query);

$conn->close();

die('ok');

?>

fetchip.php

Returnerar en textlista med nyligen registrerade IP adresser, en per ny rad. Vi tillåter både GET och POST anrop för att tillåta enkel test via webbläsare, men POST bör användas av säkerhetsskäl av systemet. Ändra alla $_REQUEST till $_POST i scriptet om du inte behöver access från webbläsare.

<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

// only return what other hosts have reported, caller own reports are already blocked in caller firewall
if (!empty($_REQUEST['exclude']) && preg_match("/^[a-z0-9\.]+$/i", $_REQUEST['exclude'])) {
	$exclude = $_REQUEST['exclude'];
} else {
	die('exclude is required');
}

// filter by reported time to avoid dublettes, timespan should be the same as used in cron to fetch this list, default 1 minute
$intval = (!empty($_REQUEST['intval']) && is_numeric($_REQUEST['intval'])) ? $_REQUEST['intval'] : 60;

$ips = '';

$conn = new mysqli('localhost', 'fail2ban', 'fail2ban', 'fail2ban');
if ($conn->connect_error) { die("Connection failed: " . $conn->connect_error); }
$query = 'SELECT DISTINCT ip FROM `fail2ban` WHERE hostname != "'.$exclude.'" AND created > NOW() - INTERVAL '.$intval.' SECOND;';
$result = $conn->query($query);

if ($result->num_rows > 0) {
	while($row = $result->fetch_assoc()) {
		$ips .= $row["ip"]."\n";
	}
}

$conn->close();

header('Content-Type:text/plain');
header('Content-Transfer-Encoding: binary');
header('Cache-Control: must-revalidate');
header('Pragma: public');
die($ips);

?>

VIKTIGT! Testa alltihopa, ordentligt, så att du är helt säker på att allt fungerar som det är tänkt!

4. Låt Fail2Ban göra uppdateringar (alla servers i gruppen)

Skapa en ny action i “./fail2ban/action.d” mappen och lägg till banaction i jail configrationen.

central_fail2ban.conf

# Fail2Ban configuration file
#
# Author: Erik Schütten <erik@scab.ax>
#
# Send banned IP and attributes to central fail2ban database
# Require "cf2b_host" var as database host full URL set in jail.local
#
# action: central_fail2ban[cf2b_host="%(cf2b_host)s", port="%(port)s", protocol="%(protocol)s"]
#

[Definition]

norestored = 1
actionstart = 
actionstop =
actioncheck =
actionban = curl -s -d "name=f2b-<name>&protocol=<protocol>&port=<port>&ip=<ip>&hostname=<fq-hostname>" -X POST <cf2b_host>
actionunban =

[Init]
port = 1:65535
protocol = tcp

jail.local

...
cf2b_host  = https://master.my.tld:49153
...
action_mw = %(banaction)s[name=%(__name__)s, ...
            %(mta)s-whois[name=%(__name__)s, ...
            central_fail2ban[cf2b_host="%(cf2b_host)s", port="%(port)s", protocol="%(protocol)s"]

Starta om fail2ban tjänsten. Vänta med följande steg tills du är helt säker på att Fail2Ban skickar in korrekt data till databasen.

5. Hämta data och blockera IP (alla servers i gruppen)

I det sista steget skall vi hämta ut data och blockera alla returnerade IP adresser i den lokala brandväggen. Vi gör detta genom att köra ett Bash script i Cron.

fetchip_list.sh

OBS! Vi använder även här en vitlista för att inte i misstag blockera några IP adresser i Fail2Ban server gruppen. Vitlistan skall vara en vanlig textfil med varje IP adress på egen rad. Vi använder grepcidr i scriptet för detta som bör vara installerad.

Vi loggar också varje anrop.

Detta script skall köras regelbundet via Cron och tar några input parametrar om man inte vill använda standardinställningar. Notera att alla input parametrar måste anges om någon parameter behövs ändras.

<exclude_host> <fetch_interval> <ban_time>

<exclude_host> = den server scriptet körs på för att inte få egna inlägg, tex host1.my.tld
<fetch_interval> = hämtningsintervall i sekunder, samma som anges i Cron, standard 60 sekunder
<ban_time> = tidsinställning i sekunder för blockeringen, standard 86400 sekunder (24h)

Scriptet skall vara körbart (chmod u+x /path/to/script/…)

#!/bin/bash
#
# Script made by erik@scab.ax (https://scab.ax), use at your own risk
#

# paths to honeypot whitelist and log
WHITELIST_PATH=/path/to/whitelist
LOGPATH=/var/log/fetchip_list.log

# (calling) host to exclude
if [ -z "$1" ]; then
	echo "No host supplied"
	exit 0
fi

# time range in seconds to fetch new items, should be same as cron interval for this script, default 1 minute
if [ -z "$2" ]; then
		SEARCH_RANGE=60
	else
		SEARCH_RANGE=$2
fi

# time range in seconds to block item, default 24 hours
if [ -z "$3" ]; then
		RULE_TIMEOUT=86400
	else
		RULE_TIMEOUT=$3
fi

SEARCH_RANGE=$(( $SEARCH_RANGE + 1 ))
HOST_IP=`hostname -I`

echo "$(date -Iseconds) INFO $1/fetchip.php.php?exclude=$HOSTNAME&intval=$SEARCH_RANGE call initiated" >> $LOGPATH

curl -s -d "exclude=$HOSTNAME&intval=$SEARCH_RANGE" -X POST "$1/fetchip.php.php" | while read line ; do

	if [ $HOST_IP = $line ]; then
		echo "$(date -Iseconds) INFO $HOST_IP = $line, no action" >> $LOGPATH
		continue
	fi

	if [ "$WHITELIST_PATH" ] && [ -f "$WHITELIST_PATH" ]; then
		if grepcidr -f "$WHITELIST_PATH" <(echo "$line") >/dev/null; then
			echo "$(date -Iseconds) INFO $line is whitelisted, no action" >> $LOGPATH
			continue
		fi
	fi

	firewall-cmd --zone=public --timeout=$RULE_TIMEOUT --add-rich-rule="rule family='ipv4' source address='$line' reject type='icmp-port-unreachable'"
	echo "$(date -Iseconds) BANNED $RULE_TIMEOUT IPv4 $line" >> $LOGPATH

done

exit 0

Skapa en ny tom loggfil

touch /var/log/fetchip_list.log

Rotera loggfilerna med /etc/logrotate.d

fetchip_list

/var/log/fetchip_list.log {
    missingok
    notifempty
    weekly
    rotate 4
    compress
    delaycompress
}

Lägg till scriptet i Cron, vi kör varje minut, alla params är med för tydliggöra det hela.

...
* * * * * /path/to/fetchip_list.sh https://master.my.tld:49153 60 86400 >/dev/null 2>&1
...

Klart! Testa, testa, testa innnan du rullar ut det i produktion. Lycka till!