Сообщить об ошибке.

Защита приложения/сайта от DDoS атак

Защита сайта от атак при помощи Nginx + fail2ban

Конечно от серьезной DDoS (Distributed Denial of Service) атаки средствами сервера не защитишься, т.к. она имеет распределенный (Distributed) характер (выполняется одновременно с большого количества устройств, которые распределены географически) и в основном направлена не на сайт, а на инфраструктуру сайта, либо на канал, который обслуживает сайт. Другими словами, такая атака забивает канал бессмысленными запросами, в результате которых запросы от обычных легитимных пользователей не доходят до сайта в виду перегруженности канала. Защита от серьезных DDoS обычно построена на уровне железа (маршрутизаторов и фаерволов).

Этот материал будет посвящен борьбе с DoS (Denial of Service) атаками - отказ в обслуживании в результате частых запросов к сайту с нескольких IP-адресов. По сути это атака, которая перегружает систему ("доводит сервер до обморока с судорогами"), чтобы конечные легитимные потребители не могли пользоваться сервисом.

Такая атака может появиться в результате:

  • кто-то просто развлекается (например, тестирует сайт утилитой ab (ApacheBench) из apache2-utils);
  • ради "хакерской" практики (ищет уязвимости, посылая запросы напрямую к скриптам или файлам);
  • из за простой "обиды/зависти", что сайт в топе, а у обиженных конкурентов в ж...;
  • кто-то решил по быстренькому спарсить сайт (не хочет ставить sleep() между запросами);
  • на сайт пришел наглый ботнет, коих сейчас развелось очень большое количество.

Последствия:

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

Первое что нужно сделать:

  1. Отказаться от Windows Server. Практика подсказывает, что сайт, который работает на Windows, в случае даже простой DoS обречен. Причина неудачи кроется в сетевом стеке Windows: когда соединений становится очень много, то сервер непременно начинает плохо отвечать.
  2. Расстаться с Apache. Серверу Apache крайне тяжело отдавать файлы, и, что еще хуже, он на фундаментальном уровне уязвим для опаснейшей атаки Slowloris, позволяющей завалить сервер чуть ли не с мобильного телефона.

Отбиваемся от DDoS.

Что делать, если пришел DDoS, а сайт не защищен? Чаще всего сайты ложаться от наглых ботнетов. Традиционная техника самообороны - почитать лог-файл HTTP-сервера:

# выведет 10 IP, которые чаще всего читают сайт 
$ cat /var/log/nginx/your-site/access.log | cut -d ' ' -f 1 | logtop

Проверить каждый IP-адрес командой $ whois ip-address и если это не поисковый робот - смело баним через iptables. Эта методика сработает... если повезет зайти на сервер по SSH. Ботнеты бывают двух типов, оба опасны, но по-разному. Один приходит на сайт с нескольких IP и в несколько потоков начинает читать сайт, другой делает это постепенно. Первый убивает сервер сразу, зато в логах появляется весь и полностью, и если забанить все его IP-адреса, то вы - победитель. Второй ботнет укладывает сайт нежно и осторожно, но банить его придется, возможно, на протяжении нескольких дней.

Ниже описан вариант борьбы со всем этим безобразием при помощи связки Nginx + fail2ban (это не панацея, есть и другие способы).

Лимитируем соединения в Nginx (limit_conn и limit_req)

HTTP-сервер Nginx имеет возможность лимитировать количество выполняемых запросов и/или соединений к сайту в единицу времени с каждого IP-адреса.

  • Директива Nginx limit_conn_zone позволяет ограничить число соединений по заданному ключу, в частности, число соединений с каждого IP-адреса.
  • Директива Nginx limit_req_zone позволяет ограничить скорость обработки запросов по заданному ключу или, как частный случай, скорость обработки запросов, поступающих с каждого IP-адреса.

Чтобы предотвратить хаотичное и бесконтрольное чтение сайта ботами (или залетными краулерами, которые выдают себя за поисковых роботов) необходимо установить адекватные лимиты, чтобы под ограничения не попали легитимные пользователи. Ограничения необходимо выбирать, руководствуясь результатами нагрузочного и регрессионного тестирования, а также здравым смыслом.

Рассуждаем: в основном провайдеры домашнего интернета "выпускают" пользователей в глобальную сеть с IPv4 адресами из за NAT (IPv4-адресов на всех не хватает, для этого и придумали IPv6) + ко всему сайты в основном работают с IPv4. Такая же история и с мобильным интернетом. Следовательно количество подключений к сайту с одного IP- адреса может достигать от 5 до 10 (в зависимости от популярности сайта). Из этого можно сделать вывод, что количество запросов, поступающих с одного IP не может быть меньше количества подключений. Допустим, что сайт не такой популярный, с количеством от 15 до 30 тыс. уникальных посетителей в сутки и с интенсивностью кликов до 60 тыс. в сутки.

Принимая во внимание вышесказанное, сгодится конфигурация следующего вида:

# файл  /etc/nginx/sites-available/flask-app.ru

# определяем зоны и лимиты
limit_conn_zone $binary_remote_addr zone=connect:5m;
# выставляем интенсивность `rate` не более 3 запросов в секунду
# с одного IP, что бы не блокировать роботов yandex и google.
limit_req_zone $binary_remote_addr zone=requests:5m rate=3r/s;

client_max_body_size 5m;

server {
    listen 80;
    server_name www.flask-app.ru;
    return 301 $scheme://flask-app.ru$request_uri;
}

server {
    listen 80;
    server_name flask-app.ru;

    # журналы логов сервера, папку для логов
    # нужно предварительно создать командой 
    # `mkdip -p /var/log/nginx/flask-app`
    error_log /var/log/nginx/flask-app/error.log warn;
    access_log /var/log/nginx/flask-app/access.log main;

    # отдаем обслуживание статики NGINX
    # тем самым облегчаем работу Python 
    location ^~ /static/ {
        root /var/www/flask-app/www;
        expires 7d;
        add_header Cache-Control "public";
        access_log off;
        log_not_found off;
    }

    # обслуживание самого приложения Flask
    location / {
        include proxy_params;
        proxy_pass http://unix:/var/www/flask-app/logs/app.sock;

        # прописываем лимит одновременных подключений с одного IP-адреса
        # допускаем, что с одного IP могут подключиться до 5 пользователей
        limit_conn connect 5;
        # лимит количества запросов с одного IP прописан при создании зоны (см. выше),
        # следующая строка определяет какой может быть кратковременный всплеск 
        # запросов `burst` (не может быть меньше `limit_conn`) и как его обрабатывать.  
        # Ставить в очередь на  обработку (delay=число запросов) или отклонять (nodelay). 
        limit_req zone=requests burst=5 nodelay;
        # определяем HTTP ошибку 429 (Too Many Requests), которая будет 
        # показываться клиентам при нарушении лимитов, установленных выше. 
        limit_conn_status 429;
        limit_req_status 429;
    }

    # обрабатываем ошибку 429 (`limit_conn_status` и `limit_req_status`)
    # показываем пользовательскую страницу ошибки `429.html`
    error_page 429 /429.html;
    location = /429.html {
        # страницу `429.html` лежит в директории `static`
        root /var/www/flask-app/www/static;
        # в случае ошибки 429 посылаем заголовок (повторите запрос через 5 секунд)
        # "правильные" роботы, типа `yandex` и `google` его понимают. 
        add_header Retry-After: 5;
        internal;
    }
}

Основа конфигурации взята из материала "Разворачиваем Nginx + Gunicorn + Gevent + Flask на VDS".

На этом этапе приложение Flask уже защищено, т.к. запросы, превышающие установленные лимиты не доходят до WSGI сервера gunicorn и не обрабатываются приложением, а отбрасываются HTTP-сервером Nginx с выдачей ошибки 429 (Too Many Requests). Данная ошибка записывается в файл логов Nginx /var/log/nginx/flask-app/error.log (см. выше в конфигурации Nginx).

Лимиты, конечно, лучше подстраивать под собственный ресурс/сайт, анализируя какое-то время нагрузку на сервер и логи доступа к сайту. Наверное даже имеет смысл установить ограничения limit_conn и limit_req для location, в которых находятся дорогостоящие к выполнению скрипты (в примере лимиты установлены на все страницы сайта).

Обратите внимание на параметр 5m в при установки зон с лимитами, например, zone=connect:5m. Он означает, что на расчет данного лимита Nginx выделит словарь с буфером в 5 мегабайт и ни байтом более. В данной конфигурации это позволит отслеживать 160 000 TCP-сессий. Для оптимизации занимаемой памяти в качестве ключа в словаре используется переменная $binary_remote_addr, которая содержит IP-адрес пользователя в бинарном виде и занимает меньше памяти, чем обычная строковая переменная $remote_addr. Нужно заметить, что вторым параметром к директиве limit_req_zone может быть не только IP, но и любая другая переменная Nginx, доступная в данном контексте, например, в случае, когда необходимо обеспечить более щадящий режим для прокси, можно использовать сочетание переменных, которые индицируют пользователя $binary_remote_addr$http_user_agent или $binary_remote_addr$http_cookie_mycookie. Но использовать такие конструкции нужно с осторожностью, поскольку, в отличие от 32-битного $binary_remote_addr, эти переменные могут быть существенно большей длины и декларированные 5m могут скоропостижно закончиться.

Блокируем доступ к сайту настырных ботов с помощью Fail2Ban.

Представленными ниже настройками будем блокировать IP-адрес ботнетов (или еще кого-то), если этот кто-то получает HTTP-ошибку 429 с интенсивностью 30 штук в течении 1 минуты и при этом не может успокоится! Это может быть похоже на DoS.

Ставим Fail2Ban на сервер:

$ apt install fail2ban

Редактируем файл /etc/fail2ban/jail.d/defaults.conf (может быть другой), добавим строки в самом конце файла:

[nginx-limit-req]
enabled = true
filter  = nginx-limit-req
port    = http,https
logpath = /var/log/nginx/*error.log
logpath = /var/log/nginx/flask-app/error.log
maxretry = 30
findtime = 60
bantime = 30m

Описание параметров конфигурации:

  • enabled - включить или выключить правило;
  • logpath - путь к логам с ошибками;
  • filter - название фильтра (берется из каталога /etc/fail2ban/filter.d/)
  • maxretry - количество записей в логе об ошибке 429 для одного IP;
  • findtime - отрезок времени в секундах, в который должны появиться maxretry записей;
  • bantime - время блокировки ip, в минутах.

Перезагружаем Fail2Ban:

$ systemctl restart fail2ban

Что еще можно сделать? Например заблокировать пользователя, который ищет уязвимости приложения, путем прямого обращения к скриптам или определенным расширением файлов.

Блокируем IP, которые обращаются к скриптам.

Внимание! Этот фильтр может съедать много ресурсов сервера у популярных сайтов, у которых очень быстро растет лог доступа к сайту, в данном случае /var/log/nginx/flask-app/access.log, т.к. Fail2Ban постоянно будет его сканировать.

Создаем новый фильтр блокирующий IP-адреса пользователей/ботов, которые ищут "забытые администратором" файлы с расширениями .php, .asp, .pl, .cgi, .sql, .zip, .tar.gz, .7z, .rar.

Создаем файл фильтра nginx-noscript.conf в каталоге /etc/fail2ban/filter.d/:

$nano /etc/fail2ban/filter.d/nginx-noscript.conf

и добавляем в него следующую конфигурацию:

[Definition]
failregex = ^<HOST> -.*GET.*(\.php|\.asp|\.pl|\.cgi|\.zip|\.rar|\.7z|\.tar.gz)
ignoreregex =

Далее, редактируем файл /etc/fail2ban/jail.d/defaults.conf (может быть другой), добавим в конец файла:

[nginx-noscript]
enabled  = true
filter = nginx-noscript
port = http,https
logpath =/var/log/nginx/flask-app/access.log
bantime = 60m
maxretry = 6

Перезагружаем Fail2Ban:

$ systemctl restart fail2ban

На этом, в принципе, приложение Flask будет достаточно защищено. Далее хочется привести параметры, которые отвечают за производительность самого HTTP-сервера Nginx.

Тюнинг веб-сервера Nginx

Конечно, можно поставить Nginx с настройками по умолчанию и надеяться, что все будет хорошо. Однако хорошо всегда не бывает. Поэтому администратор любого сервера должен посвятить немало времени тонкой настройке и тюнингу Nginx.

Лимитируем ресурсы (размеры буферов) в Nginx.

Про что нужно помнить в первую очередь? Каждый ресурс имеет лимит. Прежде всего это касается оперативной памяти. Поэтому размеры заголовков и всех используемых буферов нужно ограничить адекватными значениями на клиента и на сервер целиком. Их обязательно нужно прописать в конфиге nginx.

  • client_header_buffer_size: Задает размер буфера для чтения заголовка запроса клиента. Если строка запроса или поле заголовка запроса не помещаются полностью в этот буфер, то выделяются буферы большего размера, задаваемые директивой large_client_header_buffers.
  • large_client_header_buffers: Задает максимальное число и размер буферов для чтения большого заголовка запроса клиента.
  • client_body_buffer_size: Задает размер буфера для чтения тела запроса клиента. Если тело запроса больше заданного буфера, то все тело запроса или только его часть записывается во временный файл.
  • client_max_body_size: Задает максимально допустимый размер тела запроса клиента, указываемый в поле "Content-Length" заголовка запроса. Если размер больше заданного, то клиенту возвращается ошибка 413 (Request Entity Too Large).

Настраиваем тайм-ауты в Nginx.

Ресурсом является и время. Поэтому следующим важным шагом должна стать установка всех тайм-аутов, которые опять же очень важно аккуратно прописать в настройках nginx.

  • reset_timedout_connection on;: Помогает бороться с сокетами, зависшими в фазе FIN-WAIT.
  • client_header_timeout: Задает тайм-аут при чтении заголовка запроса клиента.
  • client_body_timeout: Задает тайм-аут при чтении тела запроса клиента.
  • keepalive_timeout: Задает тайм-аут, в течение которого keep-alive соединение с клиентом не будет закрыто со стороны сервера. Многие боятся задавать keepalive_timeout крупные значения, но мы не уверены, что этот страх оправдан.
  • send_timeout: Задает тайм-аут при передаче ответа клиенту. Если по истечении этого времени клиент ничего не примет, соединение будет закрыто.

Сразу вопрос: какие параметры буферов и тайм-аутов правильные? Универсального рецепта тут нет, в каждой ситуации они свои. Но есть проверенный подход. Нужно выставить минимальные значения, при которых сайт остается в работоспособном состоянии (в мирное время), то есть страницы отдаются и запросы обрабатываются. Это определяется только тестированием, как с десктопов, так и с мобильных устройств. Алгоритм поиска значений каждого параметра (размера буфера или тайм-аута):

  1. Выставляем математически минимальное значение параметра.
  2. Запускаем прогон нагрузочных тестов через реальный браузер.
  3. Если весь функционал сайта работает без проблем - параметр определен. Если нет - увеличиваем значение параметра и переходим к пункту 2.
  4. Если значение параметра превысило даже значение по умолчанию, то это повод для обсуждения в команде разработчиков.

В ряде случаев ревизия данных параметров должна приводить к рефакторингу/редизайну сайта. Например, если сайт не работает без трехминутных AJAX long polling запросов, то нужно не тайм-аут повышать, а long polling заменять на что-то другое... Ботнет в 10 тысяч машин, висящий на запросах по три минуты, легко убьет среднестатистический дешевый сервер.

Тюним ядро Linux.

Обратите внимание на более продвинутые настройки сетевой части (ядра) опять же по тайм-аутам и памяти. Есть более важные и менее важные. В первую очередь надо обратить внимание на:

  • net.ipv4.tcp_fin_timeout: Время, которое сокет проведет в TCP-фазе FIN-WAIT-2 (ожидание FIN/ACK-сегмента).
  • net.ipv4.tcp_{,r,w}mem: Размер приемного буфера сокетов TCP. Три значения: минимум, значение по умолчанию и максимум.
  • net.core.{r,w}mem_max: То же самое для не TCP буферов.

При канале в 100 Мбит/с значения по умолчанию еще как-то годятся, но если в наличии хотя бы гигабит в cекунду, то лучше использовать что-то вроде:

sysctl -w net.core.rmem_max=8388608
sysctl -w net.core.wmem_max=8388608
sysctl -w net.ipv4.tcp_rmem='4096 87380 8388608'
sysctl -w net.ipv4.tcp_wmem='4096 65536 8388608'
sysctl -w net.ipv4.tcp_fin_timeout=10