Оперативная реакция на DDoS-атаки

Один из ресурсов, за которым я присматриваю, вдруг стал неожиданно популярным как у хороших пользователей, так и у плохих. Мощное, в общем-то, железо перестало справляться с нагрузкой. Софт на сервере самый обычный — Linux,Nginx,PHP-FPM(+APC),MySQL, версии — самые последние. На сайтах крутится Drupal и phpBB. Оптимизация на уровне софта (memcached, индексы в базе, где их не хватало) чуть помогла, но кардинально проблему не решила. А проблема — большое количество запросов, к статике, динамике и особенно базе. Поставил следующие лимиты в Nginx:

на соединения
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn perip 100;

и скорость запросов на динамику (fastcgi_pass на php-fpm)
limit_req_zone $binary_remote_addr zone=dynamic:10m rate=2r/s;
limit_req zone=dynamic burst=10 nodelay;

Сильно полегчало, по логам видно, что в первую зону никто не попадает, зато вторая отрабатывает по полной.

Но плохиши продолжали долбить, и захотелось их отбрасывать раньше — на уровне фаервола, и надолго.

Сначала сам парсил логи, и особо настырных добавлял через iptables в баню. Потом парсил уже по крону каждые 5 минут. Пробовал fail2ban. Когда понял, что плохишей стало очень много, перенёс их в ipset ip hash.

Почти всё хорошо стало, но есть неприятные моменты:
— парсинг/сортировка логов тоже приличное (процессорное) время отнимает
— сервер тупит, если началась новая волна между соседними разборками (логов)

Нужно было придумать как быстро добавлять нарушителей в черный список. Сначала была идея написать/дописать модуль к Nginx + демон, который будет ipset-ы обновлять. Можно и без демона, но тогда придётся запускать Nginx от рута, что не есть красиво. Написать это реально, но понял, что нет столько времени. Ничего похожего не нашёл (может плохо искал?), и придумал вот такой алгоритм.

При привышении лимита, Nginx выбрасывает 503-юю ошибку Service Temporarily Unavailable. Вот я решил на неё и прицепиться!

Для каждого location создаём свою страничку с ошибкой
error_page 503 =429 @blacklist;

И соответствующий именованный location
location @blacklist {
    fastcgi_pass    localhost:1234;
    fastcgi_param   SCRIPT_FILENAME    /data/web/cgi/blacklist.sh;
    include         fastcgi_params;
}

Дальше интересней.
Нам нужна поддержка CGI-скриптов. Ставим, настраиваем, запускаем spawn-fcgi и fcgiwrap. У меня уже было готовое для collectd.

Сам CGI-скрипт
#!/bin/bash

BAN_TIME=5
DB_NAME="web_black_list"
SQLITE_DB="/data/web/cgi/${DB_NAME}.sqlite3"
CREATE_TABLE_SQL="\
CREATE TABLE $DB_NAME (\
    ip varchar(16) NOT NULL PRIMARY KEY,\
    added DATETIME NOT NULL DEFAULT (DATETIME()),\
    updated DATETIME NOT NULL DEFAULT (DATETIME()),\
    counter INTEGER NOT NULL DEFAULT 0
)"
ADD_ENTRY_SQL="INSERT OR IGNORE INTO $DB_NAME (ip) VALUES (\"$REMOTE_ADDR\")"
UPD_ENTRY_SQL="UPDATE $DB_NAME SET updated=DATETIME(), counter=(counter+1) WHERE ip=\"$REMOTE_ADDR\""
SQLITE_CMD="/usr/bin/sqlite3 $SQLITE_DB"
IPSET_CMD="/usr/sbin/ipset"

$IPSET_CMD add $DB_NAME $REMOTE_ADDR > /dev/null 2>&1

if [ ! -f $SQLITE_DB ]; then
     $SQLITE_CMD "$CREATE_TABLE_SQL"
fi

$SQLITE_CMD "$ADD_ENTRY_SQL"
$SQLITE_CMD "$UPD_ENTRY_SQL"

echo "Content-type: text/html"
echo ""

echo "<html>"
echo "<head><title>429 Too Many Requests</title></head>"
echo "<body bgcolor=\"white\">"
echo "<center><h1>429 Too Many Requests</h1></center>"
echo "<center><small><p>Your address ($REMOTE_ADDR) is blacklisted for $BAN_TIME minutes</p></small></center>"
echo "<hr><center>$SERVER_SOFTWARE</center>"
echo "</body>"
echo "</html>"

Собственно всё очевидно, кроме, разве что, SQLite. Я его добавил пока просто для статистики, но в принципе можно использовать и для удаления устаревших плохишей из черного списка. Время 5 минут пока тоже не используется.

Черный список создавался вот так
ipset create web_black_list hash:ip

Правило в iptables у каждого может быть своё, в зависимости от конфигурации и фантазии.

У одного хостера видел услугу управляемого фаервола. Заменив в скрипте ipset add не небольшую curl-сессию, можно фильтровать плохишей на внешнем фаерволе, разгрузив свой канал и сетевой интерфейс.

З.Ы.: Улыбнуло сообщение одного «хакера» на форуме, как быстро он положил сервер. Он и не догадывался, что это сервер положил на него.

Дополнения:
Спасибо товарищу megazubr за совет с использованием параметра timeout при создании черного списка — отпадает необходимость в его чистке по cron-у. Теперь команда по его созданию с таймаутом на 5 минут выглядит так:
ipset create web_black_list hash:ip timeout 300

Также благодарю alexkbs за наведение на мысли о безопасности. На рабочих серверах fastcgi-обработчик нужно вешать на unix-socket с правами только для nginx. В конфиге которого пишем:
error_page 503 =429 @blacklist;

location @blacklist {
    fastcgi_pass    unix:/var/run/blacklist-wrap.sock-1;
    fastcgi_param   SCRIPT_FILENAME    /data/web/cgi/blacklist.sh;
    include         fastcgi_params;
}
Для spawn-fcgi.wrap:
FCGI_SOCKET=/var/run/blacklist-wrap.sock
FCGI_PROGRAM=/usr/sbin/fcgiwrap
FCGI_EXTRA_OPTIONS="-M 0700 -U nginx -G nginx"
Метки:
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 50
  • +2
    Как планируется выявлять и отрабатывать false positive?
    • 0
      В базе есть поле counter — сколько раз IP-адрес добавляли в базу. По нему вполне можно определить плохиш это, или просто F5 залипло.
      Чего я точно ещё не сделал, так это механизм whitelist-а для поисковиков. В теории, они запросто могут давать подозрительную активность. Доверять спискам из интернета не стоит (?), да и Google не рекомендует так делать. Вместо этого предлагают проверять с помощью User-Agent-а и обратного разрешения IP-адреса. Всё это с лёгкостью можно сделать в скрипте.
    • +1
      Если не затруднит объясните в чем разница между

      limit_conn_zone $binary_remote_addr zone=perip:10m;
      limit_conn perip 100;
      

      и

      limit_req_zone $binary_remote_addr zone=dynamic:10m rate=2r/s;
      limit_req zone=dynamic burst=10 nodelay;
      

      Из доки, тоже как то не очевидно, в каких случаях что использовать.
      • +1
        limit_conn perip устанавливает предел количества установленных с одного IP соединений
        limit_req_zone — количество обращений с одного адреса, которое допустимо за секунду

        В рамках одного соединения может быть множество обращений. Соединение может длиться долго, а обращения могут происходить с разной интенсивностью.
        Вот здесь было разрешено 100 соединений с 1 IP (это важно для клиентов, которые за NAT), но при этом более 2 запросов в секунду нельзя формировать.

        • 0
          И к сожалению, при использовании SPDY, ограничить скорость обработки запросов нельзя, поэтому пришлось отключить. А жаль — технология реально ускоряющая загрузку страницы.
          • 0
            Если читать внимательнее, то там написано про limit_rate.
            • 0
              Упс, прошу прощения, это писатель доки ошибся, не туда ссылку поставил.
              • 0
                Скорость обработки запросов в SPDY-соединении не может быть ограничена.
                И ссылка на документацию ngx_http_limit_req_module.

                По-моему, однозначно написано. Да и логично, т.к. в SPDY отдельные запросы мультиплексируются в один пакет данных. И в теории, в одном HTTP-запросе может прийти вся страничка целиком — с текстом, картинками и скриптами.
                • +1
                  Эм… то что вы написали не имеет ничего общего с реальностью, даже в представлении о том, как работает SPDY.

                  На самом же деле все гораздо прозаичнее. Модуль limit_req работает совсем на другом уровне. Речь там о директиве limit_rate, ограничивающей bandwidth на ответ, просто потому что в nginx этот интерфейс реализован на уровне соединения, и им воспользоваться не получилось, нужно свой городить ещё выше.

                  В процессе редактирования доки кто-то неправильно понял оригинальный limitation, а я не вчитался и упустил этот момент. Исправим.
                  • 0
                    Да уж, иногда, оказывается, нужно читать не только доки, но и сам патч.
                    Спасибо!
          • 0
            limit_conn — ограничение на количество одновременно обрабатываемых запросов.
            limit_req — ограничение на частоту поступления/обработки запросов.
          • НЛО прилетело и опубликовало эту надпись здесь
            • 0
              C Cloudflare не доводилось работать, но был опыт с Amazon и Heroku. Ничего плохого не скажу, на всякий товар найдётся свой покупатель. Пока ещё dedicated-серверы могут конкурировать с «облаками» как по аптайму, так и по цене. Ну и философско-шкурный вопрос — если «не изобретать велосипед» и отдавать услугу (хостинг/администрирование) на откуп «дядькам с большими квадратными головами» (фраза знакомых из Intel-а), то самому придётся зарабатывать чем-то другим.
              из запретил доступ со всех IP, не принадлежащих им
              Вот этого не понял. Разъясни, пожалуйста.
              • +3
                Cloudflare — это не хостинг. Это CDN+Защита от DDoS + WAF. Причем базовый тариф бесплатен.
                • 0
                  В моём понимании CDN — это тоже хостинг, узко-специализированный. Про CloudFlare я очень высокого мнения, особенно после атаки на Спамхаус.
                  Бесплатный звучит заманчиво, возможно он даже очень хорош, но вот бесплатный Heroku вызывает только слёзы, а платный широко раскрывает глаза.
                  А вообще всё надо считать — у меня была ситуация, когда «CDN» из пяти «серверов» на Intel Atom оказался выгодней настоящих CDN-ов.
                  Т.е. если как-то можно грубо оценить нагрузку, то я беру dedicate, а если нагрузка может расти до облаков — то и хоститься там же.
                  • +1
                    Бесплатный тариф cloudflare с легкостью заменит решение, описанное в посте. И сделает это более продуманно, в том числе с автоматической отработкой ложных срабатываний, анализом и защитой на L7, ну и, собственно, распределением кеша контента по континентам.
                    Как академическое упражнение пост весьма полезен. Но на сегодняшний момент для большинства проектов мелкого и среднего уровня (да и крупного, чего уж там) гораздо выгоднее отдать эту тему на аутсорс таким конторам, как cloudflare, а усилия свои направить на улучшение/доработку функционала ресурса и наполнение его контентом.
            • НЛО прилетело и опубликовало эту надпись здесь
              • 0
                эм а разве без nginx нельзя ограничивать кол-во соединений с одного IP?
                примерно так:
                iptables -p tcp --dport 80 -m iplimit --iplimit-above 20 --iplimit-mask 24 -j REJECT
                еще есть такой модуль помоему:
                iptables -I INPUT 1 -p tcp --dport 80 -m string --string «GET / HTTP/1.0» --algo kmp -j DROP
                • 0
                  От DDoS-а это не спасаёт, так как атакующих много. Да и в пределах одного keep-alive соединения можно порядочно насолить.
                  А в случае с SPDY (VBart, поправьте меня, если ошибаюсь) соединение и так будет всего одно.
                  • 0
                    Это совершенно разного уровня ограничения. С помощью модуля limit_conn ограничивается не количество соединений, а количество одновременно обрабатываемых запросов (в документации это названо «активными соединениями»). Keep-alive соединений при этом может быть больше. А со SPDY может быть соединение одно, а параллельных запросов в нём много.
                  • 0
                    Пользуясь случаем хочу пропиарить решение знакомых людей, которое связыват лог апача с бан таблицами и мне очень неплохо помогало:
                    github.com/unicodefreak/log2ban

                    Для совсем тяжелых случаев (тысячи в секунду) не очень походит, для описанной ситуации более чем.
                    • +1
                      В посте упоминался fail2ban, который делает то же самое
                    • +1
                      Респект! Почерпнул оттуда идею — надо вместе с IP хранить в базе хэш URL-а, тогда плохишей будет легче отделять, они действительно ломятся (как правило) по одному адресу. А если начнут бегать по всему дереву форума? Решение хорошее, но всё равно парсер логов, от которого я пытался избавиться. Да и от логов тоже можно избавляться — нечего им убивать SSD-шку или отжирать память в tmpfs-е. В идеале — лог — это только средство отладки (ИМХО).
                      • 0
                        Не могли бы вы рассказать, почему не помог fail2ban, о принципах атаки ботов, как они его обходят в то же время загружая систему?
                        • 0
                          fail2ban помог, но я описал его недостатки — при высокой нагрузке на WEB-сервер, он уже сам по себе является тормозящим фактором (усиливающим эффект атаки?). Вот например, при ~800 запросах в секунду (это только на динамику), у нас лог вылетает со скоростью ~ 200кб/с
                          В данном случае это недостаток polling-архитектуры, логичнее и быстрее event/interrupt подход, что я и сделал.

                          Атаки, что я наблюдал у себя, были двух видов:
                          — не прекращающийся POST на регистрацию в форуме — вероятно спамеры подбирают логины
                          — ярко выраженный волнообразный наплыв однообразных запросов с нескольких сотен адресов
                        • 0
                          А по моему сейчас если можно защититься программными средствами от атаки уже ддосом не назвать… Вот если прийдет реальный syn-flood или udp flood то там дело до фаерволла или любого иного ПО даже не дойдет. Все остальное это в открытом доступе баловство с которым справляется и iptables и nginx.
                          • 0
                            Да, все так. Подобные методы работают только для пионер-атак, типа «уснул на кнопке F5» или «ололо, пацаны, я в первый раз скачал ab, такая крутая штука».
                            • 0
                              Увы, даже ab или whiletruedo программы это было популярно когда nginx еще и лимитить не умел ничего, сейчас в доступе уже вещи потяжелее, от которых Ваш хостер может в лучше случае перецепить адрес на nullroute. А что обиднее всего что это все еще и продавать умудряются, не говоря уж о полноценноп ботнете.
                              • 0
                                «вещи потяжелее» — это hping или Mausezahn, например? :)
                                • 0
                                  dns amplification штуковины, как пример вещей потяжелее.
                                  • 0
                                    dns amplification это не штуковины, это метод атаки.
                                    • 0
                                      а разве это дерево комментариев не с методов началось? :) методы и инструменты в руках скрипткидди — синонимы.
                                      • 0
                                        Вот как раз в руках скрипткидди есть только инструменты. Но при чем тут скрипткидди?
                            • +1
                              А по моему сейчас если можно защититься программными средствами от атаки
                              Cпорный вопрос — на многих аппаратных фаерволах установлены вполне себе программные операционные системы, и даже Linux. Так что это, ИМХО, вопрос терминологии.
                              уже ддосом не назвать…
                              Если атака с большого количества адресов приводит к деградации или недоступности сервиса — то это есть чистый DDoS, по определению.

                              С серьёзным SYN-флудом пока не сталкивался. Да и статья не про этот тип атаки.
                            • 0
                              Решение с CGI скриптом забавное, но я не понимаю, почему именно CGI? Мне кажется вы могли бы аналог этого bash скрипта на том же PHP написать и не извращаться с fcgiwrap
                              • 0
                                Во-первых, извращаться с fcgiwrap не пришлось — он у меня уже был настроен для collectd. Думаю что у многих так же.
                                Во-вторых, я тоже сразу подумал про PHP, но нужно, чтобы скрипт выполнялся от рута. А для этого пришлось бы подымать ещё один экземпляр php-fpm и запускать его от рута, что громоздко и не красиво.
                                • 0
                                  Дать пользователю, с которым выполняется php-fpm, права на sudo /usr/sbin/ipset add ... не думали?
                                  • 0
                                    Думал, но ИМХО, это потенциальная дырка в безопасности, ну и exec() нужно разрешать, что мне не очень нравится.
                                    • +1
                                      Мне кажется CGI-скрипт на bash, выполняющийся с высочайшими привилегиями, потенциально большая дыра в безопасности, чем очень ограниченное право на выполнение команды через sudo.
                                      • 0
                                        Придумать пример, как скомпрометировать сервер с разрёшенным ipset из PHP кода довольно несложно, особенно на хостинге с массой разношёрстных пользователей. А вот как это сделать с моим CGI скриптом я пока не нашёл. Поможете?
                                        • 0
                                          Например, есть некий пользователь samowar. Через sudoers мы даём ему такое право:

                                          samowar ALL = NOPASSWD: /usr/sbin/ipset add web_black_list *.*.*.* timeout 600

                                          Как пользователь samowar сможет скомпрометировать сервер если разрешена только такая команда? Расскажите, очень интересно.
                                          • 0
                                            Он может добавить в чёрный список адреса важных клиентов, таким образом лишив их доступа к сервису, может вырастить список до размеров, когда это станет проблемой для ядра (добавив туда весь Интернет (шутка).

                                            Кроме того, sudo у меня не используется, почти с таким же успехом можно поставить SUID на ipset.

                                            На продакшен серверах я использую spawn-fcgi, не через localhost:1234 как в примере (потому что это потенциально очень опасно), а через unix socket, на который выставлены права 700 для nginx:nginx. Сам CGI-скрипт тоже с такими правами.

                                            Таким образом, сделать что-то плохое, можно только скомпрометировав nginx. Я в него свято верю. Но вы правы — потенциально это тоже небезопасно. Какие могут быть ещё варианты?
                                            • 0
                                              Если злодей сможет через sudo добавить в блок-лист важных клиентов на 10 минут, то значит у него есть доступ к выполнению других произвольных команд или изменению кода, что в свою очередь означает что лишение доступа важных клиентов — лишь малая толика того, что этот злодей может сделать. Я бы даже сказал что вам очень повезет если злодей окажется настолько глуп что заблокирует важных клиентов, которые в свою очередь об это сообщат вам. Не о том беспокоиться надо.
                                              • +1
                                                Не совсем уловил идею, почему вы пишете «значит у него есть доступ к выполнению других произвольных команд или изменению кода». Если я вас правильно понял, вы предлагаете разрешить из PHP-кода выполнение ipset от рута. О каких других произвольных командах идёт речь?
                                                • 0
                                                  sudo вы будете разрешать выполнять через exec(), верно?
                                                  • 0
                                                    Допустим. Ну там ещё есть вагон функций, через которые можно запускать.
                              • +1
                                Собственно всё очевидно, кроме, разве что, SQLite. Я его добавил пока просто для статистики, но в принципе можно использовать и для удаления устаревших плохишей из черного списка. Время 5 минут пока тоже не используется.

                                Ipset позволяет задать как дефолтный timeout хранения записей в списке так и определять его индивидульно для каждой записи (для особо злостных адресов, например).
                                Очень удобно. Не надо городить механизм очисти списка.
                                • 0
                                  Согласен, для простой конфигурации — очень удобно. Но если надо выявлять злостных плохишей, то нужна статистика, а она в базе. Кроме того база пригодится для создания white-list-ов. Скорее всего оптимальныйм будет гибридный вариант: использовать таймаут, для того чтобы не тулить в cron скрипт по удалению старых записей, а базу использовать для сбора статистики и решения о том, на сколько банить очередного плохиша, т.е. первый раз засветился — на три минуты, второй раз — на 10 и т.п.

                                  P.S.: Очень люблю статистику, просто млею от rrd графиков.
                                  • 0
                                    Что в вашем понимании злостный плохиш? И причем тут статистика?
                                    ДДос они в африке ддос. Он может быть умным, может быть тупым. Но в любом случае, главным во время ддоса будет выживание ресурса, а не вычисление кто плохиш а кто чесный пионер.
                                    Если с адреса идёт 100500 реквестов в секунду то лучше его блокировать на неделю для профилатики и написать письмо владельцу адреса. Это в разы эфективнее.
                                  • 0
                                    Статистика как раз и нужна для того, чтобы узнать сколько было блокировок, и на основе этого задавать время очередной блокировки.
                                    Например, если мы знаем, что IP уже блокировался 20 раз, то при очередной попытке напакостить — баним на неделю, т.к. он — злостный плохиш.

                                    Про писать письмо — интересный вопрос. Когда мне валит спам спам с определённого IP — я нахожу владельца или провайдера (так как это относительно несложно), и как правило дело решается. Но когда твой сервер грузит некий IP из динамического DSL-пула где-то в Екатеринбурге, то я очень сомневаюсь, что смогу до кого-то достучаться. Ну и раз это DDoS, то таких адресов много, всем не напишешь.

                                    Пару IP я для любопытства отслеживал — они начинают и заканчивают отмечаться примерно в одно и то же время, очень похоже на домашний компьютер с трояном.

                                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.