Як захистити себе від спамерів шо атакують поштовий сервер Postfix - Fail2ban, Policyd2
Як захистити себе від спамерів що атакують поштовий сервер Postfix.
Визначаємо активних користувачів що відсилають пошту понад дозволенного ліміту за певний час, блокуємо доступ з IP адреси до сервера на певний час. Також блокуємо користувачів що ввели забагато неправильних паролів: authentication failed та супер активних користувачів що хочуть надіслати дуже швидко і багато : Connection rate limit exceeded.
Компоненти:
Policyd2
/usr/ports/mail/policyd2
policyd2-2.0.12 - Policyd v2 is a multi-platform policy server for popular MTA …
/usr/local/etc/cluebringer.conf:
[server]
Protocols to load
protocols=«EOT
Postfix
#Bizanga
EOT
Modules to load
modules=«EOT
Core
AccessControl
#CheckHelo
#CheckSPF
#Greylisting
Quotas
EOT
[database]
DSN=DBI:Pg:database=policyd;host=localhost
Username=somedbuser
Password=somedbuserpwd
Access Control module
[AccessControl]
enable=1
Greylisting module
[Greylisting]
enable=0
CheckHelo module
[CheckHelo]
enable=0
CheckSPF module
[CheckSPF]
enable=0
Quotas module
[Quotas]
enable=1
Postfix & Policyd2
postfix/main.cf:
smtpd_recipient_restrictions =
check_policy_service inet:127.0.0.1:10031,
permit_mynetworks,
reject_non_fqdn_sender,
reject_sender_login_mismatch,
permit_sasl_authenticated,
reject_unauth_destination
smtpd_end_of_data_restrictions = check_policy_service inet:127.0.0.1:10031
anvil_rate_time_unit = 240s
smtpd_client_connection_rate_limit = 10
smtpd_client_event_limit_exceptions = $mynetworks
далі налаштування policyd2 тільки через Web interface.
cluebringer-2.0.12/INSTALL:
- Install the webui/* into your apache directory, check out includes/config.php and adjust the MySQL server details.
Таким чином усі користувачі що знаходяться у таблиці Member “newuser” попадають під правила Policy List: “newusers to not internal”
А модуль QUOTAS та POLICY: “newusers to not internal” підраховує скільки відіслано повідомлень за кількістю (MessageCount) , або скільки повідомлень за розміром (MessageCumulativeSize). Якщо ліміт перевищено, то виконується дія за POLICY: “newusers to not internal” - REJECT “LIMIT AT QUOTAUSE” - відкинути, і це буде записано до postfix лог файлу.
Fail2ban
/usr/ports/security/py-fail2ban
py27-fail2ban-0.8.6 - Scans log files and bans IP that makes too many password …
/usr/local/etc/fail2ban/filter.d/postfix-sasl.conf:
failregex = (?i): warning: [-._\w]+[
/usr/local/etc/fail2ban/filter.d/postfix-conlim.conf:
failregex = Connection rate limit exceeded: .* from (.*)[
/usr/local/etc/fail2ban/filter.d/postfix-overlim.conf:
failregex = reject: RCPT from (.*)[
/usr/local/etc/fail2ban/jail.conf:
[postfix-sasl]
enabled = true
filter = postfix-sasl
action = pf
logpath = /var/log/maillog
bantime = 360000
maxretry = 4
[postfix-conlim]
enabled = true
filter = postfix-conlim
action = pf
logpath = /var/log/maillog
bantime = 360000
maxretry = 4
[postfix-overlim]
enabled = true
filter = postfix-overlim
action = pf
logpath = /var/log/maillog
bantime = 360000
maxretry = 4
Описую як треба передати інформацію до таблиці файровола pf, також завершення усіх поточних з’єднань за визначеною адресою та портами SMTP, SMTPS сервера.
Додатково для спрощення аналізу - оновлю файл з переліком блокованих адрес - fail2ban.txt.
/usr/local/etc/fail2ban/action.d/pf.conf:
actionban = /sbin/pfctl -t fail2ban -T add
actionunban = /sbin/pfctl -t fail2ban -T delete
pf.conf:
block banned SMTP’s IP
table
Надалі до таблиці policyd2 - Member “newuser”, додаємо користувачів за котрими треба “наглядати” у ручному або автоматичному режимі.
Автоматичний режим додавання користувачів до “нагляду”
Базується на аналізі лог файлу на предмет підрахувань спроб авторизації користувачів з різних країн та різних інтернет провайдерів під одним обліковим записом.
У мене в наявності є рішення з описаним класом на PHP, а також спрощену версію з використанням шел скрипту.
Ось скрипт що додає до бази даних спроби авторизації користувачів та їх адреси та код інтернет провайдеру що видав цю адресу.
store-postfix-sasl.sh:
#!/bin/sh
echo “DELETE FROM xm_sasl_user_activity;”| psql policyd2 somedbuser
( zcat /var/log/maillog.0.bz2 ; cat /var/log/maillog ) | grep “sasl_username=” | cut -d “ “ -f 1,3,4,10,8 | \
while read str; do \
#echo $str;
date=echo $str | sed "s/^\(.\*\) client.\*/\1/g"
ip=echo $str | sed "s/.\*\[\(.\*\)\].\*/\1/g"
user=echo $str | sed "s/.\*\=\(.\*\)/\1/g"
#sed “s/.*[(.*)].*/\1/g”
if [ -n “$ip” ]; then
isp=whois $ip | grep netname |head -n 1| /usr/bin/awk '{ FS = ":"; gsub(/^ \*/,"", $2);print $2;}'
if [ -z $isp ]; then
isp=whois $ip | grep '\-num:' |head -n 1| /usr/bin/awk '{ FS = ":"; gsub(/^ \*/,"", $2);print $2;}'
fi
if [ -z $isp ]; then
isp=whois $ip | grep -i 'netname:' |head -n 1| /usr/bin/awk '{ FS = ":"; gsub(/^ \*/,"", $2);print $2;}'
fi
if [ “$isp” == “N/A” ]; then
isp=whois $ip | grep -i 'ownerid:' |head -n 1| /usr/bin/awk '{ FS = ":"; gsub(/^ \*/,"", $2);print $2;}'
fi
year=date "+%Y"
echo $year $date
# echo $user
# echo $ip
# echo $isp
echo “INSERT INTO sasl_user_activity (sasl_username,ip,netname,tstamp) VALUES (‘${user}’,’${ip}’,’${isp}’,to_timestamp(‘${year} ${date
psql -q imp nobody
fi
done
Databse policyd2 table sasl_user_activity:
>psql policyd2 somedbuser
policyd2=> \d sasl_user_activity Table “public.sasl_user_activity”
Column | Type | Modifiers
—————+—————————–+—————————–
user_id | integer | not null default nextval(‘sasl_user_activity_user_id_seq’::regclass)
sasl_username | character varying(30) | not null
ip | inet | not null
netname | character varying(70) |
tstamp | timestamp without time zone | not null default now()
Indexes:
“sasl_user_activity_pkey” PRIMARY KEY, btree (user_id)
Class PHP SASL_Activity.php:
<?php
require_once ‘Net/Whois.php’;
class SASL_Activity {
protected $db;
public function __construct($db,$debug) {
$this->db = $db;
$this->whois = new Net_Whois;
$this->pattern1 = ‘/origin: *(.*)/i’;
$this->pattern3 = ‘/netname: *(.*)/’;
$this->pattern2 = ‘/ownerid: *(.*)/i’;
$this->pattern4 = ‘/netname: *(.*)/i’;
$this->pattern5 = ‘/(.*) (NET-.*)/i’;
$this->debug = $debug;
}
public function clear() {
$res=$this->db->query(“DELETE FROM sasl_user_activity”);
$result=$res->fetch();
return $result[0];
}
public function check($user,$ip) {
$res = $this->db->query(“SELECT count(*) FROM sasl_user_activity WHERE sasl_username=? AND ip=?”, array($user,$ip));
return (‘0’ == $res->fetchColumn(0));
}
public function get_isp($ip) {
$res = $this->db->query(“SELECT netname FROM sasl_user_activity WHERE ip=? LIMIT 1”, array($ip));
$isp = $res->fetchColumn(0);
if ( $isp == ‘’) {
$data1 = $this->whois->query($ip);
if (preg_match($this->pattern1,$data1,$matches1)) {
$isp=trim($matches1[1]);
}else if (preg_match($this->pattern2,$data1,$matches1)) {
$isp=trim($matches1[1]);
}else if (preg_match($this->pattern3,$data1,$matches1)) {
$isp=trim($matches1[1]);
}else if (preg_match($this->pattern4,$data1,$matches1)) {
$isp=trim($matches1[1]);
}else if (preg_match($this->pattern5,$data1,$matches1)) {
$isp=trim($matches1[1]);
}else{
$isp=”***UNDEFINED”;
}
}else{
if ($this->debug) echo “ISP detected early: $isp\t”;
}
return $isp;
public function store ($date,$user,$ip) {
if ($this->check($user,$ip)) {
$ispa=$this->get_isp($ip);
if ($this->debug) echo “ISP: $ispa”;
if ($this->db->query(“INSERT INTO sasl_user_activity (sasl_username,ip,netname,tstamp) VALUES (?,?,?,?)”,array(${user},${ip},${ispa},${date}))) {
if ($this->debug) echo “\tInsterted OK\n”;
}
}else{
if ($this->db->query(“UPDATE sasl_user_activity SET (tstamp) = (?) WHERE sasl_username=? AND ip=?”,array(${date},${user},${ip}))) {
if ($this->debug) echo “\tUpdated OK\n”;
}
}
}
public function get_activity(){
$res = $this->db->query(“SELECT foo.sasl_username, COUNT(foo.sasl_username) AS differ_isp FROM (SELECT Distinct sasl_username, netname FROM xm_sasl_user_activity GROUP BY netname, sasl_username) AS foo GROUP BY sasl_username ORDER by differ_isp DESC”);
return $res;
}
public function get_hyper_activity($limit=0){
$res = $this->db->query(“SELECT * FROM (SELECT foo.sasl_username, COUNT(foo.sasl_username) AS differ_isp FROM (SELECT Distinct sasl_username, netname FROM xm_sasl_user_activity GROUP BY netname, sasl_username) AS foo GROUP BY sasl_username) AS activity
return $res->fetchAll(Zend_Db::FETCH_ASSOC);
}
private function __clone()
{}
}
sasl-act.php:
date_default_timezone_set(‘UTC’);
set_time_limit(60 * 60 * 24);
error_reporting( E_ALL ^ E_NOTICE);
require_once ‘SASL_Activity.php’;
$debug=0;
$dirMailLog = ‘/var/log/’;
$fileMailLog = ‘maillog.0.bz2’;
$log_file=$dirMailLog . $fileMailLog;
$useractivity = new SASL_Activity($db,$debug);
//Oct 3 02:10:14 mail postfix/smtpd[49400]: 1B33DB44909: client=unknown[137.243.223.131], sasl_method=LOGIN, sasl_username=noman
//for parse log of postfix
$pattern = ‘/(\w{3}[^a-zA-Z]+)+ mail postfix.* client=.*?[([0-9.]+)+],.*sasl_username=(.*)/’;
$fh = bzopen($log_file,’r’) or die($php_errormsg);
$i = 1;
$file_prev_day=strtotime(‘-1 day’, filemtime($log_file));
$file_year=date (“Y”, $file_prev_day);
if ($debug) echo “$log_file year:$file_year from:”. date(‘Y-m-d’,$file_prev_day). PHP_EOL;
//echo “Purge table:”.$useractivity->clear().”\n”;
while (!feof($fh)) {
// read each line and trim off leading/trailing whitespace
if ($s = bzread($fh,16384)) { //fgets
// match the line to the pattern
if (preg_match($pattern,$s,$matches)) {
list($whole_match,$date,$remote_host,$user) = $matches;
if ($debug) echo “\n##:”.$i.” $file_year $date $remote_host $user”;
if ($debug) echo “\nUser: $user\tIP: $remote_host”;
$date=”$file_year “.$date;
if ($debug) echo “\nStore: “;
$useractivity->store($date,$user,$remote_host);
} else {
// complain if the line didn’t match the pattern
error_log(“Can’t parse line $i: $s”);
}
}
$i++;
}
bzclose($fh) or die($php_errormsg);
add_newusers_activity($members,$useractivity);
//add hyper acivity users to tables newusers poilicy
$hyperact=$useractivity->get_hyper_activity(10);
foreach($hyperact as &$val) {
if ($debug) echo $val[sasl_username].PHP_EOL;
$user=$val[sasl_username];
// here some methods for addMember($user.’) to policyd2 tables with members of newusers poilicy
}



