Защита от DDoS Своими силами. Часть 5, Lua модуль для nginx.

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

Немного подготовки

Создадим папку проекта рядом с nginx:

mkdir /opt/lua_filter
cd /opt/lua_filter
mkdir modules

В modules будем размещать какие-либо общие модули, а в корень проекта основные скрипты.

Ставим lua-resty-cookie:

luarocks install lua-resty-cookie

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

ngx.header['Content-Type'] = 'text/html; charset=UTF-8'
ngx.say('<h1>TEST</h1>')
ngx.exit(200)

Добавляем конфиг для тестового скрипта: /opt/nginx/conf/clients/lua_info.conf

server {
  listen  localhost:8888;
    
  location / {
    content_by_lua_file '/opt/lua_filter/info.lua';
  }
}

Тестируем и перезагружаем nginx:

cd /opt/nginx/sbin/
./nginx -t
./nginx -s reload

Пробрасываем туннель:

ssh -L 8888:localhost:8888 IP_АДРЕС_ШЛЮЗА -l root

Открываем в браузере адрес http://127.0.0.1:8888/ и видим:

Тестовая страница

Значит всё ок. Можно приступать. И так как мне дико лень расписывать что и почему я пишу в самом модуле, давайте пропустим этот этап, и перейдём сразу к далее.

А далее нам нужно вернуться к конфигам.

В клиентский (default.conf) нужно добавить директиву "rewrite_by_lua_file '/opt/lua_filter/main.lua';" и теперь этот конфиг будет выглядеть:

server {
  server_name default:80;
  
  location / {
    rewrite_by_lua_file '/opt/lua_filter/main.lua';
    proxy_set_header HOST $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass http://ЛОКАЛЬНЫЙ_IP_ВЕБ_СЕРВЕРА:80;
  }
}

И в главный конфиг nginx нужно добавить директиву по подгрузке всех наших модулей "lua_package_path '/opt/lua_filter/modules/?.lua;;';" и выглядит он сейчас так:

#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;

events {
    worker_connections  1024;
}

http {
  gzip on;
  gzip_disable "msie6";
  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;
  keepalive_timeout 65;
  types_hash_max_size 2048;
 
  include /opt/nginx/conf/mime.types;
  default_type application/octet-stream;
  
  access_log /opt/nginx/logs/access.log;
  error_log /opt/nginx/logs/error.log;
  
  lua_package_path '/opt/lua_filter/modules/?.lua;;';
  
  include /opt/nginx/conf/clients/*;
}

Далее скачиваете готовый модуль и распаковываете в нашу папку "/opt/lua_filter". Рестартуете nginx и обновляете страницу. Ничего не должно измениться, но если откроете куки-инспектор, то увидите, что к документу прикреплена кука. Это значит что модуль работает.

Что именно он делает?

У модуля есть 3 варианты работы, которые задаются в конфиге, в переменной _config.lvl. При нулевом значении модуль не работает, а сразу пропускает запрос. Этот вариант нужен на мирное время.

При lvl=1, каждому новому запросу будет присваиваться специальная кука и затем клент будет редиректиться на этот же адрес. После редиректа проверяется, есть ли кука, и если есть, то всё ок, пропускаем дальше

При lvl=2, запросу будет отдаваться страница с js кодом, который будет устанавливать куку и редиректить по тому же адресу. И после редиректа, кука опят же проверяется.

Таким образом в мирное время, модуль можно отключить и он не будет мешать. В случае атаки простеньких ботов, включаем lvl1, а если боты научатся редиректам и кукам, включаем lvl2.

У нас нет никаких преимуществ перед testcookie, зачем оно надо?

А надо оно потому, что это по сути только заготовка под настоящий модуль. Lua очень простой и гибкий язык, в модуль можно встроить всё что угодно: статистику, поведенческий анализатор, отслеживание каждого конкретного коннекта и много чего ещё.

Но это ещё не конец, нам нужно добить конфиги

nginx.conf

#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;

events {
    worker_connections  1024;
}

http {
  limit_req_zone $binary_remote_addr zone=content:10m rate=1r/s;
  limit_req_zone $binary_remote_addr zone=static:10m rate=50r/s;
  limit_req_status 429;

  gzip on;
  gzip_disable "msie6";
  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;
  keepalive_timeout 10s;
  keepalive_disable msie6;
  send_timeout 10s;
  types_hash_max_size 2048;
  server_tokens off;
  
  include /opt/nginx/conf/mime.types;
  default_type application/octet-stream;

  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  ssl_prefer_server_ciphers on;
  
  access_log /opt/nginx/logs/access.log;
  error_log /opt/nginx/logs/error.log;
  
  lua_package_path '/opt/lua_filter/modules/?.lua;;';
  
  proxy_limit_rate 1m;
  
  include /opt/nginx/conf/clients/*;
}

И default.conf

server {
  server_name default:80;
  
  set $test_ip '10.129.28.157';
  
  client_max_body_size 512k;

  if ($http_user_agent ~* (WordPress|MyIE|ApacheBench|foobar)) {
    return 444;
  }
  
  if ($http_user_agent = "") { return 444; }
  
  location ~ /. {
    deny all;
    access_log off;
    log_not_found off;
  }
  
  location = /xmlrpc.php {
    deny all;
    access_log off;
    log_not_found off;
  }
  
  location ~* .(jpg|jpeg|gif|png|ico|bmp|js|css)$ {
    limit_req zone=static burst=2;
    rewrite_by_lua_file '/opt/lua_filter/main.lua';
    proxy_set_header HOST $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass http://$test_ip:80;
  }
  
  location ~* .(rar|zip|tar|gz)$ {
    limit_req zone=content burst=1;
    rewrite_by_lua_file '/opt/lua_filter/main.lua';
    proxy_set_header HOST $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass http://$test_ip:80;
  }
  
  location / {
    limit_req zone=content burst=2;
    rewrite_by_lua_file '/opt/lua_filter/main.lua';
    proxy_set_header HOST $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass http://$test_ip:80;
  }
}

Белые списки

Даже если сайт умирает под ддосом, мы должны пропускать поисковые системы без ограничений. Для этого есть "whiteList.lua". После внесения изменений, нужно будет перезапустить nginx.

Чёрные списки

И обратная ситуация, самых ръяных ботов нужно будет банить ещё на фаерволе, чтобы до nginx запрос даже не доходил. Делается это так:

ufw insert НОМЕР_ПРАВИЛА deny from IP_бота

Правило должно стоять выше, чем правила:

uwf правила

Иначе сработает оно, и бот спокойно пройдёт дальше.

Что дальше?

Дальше открывается огромная область для творчества: допиливание lua модуля, допиливание nginx конфигов, правил фаервола. Очередной поиск нового, более толстого канала связи, ресайз виртуалок...

Но есть пределы, за которые своими силами не перейти:

Канал связи. Самый толстый входящий канал, что мне удалось найти- у Linode, но и он всего 40Гбит/с, что в прочем ещё нужно проверить. Есть конечно амазон, с практически безграничными ресурсами, но и с совсем другим порядком цен.

Время. Атакующий всегда на шаг впереди. Он тратит пол часа, подправляет свои алгоритмы/методы и уходит бухать. А вы тратите часы/сутки, чтобы понять как это всё зафильтровать. Затем он возвращается, опять за пол часа меняет алгоритм и всё повторяется. Длительную атаку отбить в одиночку очень сложно.



Защита от DDoS Своими силами. Часть 4, настраиваем шлюз.

Мы приступаем к самому интересному, настраиваем anti ddos шлюз. Нашим главным инструментом будет nginx. Но в этом случае нельзя его просто взять и поставить из репозитория, придётся его собрать самим.

Правильные белые списки для google/yandex ботов

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


(0) Комментариев