Бронированный Exim

В настоящей статье пойдет речь о небезызвестном агенте пересылки почты Exim, а именно о его настройке в части реализации достаточно мощной системы защиты от спама, блокирования подозрительных хостов, возможно рассылающих спам, защиты от подбора паролей и других полезных радостей положительно влияющих на общую защищенность почтового сервера. Материал решил оформить отдельно, т.к. редактирование (дополнение) ранее выпущенной статьи о настройке Exim будет не наглядным. Планируется периодическое дополнение и обновление данного материала, т.к. нет предела совершенству, со временем появляются новые идеи по усилению системы защиты почтового сервера. Многие приведенные в данной статье настройки Exim подходят без адаптации к конфигурации почтового сервера, описанной в статье о настройке почтовой системы. Описание системы защиты почтового сервера я начну с теоретической части, после чего перейду к технической с приведением конфигурационных директив Exim.

По моему мнению, система защиты почтового сервера, а также его пользователей, должна строиться на базе следующих основных принципов, увязанных единым замыслом:

  1. использование антивирусных программных средств;
  2. использование средств анализа сообщений (Rspamd, SpamAssassin и т.п.);
  3. запрет пересылки через сервер сообщений неизвестными хостами;
  4. никому нельзя доверять или выявление рассылки спама локальными пользователями;
  5. нельзя быть полностью в чем-то уверенным или использование в работе почтового сервера технологии, так называемых серых списков, на основе бальной системы;
    • проверка хостов, направляющих сообщения на наш сервер, на предмет соответствия требованиям RFC;
    • использование технологий, направленных на борьбу с рассылкой спама (SPF, DNSBL и т.п.);
    • использование спам-ловушек;
  6. блокирование попыток подбора паролей к почтовым ящикам пользователей;
  7. использование шифрованных соединений (везде где это возможно);
  8. использование криптографических алгоритмов с сильной криптографической стойкостью;
  9. использование актуальных версий программного обеспечения на почтовом сервере;
  10. постоянный мониторинг (контроль) работы почтового сервера.

По п.п. 1,2 думаю итак все понятно, поскольку зараженные вирусами компьютеры пользователей могут рассылать спам, как и в рассылаемом спаме могут содержаться ссылки на вредоносный код. Пункт 3, думаю, понятен и не нуждается в комментарии. Пункт 4 гласит, что необходимо предусмотреть защиту от негодяев, намеревающихся использовать учетную запись на сервере для рассылки спама. Пункт 5 гласит, что все проверки должны быть нацелены на выявление признаков рассылки спама, а не на констатацию фактов, поскольку полностью быть уверенным, что с хоста ведется только рассылка спама нельзя. Принцип под номером 6 гласит о том, что взлом учетной записи на сервере необходимо максимально усложнить. Пункты 7 и 8 логичны, поскольку в настоящее время передавать по открытым каналам связи чувствительную информацию совсем небезопасно. Также логичен пункт 9, поскольку в программном обеспечении часто находят уязвимости. Ну а следить за работой сервера, хотя бы периодически :) обязанность каждого администратора, о чем и гласит пункт 10 списка.

Следует отметить, что технология серых списков реализована мной следующим образом. Определение хоста (желающего отправить почту на наш сервер) как предполагаемого спамера происходит на основе баллов. Это значит, что практически все проверки в ACL не являются запрещающими, а накидывающими некоторое количество баллов при попадании под какое-нибудь правило. Хосты, набравшие сравнительно большое количество баллов, благополучно размещаются в локальном черном списке. Хосты, набравшие такое количество баллов, что их нельзя отнести ни к легитимным хостам, ни к рассылающим спам, заворачиваются в серый список на 29 минут. По прошествии 29 минут (большинство серверов настроены так, что повтор отправки писем осуществляется через 30 минут), если хост повторил передачу, то от него принимается письмо и отправитель заносится в белый список, точнее хэш сумма от отправителя и получателя. От остальных хостов почта принимается в обычном порядке. Чисткой устаревших записей в списках занимается скрипт по крону (его код здесь не будет приведен). Также отмечу, что с целью упрощения отладки правил в ACL и решения проблем, в случае их возникновения на этапе приема письма, в данной конфигурации я сохраняю информацию о проведенных проверках в ACL, которые не прошла отправляющая сторона (см. переменную acl_c_spamlog). Система баллов подобрана мной так, чтобы нарушители при провале важных на мой взгляд проверок попадали либо в серый список, либо в черный, за малозначимые косяки никаких санкций не применяется (при желании можно разработать другую систему баллов, установить новые пороги для попадания в серый и черный списки). Стоит отметить, что называемые мной здесь списки по сути являются обычными таблицами в базе данных, о чем подробно написано на первой странице статьи о настройке почтовой системы.

Итак, самое время, взглянуть на настройки Exim. Обращаю внимание, что далее приводятся отрывки конфигурационного файла Exim с директивами из разных его разделов, которыми реализованы выше указанные принципы. По ходу будут даны соответствующие пояснения.

######################################################################
#                    Определение макросов                            #
######################################################################

# Внешний IP адрес, на котором осуществляется прием почты
EXT_IP = ***.***.***.***

# Директория, в которой расположены дополнительная конфигурация
EXTRA_PREFIX = /usr/local/etc/exim/extra

# SQL запрос для включения в список хостов, с которых осуществлялись
# попытки подбора паролей к аккаунтам пользователей.
INSERT_CRACK_IP = \
 ${lookup pgsql{\
   INSERT INTO "crackhosts_tb"("ip", "description") VALUES (\
   '${quote_pgsql:$sender_host_address}',\
   'Cracking a user ${quote_pgsql:$acl_c_af_id}')\
  }{yes}{yes}}

INSERT_BF_IP = \
 ${lookup pgsql{\
   INSERT INTO "crackhosts_tb"("ip", "description") VALUES (\
   '${quote_pgsql:$sender_host_address}',\
   'Bruteforce passwords')\
 }{yes}{yes}}


####################################################################
#                    Основные параметры                            #
####################################################################

# Задаем новые списки. На эти списки можно ссылаться далее в
# конфигурационном файле, используя следующий синтаксис
# +blacklist, +badhosts, и +crackhosts.

# "Черный" список IP адресов хостов, которые попали сюда
# за нарушения множества правил RFC, т.е. провалили проверки в ACL
hostlist blacklist = ${lookup pgsql{SELECT "ip" FROM "blacklist_tb" \
          WHERE "ip" = '${quote_pgsql:$sender_host_address}'}}

# Список IP адресов хостов, с которых выявлены факты рассылки спама,
# однако с точки зрения RFC настроены правильно. Например, это 
# могут быть легитимные почтовые серверы, которые были хакнуты.
# Выявляются самостоятельно администратором почтового сервера и
# собственноручно добавляются в данный список по его усмотрению.
hostlist badhosts = ${lookup pgsql{SELECT "ip" FROM "badhosts_tb" \
          WHERE "ip" = '${quote_pgsql:$sender_host_address}'}}

# Список хостов, которые очень активно осуществляли подбор паролей
# к учетным записям пользователей
hostlist crackhosts = ${lookup pgsql{SELECT "ip" FROM "crackhosts_tb" \
          WHERE "ip" = '${quote_pgsql:$sender_host_address}'}}

# Объявляем ACL, которые необходимы для реализации поставленной задачи.
# Назначение каждой ACL будет понятно исходя из заданных в ней правил фильтрации.
# Дополнительно рекомендую почитать документацию Exim по данным ACL, чтобы
# понимать на каком этапе происходит срабатывание правил.
acl_not_smtp     = acl_check_not_smtp
acl_smtp_auth    = acl_check_auth
acl_smtp_connect = acl_check_connect
acl_smtp_mail    = acl_check_mail
acl_smtp_rcpt    = acl_check_rcpt
acl_smtp_predata = acl_check_predata
acl_smtp_dkim    = acl_check_dkim
acl_smtp_data    = acl_check_data
acl_smtp_notquit = acl_check_notquit
acl_smtp_quit    = acl_check_quit

# Указываем параметры для взаимодействия с антивирусным ПО. Я использую ClamAV.
# Смотрите документацию, чтобы узнать, как подключить другие антивирусы.
av_scanner = clamd:/var/run/clamav/clamd.sock

# Данная опция задает параметры подключения к средству анализа
# сообщений Rspamd
spamd_address = 127.0.0.1 11333 retry=3s variant=rspamd

# Указываем опции для библиотеки OpenSSL, которые будут использоваться
# при установке защищенного соединения с хостами. Задается как список,
# разделенные пробелами, где каждый элемент может быть добавлен "+added"
# или исключен "-substracted" из текущего набора опций.
# Внимание: для тех, у кого библиотека OpenSSL версии >= 1.0.0, желательно
# в целях повышения надежности задать опцию "+no_compression" ("CRIME" attack)
# (см. https://lists.exim.org/lurker/message/20121009.173420.ed5bd052.en.html)
# Также отключаем устаревшие протоколы шифрования
openssl_options = +no_sslv2 +no_sslv3 \
                 +no_compression

# Разрешаем только стойкие шифры (ciphers) для входящих SSL/TLS
# соединений (Exim должен быть скомпилирован с поддержкой OpenSSL. Для тех,
# кто использует GnuTLS параметр задается по-другому, см. документацию).
# Дополнительную информацию по шифрам можно получить по команде man ciphers.
tls_require_ciphers = HIGH:MEDIUM:!LOW:!ADH:!RC4:!MD5:!EXP:!aNULL:!eNULL:!NULL

# В данной переменной задается шаблон для заголовка "Received", который
# добавляется Exim в каждое сообщение. Данный шаблон раскрывается
# при каждом приеме сообщения. Если параметр установлен как пустой,
# тогда заголовок "Received" не будет добавляться в сообщения.
# P.S. шаблон практически идентичный стандартному, за исключением того,
# что в нем отсутствуют сведения о версии Exim.
received_header_text = Received: \
 ${if def:sender_rcvhost {from $sender_rcvhost\n\t}\
 {${if def:sender_ident \
 {from ${quote_local_part:$sender_ident} }}\
 ${if def:sender_helo_name {(helo=$sender_helo_name)\n\t}}}}\
 by $primary_hostname \
 ${if def:received_protocol {with $received_protocol}} \
 ${if def:tls_in_cipher {($tls_in_cipher)\n\t}}\
 ${if def:sender_address \
 {(envelope-from <$sender_address>)\n\t}}\
 id $message_exim_id\
 ${if def:received_for {\n\tfor $received_for}}

#####################################################################
#                        Параметры ACL                              #
#####################################################################


begin acl

acl_check_not_smtp:
 # Защищаемся от рассылки спама непосредственного с самого сервера,
 # для чего ограничиваем количество отправляемых писем. В данном случае
 # каждому пользователю допустимо слать не более 30 сообщений в час.
 deny message   = Your ratelimit of outgoing mail is very high ($sender_rate / $sender_rate_period)
      ratelimit = 30/1h/leaky/$local_part@$domain

 accept

acl_check_auth:
 # Блокируем команду AUTH для хостов, которые пойманы на попытках взлома.
 # Далее, в ACL acl_check_notquit, acl_check_quit будет видно, как
 # осуществляется наполнение данного списка.
 deny message = Your host is suspected of breaking
      hosts   = +crackhosts

 # Сохраняем использованные удаленным хостом данные авторизации.
 # Данная переменная используется в служебных целях для выявления
 # попыток подбора пароля к учетным записям удаленным хостом.
 # Отмечу, что я использую метод аутентификации PLAIN, причем
 # он разрешен только через шифрованное соединение.
 warn set acl_c_af_id = ${if match{$smtp_command_argument}\
                           {\N(?i)^(?:login|plain) (.+)$\N}\
                        {$1}}

 # Блокируем попытки авторизации (использования команды AUTH в
 # рамках SMTP сессии) с частотой более 3 раз в 1 минуту
 deny message     = The number of attempts of authorization is exceeded
      hosts       = !+relay_from_hosts
      ratelimit   = 3/1m/per_cmd/leaky/auth_${sender_host_address}
      delay       = 5s
      log_message = Ratelimit: $sender_rate/$sender_rate_period \
                      (max - $sender_rate_limit)

 accept

acl_check_connect:
 # Сохраняем в переменную содержимое обратной записи (PTR) для IP подключенного клиента
 # В дальнейшем данная переменная используется для проверки соответствия хоста RFC
 # Модификатор defer_never указан для того, чтобы Exim не делал повторных DNS запросов, иначе
 # в логах постоянно будет маячить ошибка Warning: ACL "warn" statement skipped: condition test deferred:
 # failed to expand ACL string "${lookup dnsdb{ptr=$sender_host_address}{$value}}": lookup of "ptr=***.***.***.***" gave DEFER:

 warn set acl_c_reverse_address = ${lookup dnsdb{defer_never,ptr=$sender_host_address}{$value}}

 accept

acl_check_mail:
 # Инициализируем переменную, в которой сохраняется общее количество спам баллов
 # Обратите внимание, что используется переменная типа acl_c, поскольку необходимо
 # сохранять значение в течение SMTP-сессии
 warn set acl_c_spamscore = 0

 # Блокируем хосты из локального черного списка
 deny message = Your IP address in local blacklist.
      hosts   = +blacklist

 # Как было сказано ранее, у меня есть список хостов, с которых
 # была выявлена рассылка спама, однако данные хосты настроены по RFC.
 # Такие хосты пройдут все проверки в ACL. Для того, чтобы ограничить
 # рассылку спама с данных хостов, накидываем им пограничное количество
 # баллов, чтобы они однозначно попали в серый список, а в случае
 # провала какой-либо проверки в ACL попали в черный список.
 warn hosts = +badhosts
      set acl_c_spamscore = ${eval:$acl_c_spamscore + 75}
      set acl_c_spamlog = $acl_c_spamlog From your IP address spam goes to my host. \
                             Please contact with postmaster if you consider that it not so.

 # Проверки HELO/EHLO
 #---------------------------------------------------------

 # Накидываем сверху баллов за неверный HELO/EHLO
 warn !authenticated = *
      hosts = !+relay_from_hosts
      condition = ${if and{\
                          {!match{$sender_helo_name}{\N(?i)^([a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?\.)+[a-z]{2,6}$\N}}\
                          {!eqi{$sender_helo_name}{[$sender_host_address]}}\
                       }\
                   }
      set acl_c_spamscore = ${eval:$acl_c_spamscore + 25}
      set acl_c_spamlog = $acl_c_spamlog Bad HELO/EHLO;

 # Накидываем баллы за использование в HELO/EHLO любых данных,
 # принадлежащих нашему серверу
 warn !authenticated = *
      hosts = !+relay_from_hosts
      set acl_m_islocal = ${lookup pgsql{SELECT "domainname" FROM "domains_tb" \
                             WHERE "domainname" = '${quote_pgsql:$sender_helo_name}'}{yes}{no}}
      condition = ${if or{\
                          {eq{$sender_helo_name}{EXT_IP}}\
                          {eq{$sender_helo_name}{[EXT_IP]}}\
                          {eqi{$sender_helo_name}{$primary_hostname}}\
                          {eq{$acl_m_islocal}{yes}}\
                         }\
                   }
      set acl_c_spamscore = ${eval:$acl_c_spamscore + 50}
      set acl_c_spamlog = $acl_c_spamlog Your HELO is one of local domain name;

 # Проверки DNS
 #---------------------------------------------------------

 # Добавляем баллов за то, что нет обратного адреса для данного IP в DNS
 warn !authenticated = *
      hosts = !+relay_from_hosts
      condition = ${if eq{$acl_c_reverse_address}{}}
      set acl_c_spamscore = ${eval:$acl_c_spamscore + 50}
      set acl_c_spamlog = $acl_c_spamlog PTR == NULL;

 # Добавляем еще баллов за то, что обратная DNS запись не совпадает с прямой.
 warn !authenticated = *
      hosts = !+relay_from_hosts
      condition = ${if !eqi{$acl_c_reverse_address}{$sender_helo_name}}
      set acl_c_spamscore = ${eval:$acl_c_spamscore + 25}
      set acl_c_spamlog = $acl_c_spamlog PTR != HELO;

 # Добавляем баллов за то, что IP хоста из диапазона динамических адресов
 # содержимое файла dynamic_pools можно увидеть в статье о настройке Exim, указанной в начале данного материала
 warn !authenticated = *
      hosts = !+relay_from_hosts
      condition = ${lookup{$acl_c_reverse_address}wildlsearch{EXTRA_PREFIX/dynamic_pools}{yes}{no}}
      set acl_c_spamscore = ${eval:$acl_c_spamscore + 50}
      set acl_c_spamlog = $acl_c_spamlog PTR in dynamic pools;

 # Проверки SPF записей (Exim должен быть скомпилирован с опцией SPF)
 #---------------------------------------------------------
 # Накидываем баллы за попытку отправить почту с сервера, не указанного в SPF
 warn !authenticated = *
      hosts = !+relay_from_hosts
      spf = fail : softfail
      set acl_c_spamscore = ${eval:$acl_c_spamscore + 50}
      set acl_c_spamlog = $acl_c_spamlog SPF fail;

 # За отсутствие записи SPF накидываем достаточное для попадания
 # в серый список количество баллов
 warn !authenticated = *
      hosts = !+relay_from_hosts
      spf = none
      set acl_c_spamscore = ${eval:$acl_c_spamscore + 25}
      set acl_c_spamlog = $acl_c_spamlog SPF none;

 # Накидываем немного баллов за некорректно оформленную SPF запись
 # или при возникновении ошибки во время её получения
 warn !authenticated = *
      hosts = !+relay_from_hosts
      spf = permerror : temperror : neutral
      set acl_c_spamscore = ${eval:$acl_c_spamscore + 25}
      set acl_c_spamlog = $acl_c_spamlog SPF syntax error or not received;

 # Проверка IP в черных списках. За каждое срабатывание правила
 # накидываем еще немного баллов. P.S. Периодически стоит проверять
 # работоспособность данных сайтов, поскольку иногда они перестают
 # работать и необходимо искать другие.
 #---------------------------------------------------------
 warn !authenticated = *
      hosts          = !+relay_from_hosts
      dnslists       = zen.spamhaus.org
      add_header     = X-Warning: $sender_host_address is in a black list at $dnslist_domain
      set acl_c_spamscore = ${eval:$acl_c_spamscore+25}
      set acl_c_spamlog = $acl_c_spamlog Blacklist zen.spamhaus.org;

 warn !authenticated = *
      hosts          = !+relay_from_hosts
      dnslists       = dnsbl.sorbs.net
      add_header     = X-Warning: $sender_host_address is in a black list at $dnslist_domain
      set acl_c_spamscore = ${eval:$acl_c_spamscore+25}
      set acl_c_spamlog = $acl_c_spamlog Blacklist $dnslist_domain;

 warn !authenticated = *
      hosts          = !+relay_from_hosts
      dnslists       = bl.spamcop.net:cbl.abuseat.org
      add_header     = X-Warning: $sender_host_address is in a black list at $dnslist_domain
      set acl_c_spamscore = ${eval:$acl_c_spamscore+25}
      set acl_c_spamlog = $acl_c_spamlog Blacklist $dnslist_domain

 # Проверяем авторизованного пользователя на предмет подмены адреса отправителя
 #---------------------------------------------------------
 deny message = Address ($sender_address) does not match with authenticated data ($authenticated_id). Check your email program settings.
      authenticated = *
      condition = ${if !eq{$sender_address}{$authenticated_id}{yes}{no}}

 accept

# Данная ACL используется для каждой команды RCPT при получении писем.
# Данную ACL я решил выложить полностью, т.к. важен порядок правил
acl_check_rcpt:
 # Принять, если отправитель - локальный хост (т.е. не через TCP/IP).
 accept hosts = :
        control = dkim_disable_verify

 ###################################################################
 # Следующая секция ACL проверяет локальную часть адреса на предмет
 # содержания символов [@%!/|.(точка)] в правильных местах.
 #
 # Символы кроме точек часто находятся не на своих местах, такое часто
 # делают люди, которые надеются обойти ограничения. Поэтому, несмотря
 # на то, что они допустимы в локальных частях, эти правила блокируют 
 # такие попытки.
 #
 # Пустые компоненты адреса (случай, когда в адресе стоят две точки
 # подряд) запрещены в RFC 2822, но Exim позволяет обойти такое
 # ограничение, потому что они встретились (х/з как тут перевести:
 # ....but Exim allows them because they have been encountered).
 # (Предполагается, что адрес имеет вид
 # "firstinitial.secondinitial.familyname", но что делать тем кто не имеет
 # "secondinitial"). Однако, локальная часть адреса, начинающаяся с
 # точки или содержащая /../ может доставить неприятности, если
 # используется как часть файла (например, для списка рассылки).
 # Такое же замечание справедливо и для локальных частей,
 # которые содержат наклонные черты. Символ переадресации
 # вывода (<, |, >) может также доставить проблемы, если локальная
 # часть легкомысленно включена в командную строку оболочки.
 #
 # В связи с этим для проверки используется два правила. Первое
 # используется для писем направленных для локальных доменов.
 # Строка "domains = +local_domains" реализовывает сказанное:
 # только локальные домены. Правило блокирует локальные части,
 # начинающиеся с точки или содержащие символы @ % ! / или |.
 # Если у вас есть локальные учетки имеющие в названии данные
 # символы, то вам необходимо модифицировать данное правило.

 deny    message       = Restricted characters in address
         domains       = +local_domains
         local_parts   = ^[.] : ^.*[@%!/|]

 # Второе правило применяется для остальных доменов и оно
 # не такое строгое как предыдущее.
 # Строка "domains = !+local_domains" указывает для каких доменов
 # применять правило. Данное правило позволяет локальным
 # пользователям отправлять письма во внешний мир, где адресаты
 # могут иметь косую или вертикальную черту в локальной части.
 # Так же правило блокирует адреса, локальная часть которых
 # начинается с точки, косой или вертикальной черты, но допускает
 # их использование в любом другом месте локальной части.
 # Локальная часть такого вида - /../ запрещена. Использование
 # символов @ % и ! запрещено, как и в предыдущем правиле.
 # Это сделано, чтобы локальные пользователи (или вирусы на их
 # компьютерах) не могли каким-либо образом осуществить
 # атаку на удаленный хост.

 deny    message       = Restricted characters in address
         domains       = !+local_domains
         local_parts   = ^[./|] : ^.*[@%!] : ^.*/\\.\\./
 ###################################################################

 # Добавляем баллов за то, что адрес отправителя совпадает с адресом получателя
 warn condition = ${if eqi{$sender_address}{$local_part@$domain}{yes}{no}}
      set acl_c_spamscore = ${eval:$acl_c_spamscore+25}
      set acl_c_spamlog = $acl_c_spamlog Sender == recipient;

 # Добавляем баллов за отправку письма на адрес-ловушку
 # P.S. В качестве адресов ловушек используются давно забытые заброшенные
 # адреса или специально созданные. То есть это обычный почтовый ящик.
 warn local_parts = spam : spamtrap
      domains     = +local_domains
      set acl_c_spamscore = ${eval:$acl_c_spamscore+50}
      set acl_c_spamlog = $acl_c_spamlog Spamtrap;

 # Заканчиваем проверки в данной ACL и отправляем на следующую ACL,ку клиентов,
 # набравших слишком много баллов для добавления их в локальный черный список
 # (в список blacklist). Предполагается, что более менее нормально настроенный
 # почтовый сервер не сможет набрать такое количество баллов, следовательно
 # скорее всего к нам подключился спамер.
 accept condition = ${if >={$acl_c_spamscore}{100}{yes}{no}}

 # Принимать письма для пользователя postmaster для любого локального
 # домена независимо от источника и без проверки отправителя.
 warn local_parts   = postmaster
      domains       = +local_domains
      set acl_c_spamscore = 0

 # Не даем использовать локальными хостами наш сервер для рассылки спама.
 # Правило не дает принимать письма с локальных хостов (с которых мы разрешили пересылку),
 # если не удалось проверить отправителя.
 deny !authenticated = *
      hosts = +relay_from_hosts
      !verify = sender

 # Добавляем баллов за невозможность проверки существования адреса отправителя
 # со всех других неизвестных нам хостов
 warn hosts = !+relay_from_hosts
      !verify = sender/callout=3m,defer_ok
      set acl_c_spamscore = ${eval:$acl_c_spamscore+25}
      set acl_c_spamlog = $acl_c_spamlog Callout error;

 # Проверять получателя во входящих письмах. Эта правило будет
 # проводить проверку локальной части для локальных доменов, а
 # а для удаленных проверку доменной части. Единственным способом
 # проверять локальную часть для удаленных доменов использовать
 # механизм обратных вызовов (добавить /callout), но сначала
 # прочитайте в документации про этот механизм. 
 require verify = recipient

 # Ограничиваем количество отправляемых всеми пользователями писем.
 # Для хостов (с которых мы разрешили пересылку) разрешаем отправлять
 # не более 30 писем в час. Авторизованным на нашем сервере
 # пользователям разрешаем отправлять 70 сообщений в час.
 deny message = Your ratelimit of outgoing mail is very high ($sender_rate / $sender_rate_period)
      !authenticated = *
      hosts = +relay_from_hosts
      ratelimit = 30/1h/leaky/$local_part@$domain

 deny message = Your ratelimit of outgoing mail is very high ($sender_rate / $sender_rate_period)
      authenticated = *
      ratelimit = 70/1h/leaky/$authenticated_id

 # Принимать письма, которые приходят с хостов, для которых этот хост
 # является релеем. Подразумевается, что эти хосты скорее всего MUA,
 # так что здесь установлен модификатор control=submission, который
 # заставляет Exim работать в режиме передачи. Это позволит подправить
 # некоторые ошибки в письме, например, нет заголовка Date. Если этот
 # хост является релеем для других MTA, то вам может понадобиться
 # отключить эту плюшку. Если вы хотите пересылать письма с MTA
 # и в "режиме передачи" с MUA, то вы должны разделить это правило
 # на два и обрабатывать такие письма отдельно. 
 accept  hosts         = +relay_from_hosts
         control       = submission
         control       = dkim_disable_verify

 # Принимать сообщение, если оно отправлено клиентом, прошедшим
 # аутентификацию.
 accept  authenticated = *
         control = submission
         control = dkim_disable_verify

 # Запрещаем пересылать письма через наш сервер неизвестным хостам.
 require message = relay not permitted
         domains = +local_domains : +relay_to_domains

 accept

acl_check_predata:
 # Запрещаем письма, отправленные нескольким адресатам от "пустого" отправителя.
 deny message = Sorry, sender address <> disallowed for many rcpt commands
      senders = :
      condition = ${if >{$rcpt_count}{1}{yes}{no}}


 # Можно писать в лог дополнительную ифномарцию о сообщениях, которые
 # набрали немного спам баллов, чтобы добавить еще какие-нибудь проверки,
 # если это сообщение все же окажется спамом.
 # warn condition = ${if <{$acl_c_spamscore}{50}{yes}{no}}
 #      condition = ${if >{$acl_c_spamscore}{0}{yes}{no}}
 #      logwrite  = Debug: $acl_c_spamlog

 # Принимаем сообщение от почтового сервера, который набрал мало спам баллов
 accept condition = ${if <{$acl_c_spamscore}{50}{yes}{no}}

 # Помучаем хост небольшой задержкой (многие хосты, рассылающие спам,
 # не выдерживают такого испытания временем)
 warn delay = 20s

 # Блочим хосты с большим количеством баллов и добавляем их
 # в локальный черный список.
 #---------------------------------------------------------
 deny message = Sorry, your spam score very high. Reasons: $acl_c_spamlog
      condition = ${if >={$acl_c_spamscore}{100}{yes}{no}}
      condition = ${lookup pgsql{\
                  DELETE FROM "blacklist_tb" WHERE "ip" = '$sender_host_address';\
                  INSERT INTO "blacklist_tb" VALUES ('$sender_host_address', DEFAULT, \
                    'Reason: ${quote_pgsql:$acl_c_spamlog}')}{yes}{yes}}

 # Реализация серого списка. Сюда попадают хосты, набравшие недостаточное
 # количество баллов для попадания в локальный черный список, но
 # превысившие максимальный порог для свободного прохождения письма.
 # Эти хосты нельзя отнести ни к легитимным ни к спам хостам, поэтому лучше
 # еще помучить их серым списком.
 #---------------------------------------------------------
 # Принимаем сообщение от хоста, который уже прощел проверку "серым списком"
 accept condition = ${lookup pgsql{\
                    SELECT "ip" FROM "whitelist_tb" WHERE "ip" = '$sender_host_address' \
                       AND "addrhash" = md5('$sender_address')\
                    }{yes}{no}}

 # Сообщаем отправителю, что пока он сидит в сером списке и ему еще надо подождать
 defer message = Message deferred. Your address already exists in Greylist. Try again later. Reasons: $acl_c_spamlog
       condition =  ${lookup pgsql{\
                     SELECT "ip" FROM "greylist_tb" WHERE "ip" = '$sender_host_address' \
                       AND "addrhash" = md5('$sender_address$local_part@$domain') \
                       AND "ctime" + 1740 > date_part('epoch'::text, now())\
                     }{yes}{no}}
       delay = ${eval:$acl_c_spamscore/3}s

 # Принимаем сообщение, если отправитель выждал необходимое время
 accept condition = ${lookup pgsql{\
                    SELECT "ip" FROM "greylist_tb" WHERE "ip" = '$sender_host_address' \
                      AND "addrhash" = md5('$sender_address$local_part@$domain') \
                      AND "ctime" + 1740 <= date_part('epoch'::text, now())\
                    }{yes}{no}}
        condition = ${lookup pgsql{\
                    DELETE FROM "greylist_tb" WHERE "ip" = '$sender_host_address' \
                      AND "addrhash" = md5('$sender_address$local_part@$domain'); \
                    INSERT INTO "whitelist_tb" VALUES('$sender_host_address', \
                      md5('$sender_address'), DEFAULT)\
                    }{yes}{yes}}

 # Добавляем отправителя в серый список
 defer message = Message deferred. Your address added to Greylist. Try again later. Reasons: $acl_c_spamlog
       condition = ${lookup pgsql{\
                   INSERT INTO "greylist_tb" VALUES('$sender_host_address',\
                     md5('$sender_address$local_part@$domain'), DEFAULT);\
                   }{yes}{yes}}
       delay = ${eval:$acl_c_spamscore/3}s

 deny

# Этот ACL используется после того, как получено тело письма. В этом ACL
# вы можете проверять тело письма или его заголовки, в частности здесь
# можно отправить тело письма на проверку антивирусом или спам сканером.
# Примеры некоторых тестов приведены ниже и закомментированы.
# Без этих тестов данная ACL принимает все сообщения. Если вы хотите
# использовать данные тесты, то Exim должен быть собран с
# соответствующими опциями (WITH_CONTENT_SCAN=yes in Local/Makefile).
acl_check_data:

 # Запрещаем прием и отправку сообщений, в которых имеются строки
 # с длиной более 1000 символов. По RFC любая строка в сообщении
 # не должна превышать 1000 символов с учетом символов перевода строки (CRLF).
 deny    message    = maximum allowed line length is 998 octets, \
                      got $max_received_linelength
         condition  = ${if > {$max_received_linelength}{998}}

 # Блочим письма с вирусами.
 deny message = This message contains a virus ($malware_name).
      malware = *

 # Пропускаем письмо через Rspamd. После проведенных проверок
 # в определенных переменных появится информация о количестве набранных баллов.
 warn spam = nobody:true

 # Блочить письма с неверным синтаксисом заголовков. При очень большом потоке
 # писем лучше отключить эту проверку.
 deny message = Invalid header syntax
      !verify = header_syntax

 # Иногда средства анализа сообщений (например SpamAssassin) присваивают
 # переменным отрицательные значения, что плохо сказывается при использовании
 # такого значения в вычислениях. Такое бывает, когда письмо проходит успешно
 # все проверки SpamAssassin.
 warn set acl_m_spam_score_int = $spam_score_int
      condition = ${if match{$spam_score_int}{\N^-\N}}
      set acl_m_spam_score_int = 0

 # Если в письме не найдены заголовки DKIM, тогда необходимо объявить
 # переменную, поскольку она используется далее в вычислениях
 warn condition = ${if !def:acl_m_dkim_score}
      set acl_m_dkim_score = 0

 # Добавляем информацию о проверках SPF в заголовки письма
 warn condition = ${if def:spf_received}
      add_header = :at_start:$spf_received

 # Подсчитываем общее количество спам баллов по результатам проверок
 # Rspamd и DKIM. Полученный результат можно использовать в почтовой программе
 # указав фильтр Sieve, который будет перемещать спам в соответствующую папку.
 # Письма набравшие 50 очков и более можно смело относить к спаму.
 warn add_header = X-Spamscore: $acl_c_spamscore\n\
                   X-Rspamd: ${eval:($acl_m_spam_score_int)+($acl_m_dkim_score)}\n\
                   X-Rspamd-action: $spam_action

 accept

# Здесь осуществляются проверки DKIM заголовков в письме. В зависимости от результата,
# переменной $acl_m_dkim_score присваивается конкретное количество баллов, которое
# в дальнейшем используется при подведении итогов в acl_check_data.
# P.S. Я решил не использовать проверки DKIM для определения относимости хоста к спамерам,
# так как данная проверка скорее позволяет определять относимость именно сообщения к спаму.
acl_check_dkim:
 # При отсутствии DKIM записей начинаем немного сомневаться в письме
 warn dkim_status = none
      add_header = :at_start:Authentication-Results: dkim=$dkim_verify_status (address=$sender_address domain=$dkim_cur_signer)
      set acl_m_dkim_score = 10

 # В случае сбоя при проверке DKIM записей начинаем сомневаться в письме
 warn dkim_status = fail
      add_header = :at_start:Authentication-Results: dkim=$dkim_verify_status (address=$sender_address domain=$dkim_cur_signer); $dkim_verify_reason.
      set acl_m_dkim_score = 15

 # В случае определения не соответствия DKIM записей начинаем сильно сомневаться в письме
 warn dkim_status = invalid
      add_header = :at_start:Authentication-Results: dkim=$dkim_verify_status (address=$sender_address domain=$dkim_cur_signer); $dkim_verify_reason.
      set acl_m_dkim_score = 20

 # В случае коректности DKIM записей в письме не сомневаемся
 warn dkim_status = pass
      add_header = :at_start:Authentication-Results: dkim=$dkim_verify_status, header.i=@$dkim_cur_signer
      set acl_m_dkim_score = 0

 accept

# Данная ACL срабатывает после завершения соединения с хостом (без команды QUIT,
# например при обрыве соединения)
acl_check_notquit:
 # Если хост во время нескольких подключений безуспешно пытался авторизоваться
 # под конкретной учетной записью, тогда можно судить о том, что с данного IP
 # ведется подбор паролей к учеткам на сервере. В данном случае в список взломщиков
 # попадают хосты, которые безуспещно авторизовывались под конкретной учетной
 # записью более 5 раз за 1 минуту. Также здесь учитываются данные $acl_c_af_id.
 # P.S. О безуспешной попытке авторизации в Exim можно судить по тому, что переменная
 # $sender_host_authenticated не инициализирована, а $authentication_failed установлена в "1"

 warn !authenticated = *
      hosts          = !+relay_from_hosts : !+crackhosts
      condition      = ${if and{\
                          {!def:sender_host_authenticated}\
                          {={$authentication_failed}{1}}\
                       }}
      condition      = ${if def:acl_c_af_id}
      set acl_c_afh  = ${sg{\
                          ${nhash_1024_1024:$acl_c_af_id}}{/}{_}\
                       }
      ratelimit      = 5/1m/per_cmd/strict/auth_${sender_host_address}_${acl_c_afh}
      condition      = INSERT_CRACK_IP
      log_message    = Bruteforce attack from $sender_host_address ($sender_helo_name)

 ### Добавлено в декабре 2019 года ###
 # Если хост пытался авторизоваться в течение часа более 25 раз под разными
 # учетными записями, тогда расцениваем это как осуществление подбора паролей 
 warn !authenticated = *
      hosts          = !+relay_from_hosts : !+crackhosts
      condition      = ${if and{\
                          {!def:sender_host_authenticated}\
                          {={$authentication_failed}{1}}\
                       }}
      ratelimit      = 25/1h/per_cmd/strict/auth_host_${sender_host_address}
      condition      = INSERT_BF_IP
      log_message    = Bruteforce attack from $sender_host_address ($sender_helo_name)

 accept

# Данная ACL срабатывает после завершения соединения с хостом (по команде QUIT)
acl_check_quit:
 # Если хост во время нескольких подключений безуспешно пытался авторизоваться
 # под конкретной учетной записью, тогда можно судить о том, что с данного IP
 # ведется подбор паролей к учеткам на сервере. В данном случае в список взломщиков
 # попадают хосты, которые безуспещно авторизовывались под конкретной учетной
 # записью более 5 раз за 1 минуту. Также здесь учитываются данные $acl_c_af_id.
 # P.S. О безуспешной попытке авторизации в Exim можно судить по тому, что переменная
 # $sender_host_authenticated не инициализирована, а $authentication_failed установлена в "1"

 warn !authenticated = *
      hosts          = !+relay_from_hosts : !+crackhosts
      condition      = ${if and{\
                          {!def:sender_host_authenticated}\
                          {={$authentication_failed}{1}}\
                       }}
      condition      = ${if def:acl_c_af_id}
      set acl_c_afh  = ${sg{\
                          ${nhash_1024_1024:$acl_c_af_id}}{/}{_}\
                       }
      ratelimit      = 5/1m/per_cmd/strict/auth_${sender_host_address}_${acl_c_afh}
      condition      = INSERT_CRACK_IP
      log_message    = Bruteforce attack from $sender_host_address ($sender_helo_name)

 ### Добавлено в декабре 2019 года ###
 # Если хост пытался авторизоваться в течение часа более 25 раз под разными
 # учетными записями, тогда расцениваем это как осуществление подбора паролей 
 warn !authenticated = *
      hosts          = !+relay_from_hosts : !+crackhosts
      condition      = ${if and{\
                          {!def:sender_host_authenticated}\
                          {={$authentication_failed}{1}}\
                       }}
      ratelimit      = 25/1h/per_cmd/strict/auth_host_${sender_host_address}
      condition      = INSERT_BF_IP
      log_message    = Bruteforce attack from $sender_host_address ($sender_helo_name)

 # You do not need to have a final accept, but if you do, you can use
 # a message modifier to specify custom text that is sent as part of
 # the 221 response to QUIT.
 accept

######################################################################
#                   Параметры аутентификации                         #
######################################################################

begin authenticators

# PLAIN метод. Клиент отправляет идентификатор сессии (который тут
# не используется), логин и пароль. После, доступ к логину и паролю
# можно получить через переменные $auth2 и $auth3 и проверить
# их корректность.

# Аутентификация пользователя проводится средствами Dovecot.
# Правило server_advertise_condition = ${if def:tls_cipher } запрещает
# пользователям проходить аутентификаю, если соединение не защищено.

PLAIN:
 driver                     = dovecot
 public_name                = PLAIN
 server_set_id              = $auth1
 server_socket              = /var/run/dovecot/auth-client
 server_advertise_condition = ${if def:tls_cipher }

Данные правила позволяют фильтровать большое количество спама. В связке с средством анализа сообщений Rspamd я уже практически и забыл что такое спам. Изредка конечно проскакивают сообщения с рекламой, но после скармливания их Rspamd, снова наступает мир и покой.

Добавить комментарий

CAPTCHA
Этот вопрос задается для того, чтобы выяснить, являетесь ли Вы человеком или представляете из себя автоматическую спам-рассылку.
Яндекс.Метрика