Защита веб-сервера от простых атак

()

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

Сначала несколько случаев из личного опыта автора. Случай первый: шла долбёжка с одного адреса. Судя по всему работал скрипт, который в бесконечном цикле устанавливал tcp-соединение, запрашивал индексную страница сайта и рвал соединение.

Случай второй: так же с одного IP, так же судя по всему скрипт, устанавливал tcp-соединение и через секунду рвал.

Случай третий: чем-то напоминает первый, но после установки tcp-соединения посылался заголовок "Connection: Keep-Alive" и вместо одиночного запроса скрипт слал запросы по этому соединению в бесконечном цикле.

Во всех случаях скрипт был многопоточным и его целью было забить backlog на listen-сокете веб-сервера. Но в первом и третьем случае дополнительно страдали бэкенды, которые вынуждены были обрабатывать запросы (получать данные из кэша, бд, формировать страницу и т.д.).

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

Поскольку комплекс серверов был построен аналогично описанному ранее, то есть состоял из фронтенда и пачки бэкендов, то разумеется защищать достаточно было фронтенд. В описываемом случае на фронтенде стоит Nginx, а сам фронтенд работает под управлением Ubuntu 12.04 LTS.

Небольшая но очень важная ремарка: статика отдавалась отдельным сервером, поскольку на статический контент приходилось около 70% всех запросов. Если отдавать статический контент вместе с динамическим то описываемые далее способы защиты могут нарушить отдачу статики, ошибочно приняв такие запросы за атаку. Потому настоятельно рекомендуется вынести статику на отдельный сервер.

Начнём с мелочей (хотя если говорить о проектах с высокой посещаемостью - мелочей там нет). Например если на фронтенде у вас несколько IP-адресов (скажем 1.1.1.1, 1.1.1.2 и 1.1.1.3) то первым делом стоит поменять параметр listen в конфигурации nginx:

# Было:
#listen *:80;

# Стало:
listen 1.1.1.1:80;
listen 1.1.1.2:80;
listen 1.1.1.3:80;

Зачем? Да очень просто: в старом варианте создаётся один listen-сокет, а в новом - целых три. Поскольку backlog у сокета не резиновый, то надо либо делать несколько сокетов, либо увеличивать backlog. Но увеличение backlog не ускорит обработку очереди (к сожалению для каждого сокета она идёт в один поток), и клиенты из конца очереди могут не дождаться и отвалиться по тайм-ауту. Заодно такая настройка в целом положительно скажется на скорости обработки запросов клиентов, это особенно заметно на большом трафике.

Далее установим ограничения на число одновременных tcp-соединений и частоту установки новых.

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

Фрагмент обновлённой конфигурации скрипта iptables:

# Разрешаем http для google-бота
for net in 66.249.0.0/16 74.125.0.0/16 72.14.0.0/16 173.194.0.0/16; do
        iptables -A INPUT -i ${IF_EXT} -s ${net} -m tcp -p tcp --dport 80 -j ACCEPT
done

# Разрешаем устанавливать новые tcp-соединения на 80-й порт не чаще 20 раз в секунду
iptables -A INPUT -i ${IF_EXT} -m tcp -p tcp --dport 80 -m state --state NEW -m recent --name dpt80 --set
iptables -A INPUT -i ${IF_EXT} -m tcp -p tcp --dport 80 -m state --state NEW -m recent --name dpt80 --update --seconds 1 --hitcount 20 -j DROP

# Одновременно возможно не более 15 tcp-соединений на 80-й порт
iptables -A INPUT -i ${IF_EXT} -m tcp -p tcp --syn --dport 80 -m connlimit --connlimit-above 15 -j REJECT --reject-with tcp-reset

# А это правило уже было
# Оно разрешает весь tcp-трафик на 80-й порт, который не был срезан предыдущими правилами
iptables -A INPUT -i ${IF_EXT} -m tcp -p tcp --dport 80 -j ACCEPT

Хотя мы и рассматриваем защиту фронтенда на Linux, но всё-таки приведём аналогичные настройки и для pf на FreeBSD (фрагмент файла правил):

# Разрешаем трафик google-бота
pass in quick on $if_ext inet proto tcp from 66.249.0.0/16 to ($if_ext) port 80 flags S/SA keep state
pass in quick on $if_ext inet proto tcp from 74.125.0.0/16 to ($if_ext) port 80 flags S/SA keep state
pass in quick on $if_ext inet proto tcp from 72.14.0.0/16 to ($if_ext) port 80 flags S/SA keep state
pass in quick on $if_ext inet proto tcp from 173.194.0.0/16 to ($if_ext) port 80 flags S/SA keep state

# Разрешаем не более 15 одновременных tcp-соединений на 80-й порт и установку со скоростью не чаще 20 раз в секунду
pass in quick on $if_ext inet proto tcp from any to ($if_ext) port 80 flags S/SA keep state (max-src-conn 15, max-src-conn-rate 20/1)

# Старое правило. Оно больше не нужно.
#pass in quick on $if_ext inet proto tcp from any to ($if_ext) port 80 flags S/SA keep state

Переходим к настройкам nginx. Здесь мы будем использовать возможности модуля ngx_http_limit_req_module. Для начала опишем зону ограничений. Для этого создадим файл /etc/nginx/conf.d/req_zones.conf следующего содержания:

# Описываем зону с именем reqsglob и ограничением в 30 запросов в секунду
limit_req_zone $binary_remote_addr zone=reqsglob:16m rate=30r/s;

# Включаем ограничение
# В данном случае будет выполняться не более 30 запросов в секунду,
# Пропуская без задержек первые 9 запросов
limit_req zone=reqsglob burst=9 nodelay;

Если вы всё-таки решили отдавать статический контент с основного фронтенда то имеет смысл в настройках виртуальных хостов описать два location'а. Один для "/", а другой для статики и ограничение вписывать не в глобальную конфигурацию, а в "location /".

Как проверить что получилось? Ну конечно же зафлудить сайт запросами самостоятельно:) Сделать это можно несложным скриптом. Например вот таким:

#!/usr/bin/perl

use strict;
use warnings;
use diagnostics;

use IO::Socket::INET;

# IP-адрес вашего сервера
my $conf_ip = '127.0.0.1';
# Имя вашего домена
my $conf_host = 'www.example.com';

# Создаём 300 форков скрипта
my $i = 0;
for ($i = 0; $i < 299; $i++) {
        if (fork() == 0) {
                last;
        }
}

# В бесконечном цикле закидываем сайт запросами
while (1) {
        eval {
                my $sock = IO::Socket::INET->new(PeerAddr => $conf_ip,
                        PeerPort => '80',
                        Proto    => 'tcp',
                        Timeout => '3') or print "Fail\n";
                print $sock "GET / HTTP/1.1\nhost: $conf_host\n\n";
                close $sock;
        }
}

После запуска такого скрипта на своей машине вы вряд ли сможете зайти на тестируемый сайт с неё же. Потому либо запускайте скрипт на какой-то другой машине (с другим внешним адресом), либо найдите себе внешний прокси и попытайтесь зайти на сайт через него. Важно чтобы флуд шёл с одного адреса, а вы заходили с другого. Если зайти получилось - значит защита работает. Ни в коем случае не тестируйте чужие сайты: это уголовно-наказуемое деяние!

На этом всё. Приятной работы.

P.S. Эта статья ни в коем случае не претендует на звание серьёзного научного труда и охватывает лишь малую часть затронутой темы.

Ключевые слова: nginx, httplimitreqmodule, iptables, pf, limit_req, limit_req_zone.

Подписаться на обновления: RSS-лента Канал в TamTam Telegram канал Канал в ICQ

Комментарии:

Vadim Bazilevich 2013-01-31 19:23:16 (#)

Мне больше нравится решение на nginx.
Если коротко механизм следующий:
клиенту присваиваться кука (боты в большей своей части с куками работать не умеют)
далее идет проверка наличия присвоенной куки - куки нет - запрос заворачивается опять на присвоение куки. Кука есть - запрос пропускается дальше.
На таком механизме сервер держал вполне сносно 800мб ддос на гигабитном канале. Далее проверить не удалось - ситуация не понравилась хостеру и из-за отсутствия у них механизмов фильтрации сервер был забанен. Пришлось переехать к более устойчивому к ддосу хостеру.
Есть еще один вариант самообучаемого фильтра на базе ipset. Но из-за не очень высокой скорости реакции на большом ддосе его хорошо использовать совместно с предыдущим фильтром.
Фильтр на nginx разгружает сервер, второй фильтр банит плохишей.
Вышеуказанная настройка nginx - вообще должна быть сделана по умолчанию. Еще неплохо при установке последнего проверять наличие модуля geo. Не последняя вещь при защите сервера.
Новый комментарий

Жирный текстКурсивный текстПодчёркнутый текстЗачёркнутый текстПрограммный кодСсылкаИзображение




© 2006-2024 Вадим Калинников aka MooSE
Политика конфиденциальности