Конечно от серьезной DDoS (Distributed Denial of Service) атаки средствами сервера не защитишься, т.к. она имеет распределенный (Distributed) характер (выполняется одновременно с большого количества устройств, которые распределены географически) и в основном направлена не на сайт, а на инфраструктуру сайта, либо на канал, который обслуживает сайт. Другими словами, такая атака забивает канал бессмысленными запросами, в результате которых запросы от обычных легитимных пользователей не доходят до сайта в виду перегруженности канала. Защита от серьезных DDoS обычно построена на уровне железа (маршрутизаторов и фаерволов).
Этот материал будет посвящен борьбе с DoS (Denial of Service) атаками - отказ в обслуживании в результате частых запросов к сайту с нескольких IP-адресов. По сути это атака, которая перегружает систему ("доводит сервер до обморока с судорогами"), чтобы конечные легитимные потребители не могли пользоваться сервисом.
Такая атака может появиться в результате:
ab
(ApacheBench) из apache2-utils
);sleep()
между запросами);Последствия:
Первое что нужно сделать:
Что делать, если пришел 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 (это не панацея, есть и другие способы).
limit_conn
и limit_req
)HTTP-сервер Nginx имеет возможность лимитировать количество выполняемых запросов и/или соединений к сайту в единицу времени с каждого IP-адреса.
limit_conn_zone
позволяет ограничить число соединений по заданному ключу, в частности, число соединений с каждого IP-адреса. 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
Что еще можно сделать? Например заблокировать пользователя, который ищет уязвимости приложения, путем прямого обращения к скриптам или определенным расширением файлов.
Внимание! Этот фильтр может съедать много ресурсов сервера у популярных сайтов, у которых очень быстро растет лог доступа к сайту, в данном случае
/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.
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.
reset_timedout_connection on;
: Помогает бороться с сокетами, зависшими в фазе FIN-WAIT.client_header_timeout
: Задает тайм-аут при чтении заголовка запроса клиента.client_body_timeout
: Задает тайм-аут при чтении тела запроса клиента.keepalive_timeout
: Задает тайм-аут, в течение которого keep-alive соединение с клиентом не будет закрыто со стороны сервера. Многие боятся задавать keepalive_timeout
крупные значения, но мы не уверены, что этот страх оправдан. send_timeout
: Задает тайм-аут при передаче ответа клиенту. Если по истечении этого времени клиент ничего не примет, соединение будет закрыто.Сразу вопрос: какие параметры буферов и тайм-аутов правильные? Универсального рецепта тут нет, в каждой ситуации они свои. Но есть проверенный подход. Нужно выставить минимальные значения, при которых сайт остается в работоспособном состоянии (в мирное время), то есть страницы отдаются и запросы обрабатываются. Это определяется только тестированием, как с десктопов, так и с мобильных устройств. Алгоритм поиска значений каждого параметра (размера буфера или тайм-аута):
В ряде случаев ревизия данных параметров должна приводить к рефакторингу/редизайну сайта. Например, если сайт не работает без трехминутных AJAX long polling запросов, то нужно не тайм-аут повышать, а long polling заменять на что-то другое... Ботнет в 10 тысяч машин, висящий на запросах по три минуты, легко убьет среднестатистический дешевый сервер.
Обратите внимание на более продвинутые настройки сетевой части (ядра) опять же по тайм-аутам и памяти. Есть более важные и менее важные. В первую очередь надо обратить внимание на:
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