На сервере, где находятся сайты различных пользователей, чаще всего предусмотрена возможность отправки сообщений через локальный почтовый сервер. В случае взлома одного из сайтов существует возможность рассылки спам-сообщений, что может привести к серьезным последствиям, таким, как занесение IP-адреса почтового сервера в листы блокировки.

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

Проблема не является абсолютно тривиальной, и задачей данной статьи является демонстрация готового решения, которое может применяться в продакшене.

Исходные данные: ОС Debian 8 («Jessie»), почтовый сервер Postfix.

Установка и настройка пакета postfwd


Установим пакет postfwd при помощи apt:

apt-get install postfwd

Пакет не имеет файла конфигурации и не запускается по умолчанию. Создадим файл конфигурации /etc/postfix/postfwd.cf и добавим правило (отправка не более 50 сообщений в час для одного пользователя):

id=R001; action=rcpt(sender/50/3600/REJECT limit of 50 recipients per hour for sender $$sender exceeded)

Подробнее о конфигурации postfwd можно прочитать в документации на сайте проекта.

Отредактируем файл /etc/default/postfwd:

# Разрешить запуск демона
STARTUP=1
# Путь к файлу, где содержатся правила
CONF=/etc/postfix/postfwd.cf
# IP адрес, на котором демон будет слушать входящие сообщения
INET=127.0.0.1
# Порт, на котором демон будет слушать входящие сообщения
PORT=10040
# Пользователь, от которого работает демон
RUNAS="nobody"
# Аргументы, которые передаются демону при старте
ARGS="--summary=600 --cache=600 --cache-rdomain-only --cache-no-size"

Перезапустим сервис postfwd:

service postfwd restart

И проверим, что всё работает как надо:

srv1:~# tail /var/log/mail.log
Jun  9 14:14:18 srv1 postfwd2/master[37242]: postfwd2 1.35 starting
Jun  9 14:14:18 srv1 postfwd2/master[37244]: Started cache at pid 37245
Jun  9 14:14:18 srv1 postfwd2/master[37244]: Started server at pid 37246
Jun  9 14:14:18 srv1 postfwd2/cache[37245]: ready for input
Jun  9 14:14:18 srv1 postfwd2/policy[37246]: ready for input

Интеграция postfix и postfwd


Для того, чтобы выполнить интеграцию почтового сервера Postfix и службы postfwd, необходимо изменить конфигурационный файл /etc/postfix/main.cf, добавив в него следующие параметры:

# Подключаем наш policy service - postfwd и разрешаем отправку только авторизированным пользователям
smtpd_recipient_restrictions = check_policy_service inet:127.0.0.1:10040, permit_sasl_authenticated, reject_unauth_destination
# Подключаем наш policy service - postfwd
smtpd_end_of_data_restrictions = check_policy_service inet:127.0.0.1:10040

Подключение posftwd должно осуществляться в начале, это связанно с особенностью интерпретации параметров почтовым сервером Postfix. В случае, если в конфигурационном файле уже заданы параметры smtpd_recipient_restrictions и smtpd_end_of_data_restrictions, необходимо изменить их таким образом, чтобы check_policy_service inet:127.0.0.1:10040 и permit_sasl_authenticated размещались в начале.

Перезагружаем конфигурацию:

srv1:~# postfix reload
postfix/postfix-script: refreshing the Postfix mail system

После перезагрузки postfwd начинает свою работу и периодически выводит статистику в почтовый журнал (/var/log/mail.log):

srv1:~# tail /var/log/mail.log | grep postfwd
Jun  9 14:24:18 srv1 postfwd2/master[37244]: [STATS] postfwd2::policy 1.35: 1 requests since 0 days, 00:09:59 hours
Jun  9 14:24:18 srv1 postfwd2/master[37244]: [STATS] Requests: 0.10/min last, 0.10/min overall, 0.10/min top
Jun  9 14:24:18 srv1 postfwd2/master[37244]: [STATS] Dnsstats: 0.00/min last, 0.00/min overall, 0.00/min top
Jun  9 14:24:18 srv1 postfwd2/master[37244]: [STATS] Hitrates: 0.0% ruleset, 0.0% parent, 0.0% child, 0.0% rates
Jun  9 14:24:18 srv1 postfwd2/master[37244]: [STATS] Timeouts: 0.0% (0 of 0 dns queries)
Jun  9 14:24:18 srv1 postfwd2/master[37244]: [STATS]   1 matches for id:  R001

Теперь всем пользователям, отправляющим почтовые сообщения через SMTP сервер, будут установлены лимиты, соответствующие заданным в postfwd правилам (в нашем случае задано только одно правило). Однако все сообщения, отправляемые через /usr/sbin/sendmail, не будут подвергаться фильтрации, поскольку Postfix отправляет их напрямую в очередь, минуя postfwd.

Настройка «заглушки» для /usr/sbin/sendmail


Наиболее простым и эффективным решением является использование «заглушки» для скрипта /usr/sbin/sendmail, которая отправляла бы всю корреспонденцию через SMTP сервер с авторизацией. Соответственно, необходимо также запретить отправку сообщений через SMTP неавторизированным локальным пользователям. Проверяем, что в конфигурационном файле /etc/postfix/main.cf отсутствует параметр permit_mynetworks в smtpd_recipient_restrictions — в этом случае авторизация потребуется даже локальным пользователям.
Для того, чтобы применить решение с «заглушкой» на практике, необходимо создать на почтовом сервере ящики, которые будут соответствовать пользователям, а также ящик «по умолчанию», например:

  • site1@myserver.org
  • site2@myserver.org
  • default@myserver.org

Необходимо задать один пароль для всех ящиков (site1, site2 и т.д.), и другой пароль для default@myserver.org. Сохранять письма в эти ящики не нужно, так что можно настроить форвардинг в /dev/null.
Подобная конфигурация для приведенного выше правила postfwd разрешает отправку до 50 писем в час всем пользователям c «отдельным» аккаунтом, и отправку 50 сообщений «на всех» для тех пользователей, у которых нет отдельного аккаунта.
«Заглушка» будет работать следующим образом:

  1. Определять имя пользователя, который запустил наш скрипт
  2. Попытаться авторизироваться на почтовом сервере, используя полученное в п. 1 имя пользователя
  3. В случае ошибки авторзироваться как default
  4. Пересылать письмо

Код написан на Perl и требует установки дополнительных модулей, установим их через CPAN:

srv1:~# cpan Net::SMTP_auth Email::Address

Создадим дирректорию, в которой мы будем хранить скрипт (например, /usr/local/bin/private), а также непривилегированного пользователя, который станет «владельцем» дирректории и скрипта:

srv1:~# mkdir /usr/local/bin/private
srv1:~# useradd sendmail

Добавим в созданную директорию наш скрипт-«заглушку» (/usr/local/bin/private/sendmail.pl), заменив переменные $smtp_password, $smtp_default_password и $server соответственно на пароль пользовательских ящиков, пароль ящика «по умолчанию» и адрес вашего хоста, на котором созданы ящики:

#!/usr/bin/perl
use strict;
use warnings;
use Net::SMTP_auth;
use Email::Address;

my $user = getpwuid( $< );
my $smtp_password = 'password';
my $smtp_default_password = 'password';
my $server = 'srv1.re-hash.org';

my $input = '';
my $to_string = '';
foreach my $line ( <STDIN> ) {
  $input .= $line;
  if ($line =~ /^To:/) {
    $to_string = $line;
  }
}

my @addrs = Email::Address->parse($to_string);

if (0+@addrs eq 0) {
  die "No recipients";
}

my $rec = $addrs[0];
$rec =~ s/\@/\\@/;

my $smtp = Net::SMTP_auth->new('127.0.0.1', Port => 25, Timeout => 10, Debug => 0);
die "Could not connect to SMTP server!\n" unless $smtp;
if (!$smtp->auth('PLAIN', $user.'@'.$server, $smtp_password)) {
 $smtp->auth('PLAIN', 'default@'.$server, $smtp_default_password) or die "Auth failed!\n";
}
$smtp->mail($user.'\@'.$server);
$smtp->to($rec);
$smtp->data();
$smtp->datasend($input);
$smtp->dataend();
$smtp->quit;

Установим права на дирректорию и скрипт. Необходимо сделать так, чтобы пользователи могли выполнять скрипт, но не читать его:

srv1:~# chown -R sendmail:sendmail /usr/local/bin/private
srv1:~# chmod -R 4711 /usr/local/bin/private

Старый файл /usr/sbin/sendmail можно переименовать (например, в sendmail-postfix), или снять с него права на выполнение.
В принципе, теперь вместо /usr/sbin/sendmail можно использовать путь /usr/local/bin/private/sendmail.pl (или сразу сохранить скрипт как /usr/sbin/sendmail), но мне захотелось сделать иначе. Поэтому я решил написать еще один враппер (да, враппер поверх враппера), который заменит /usr/sbin/sendmail. Код враппера (wrapper.c) написан на C и выглядит следующим образом:

/*  wrapper.c  */
#define REAL_PATH "/usr/local/bin/private/sendmail.pl"
      main(ac, av)
          char **av;
      {
          execv(REAL_PATH, av);
      }

Выполним компиляцию этого файла, скопируем его в /usr/sbin и выставим соответствующие права:

srv1:~# cc -o sendmail wrapper.c 
srv1:~# cp -a ./sendmail /usr/sbin/sendmail
srv1:~# chown -R sendmail:sendmail /usr/sbin/sendmail
srv1:~# chmod -R 4711 /usr/sbin/sendmail

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

Исходный код враппера доступен в GitHub: github.com/xtremespb/sendmail-wrapper.
Буду рад замечаниям и предложениям по совершенствованию механизма лимитирования в комментариях.

Комментарии (12)


  1. F1RST
    10.06.2015 06:22

    Спасибо за статью, полезная.
    Около 2-3 месяцев назад была атака на хостинг. На некоторых сайтах использовали уязвимость и спам как раз сыпался через sendmail прямо в очередь. Пришлось извращаться со spamassassinon и скриптом в кроне, который мониторил очередь и убивал сообщения от определенных адресов. Костыль конечно корявый, но хотя бы закрыл поток спама и дал время для устранения дыры программистам.


  1. evg_krsk
    10.06.2015 11:00

    Проверяем, что в конфигурационном файле /etc/default/postfwd отсутствет параметр permit_mynetworks в smtpd_recipient_restrictions

    Наверное, имелся ввиду /etc/postfix/main.cf?


    1. xtremespb Автор
      10.06.2015 11:17

      Спасибо, поправил.


      1. evg_krsk
        10.06.2015 12:19

        Понятно, спелчекер

        отсутствет
        :-)


        1. xtremespb Автор
          10.06.2015 12:55

          Исправлено. Если можно, об опечатках пишите в личку.


  1. evg_krsk
    10.06.2015 11:12
    +1

    А так, до описания заглушки — ничего особенного, всё штатно. А дальше костыли, хардкод и уныние.


    1. xtremespb Автор
      10.06.2015 11:21
      +2

      Штатными средствами Postfix сделать подобное лимитирование в принципе нельзя.
      Если предложенную заглушку обозвать «костылем», менее полезной от этого она не становится ;-) Код на GitHub в помощь, никто не мешает сделать более гибкое решение с DBI и т.д.
      А вот когда из-за спамеров почтовый сервер оказывается DNSBL — тогда начинается уныние и хардкор.


  1. evg_krsk
    10.06.2015 14:18

    Что-то не понимаю, зачем делать обязательную SASL-аутентификацию для локальных пользователей. Что это даёт? Ведь она в любом случае пройдёт успешно. Так зачем жечь энергию и время?

    Не понял, зачем нужен setuid. Вроде бы он нигде не используется.

    Для остального более вменяемо выглядит mini_sendmail


    1. xtremespb Автор
      10.06.2015 14:49

      SASL-авторизация для локальных пользователей дает возможность установить необходимый лимит количества отправляемых сообщений для каждого пользователя, а также идентифицировать сообщения от пользователей, сайты которых работают под одним именем пользователя (например, www-data).
      mini_sendmail и подобные заглушки работают только с серверами, которые не требуют аутентификации от локальных пользователей. В этом случае можно лишь сделать общее правило для всех неавторизированных пользователей.
      SETUID нужен для того, чтобы скрипт заглушки был executable, но не readable — пользователи не смогут увидеть данные конфигурации, такие, как пароли.


    1. merlin-vrn
      10.06.2015 16:08

      То, что отправляется через sendmail с сервера, в почтовую очередь в итоге попадает через сервис Postfix cleanup. У него нет похожих фич, есть кое-какие проверки на спам, но ограничение количества писем в час через них не реализуешь.

      check_policy_service, вокруг которой в топике всё построено — фича сервиса smtpd в составе Postfix. Чтобы использовать эту фичу для локальных пользователей, необходимо, чтобы почта от них входила в систему через smtpd, то есть, через протокол smtp поверх tcp (или tcp6, если кому-то угодно).

      Но, при передаче локалхосту через tcp теряется информация о том, какому локальному пользователю принадлежит процесс, сделавший это. Эта информация нужна, чтобы знать, чьё мы письмо видим, надо ли его задержать или можно отправить, кому увеличить счётчик и так далее.
      Чтобы эту информацию иметь, придётся организовать для локальных процессов дополнительную аутентификацию, которая в протоколе ESMTP реализована через SASL.


      1. evg_krsk
        10.06.2015 18:03

        Виноват, сам мутно сформулировал вопрос. Это было к тому, что я вот смотрю на код и документашку mini_sendmail и там ясно видно, что он вполне себе сохраняет информацию о локальном пользователе (определяет имя через getlogin и, опционально, getpwuid) и выставляет её в виде MAIL FROM. Так что AUTH как бы и не нужен. Или я ошибаюсь и MAIL FROM недостаточно для postfwd и нужен обязательно AUTH?

        P.S.: просто ещё не смотрел доки на postfwd, а с mini_sendmail проще оказалось читать исходники.


        1. merlin-vrn
          11.06.2015 08:26

          Если его MAIL FROM нельзя подделать, то достаточно для аутентификации. Минус в том, что клиент не узнает, что письмо было задержано и насколько. (Если бы клиент сам говорил по SMTP с сервером, он узнал бы о задержке из 450-кода о временной проблеме и соответствующего комментария к нему.)