DDoS атаки на 7 уровень (на уровень приложения) наиболее простой способ привести в нерабочее состояние сайт и навредить бизнесу. В отличие от атак на другие уровни, когда для отказа сайта необходимо организовать мощный поток сетевого трафика, атаки на 7 уровень могут проходить без превышения обычного уровня сетевого трафика. Как это происходит, и как от этого можно защищаться я рассмотрю в этом сообщении.

Атаки 7 уровня на сайты включают атаки на уровень веб-сервера (nginx, apache и т.д.) и атаки на уровень сервера приложений (php-fpm, nodejs и т.д.), который, как правило, расположен за проксирующим сервером (nginx, apache и т.д.). С точки зрения сетевых протоколов, оба варианта являются атакой на уровень приложения. Но нам, с практической точки зрения, нужно разделить эти два случая. Веб-сервер (nginx, apache и т.д.), как правило, самостоятельно отдает статические файлы (картинки, стили, скрипты), а запросы на получение динамического контента проксирует на сервер приложений (php-fpm, nodejs и т.д.). Именно эти запросы становятся мишенью для атак, так как в отличие от запросов статики, серверы приложений при генерировании динамического контента требуют на несколько порядков больше ограниченных системных ресурсов, чем и пользуются атакующие.

Как ни банально звучит, чтобы защититься от атаки, ее нужно сначала выявить. На самом деле, к отказу сайта могут привести не только DDoS атаки, но и другие причины, связанные с ошибками разработчиков и системных администраторов. Для удобства анализа, необходимо добавить в формат логов nginx (сорри, варианта с apache у меня нет) параметр $request_time, и логировать запросы к серверу приложений отдельный файл:

      log_format timed '$remote_addr - $remote_user [$time_local] '
            '$host:$server_port "$request" $status $body_bytes_sent '
            '"$http_referer" "$http_user_agent" ($request_time s.)';

      location /api/ {
           proxy_pass http://127.0.0.1:3000;
           proxy_http_version 1.1;
           proxy_set_header Upgrade $http_upgrade;
           proxy_set_header Connection 'upgrade';
           proxy_set_header Host $host;
           proxy_cache_bypass $http_upgrade;
           proxy_set_header X-Real-IP $remote_addr;
           proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
           proxy_set_header X-NginX-Proxy true;
           access_log /var/log/ngunx/application_access.log timed;
       }


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

Выявив атаку можно, приступать к защите.

Очень часто, системные администраторы пытаются защитить сайт, ограничив количество запросов с одного IP-адреса. Для этого используют 1) Директиву limit_req_zone nginx (см. документацию), 2) fail2ban и 3) iptables. Безусловно, эти способы нужно использовать. Однако, такой способ защиты уже как 10-15 лет является малоэффективным. На это есть две причины:

1) Трафик, который генерирует сеть ботов при атаке на 7 уровень, по своему объему может быть меньше чем трафик обычного посетителя сайта, так как у обычного посетителя сайта на один «тяжелый» запрос к серверу приложений (php-fpm, nodejs и т.д.) приходится примерно 100 «легких» запросов на загрузку статических файлов, которые отдаются веб-сервером (nginx, apache и т.д.). От таких запросов iptables не защищает, так как может ограничивать трафик только по количественным показателям, и не учитывает разделение запросов на статику и динамику.

2) Вторая причина — распределённость сети ботов (первая буква D в аббревиатуре DDoS). В атаке обычно принимает участие сеть из нескольких тысяч ботов. Они в состоянии делать запросы реже, чем обычный пользователь. Как правило, атакуя сайт, злоумышленник опытным путем вычисляет параметры limit_req_zone и fail2ban. И настраивает сеть ботов так, чтобы эта защита не срабатывала. Часто, системные администраторы начинают занижать эти параметры, отключая таким образом реальных клиентов, при этом без особого результата с точки зрения защиты от ботов.

Для успешной защиты сайта от DDoS, необходимо, чтобы на сервере были задействованы в комплексе все возможные средства защиты. В моем предыдущем сообщении на эту тему Защита от DDoS на уровне веб-сервера есть ссылки на материалы, как настроить iptables, и какие параметры ядра системы нужно привести в оптимальное значение (имеется в виду, прежде всего, количество открытых файлов и сокетов). Это является предпосылкой, необходимым, но не достаточным условием для защиты от ботов.

Кроме этого необходимо построить защиту, основанную на выявлении ботов. Все, что нужно для понимания механики выявления ботов, было подробно описано в исторической статье на Хабре Модуль nginx для борьбы с DDoS автора kyprizel, и реализована в библиотеке этого же автора testcookie-nginx-module

Это библиотека на С, и она продолжает развиваться небольшим сообществом авторов. Наверное, не все системные администраторы готовы на продакшин сервере прикомпилировать незнакомую библиотеку. Если же потребуется дополнительно внести изменения в работу библиотеки — то это и вовсе выходит за рамки рядового системного администратора или разработчика. К счастью, в настоящее время появились новые возможности: скриптовый язык Lua, который может работать на сервере nginx. Есть две популярные сборки nginx со встроенным скриптовым движком Lua: openresty, разработка которого изначально споснировалась Taobao, затем Cloudfare, и nginx-extras, который включен в состав некоторых дистирибутивов Linux, например Ubuntu. Оба варианта используют одни и те же библиотеки, поэтому не имеет большой разницы, который из них использовать.

Защита от ботов может быть основана на определении способности веб-клиента: 1) выполнять код JavaScript, 2) делать редиректы и 3) устанавливать cookie. Из всех этих способов, выполнение кода JavaScript оказалось наименее перспективным, и от него я отказался, так как код JavaScript не выполняется, если контент загружается фоновыми (ajax) запросами, а повторная загрузка страницы средствами JavaScript искажает статистику переходов на сайт (так как теряется загловок Referer). Таким образом, остаются редиректы которые устанавливают cookie, значения котрых подчиняются логике, которая не может быть воспроизведена на клиенте, и не допускают на сайт клиентов без этих cookie.

В своей работе я основывался на библиотеке leeyiw/ngx_lua_anticc, которая в настоящее время не развивается, и я продолжил доработки в своем форке apapacy/ngx_lua_anticc, так как работа оригинальной библиотеки не во всем устраивала.

Для работы счетчиков запросов в библиотеке используются таблицы памяти, которые поддерживают удобные для наращивания значения счетчиков методы incr, и установку значений с TTL. Например, в приведенном ниже фрагменте кода наращивается значение счетчика запросов с одного IP-адреса, если у клиента не установлены cookie с определенным именем. Если счетчик еще не был инициализирован, он инициализируется значением 1 с TTL 60 секунд. После превышения количества запросов 256 (за 60 секунд), клиент на сайт не допускается:

local anticc = ngx.shared.nla_anticc
local remote_id = ngx.var.remote_addr
if not cookies[config.cookie_name] then
  local count, err = anticc:incr(remote_id, 1)
  if not count then
    anticc:set(remote_id, 1, 60)
    count = 1
   end
   if count >= 256 then
     if count == 256 then
       ngx.log(ngx.WARN, "client banned by remote address")
     end
     ngx.exit(444)
     return
  end
end


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

local whitelist = ngx.shared.nla_whitelist
in_whitelist = whitelist:get(ngx.var.remote_addr)
if in_whitelist then
    return
end


Но не всегда это возможно. Одной из проблем является неопределенность с адресами ботов Google. Пропускать всех ботов подделывающихся под боты Google, равносильно снятию защиты с сайта. Поэтому, воспользуемся модулем resty.exec для выполнения команды host:

local exec = require 'resty.exec'
if ngx.re.find(headers["User-Agent"],config.google_bots , "ioj") then
    local prog = exec.new('/tmp/exec.sock')
    prog.argv = { 'host', ngx.var.remote_addr }
    local res, err = prog()
    if res and ngx.re.find(res.stdout, "google") then
	ngx.log(ngx.WARN, "ip " .. ngx.var.remote_addr .. " from " .. res.stdout .. " added to whitelist")
	whitelist:add(ngx.var.remote_addr, true)
        return
    end
    if res then
        ngx.log(ngx.WARN, "ip " .. ngx.var.remote_addr .. " from " .. res.stdout .. "not added to whitelist")
    else
        ngx.log(ngx.WARN, "lua-resty-exec error: " .. err)
    end
end


Как показывает опыт, такая стратегия защиты позволяет защитить сайт от оределенного класса атак, которые часто используются для в целях недобросовестной конкуренции. Понимание же механизмов атак и способов защиты помогает сэкономить массу времени на безуспешные попытки защититься fail2ban-ом, а при использовании сторонней защиты (например от Cloudfare), более осознанно выбирать параметры защиты.

apapacy@gmail.com
9 мая 2021 года