Стремительный поток изумрудных светящихся полос заливает тёмный дата-центр, разбиваясь и огибая светящийся дефлектор выше по потоку; лишь несколько чистых линий достигают матово-чёрного серверного блейда позади него
Руководство по сети

VPS в сети под DDoS

Три совершенно разные атаки прячутся под одним именем DDoS, и почти любой совет, который вам встретится, не срабатывает именно потому, что считает их одним и тем же: забитый аплинк лечат директивой nginx, а изощрённый флуд уровня приложений — сервером помощнее. Правильный вопрос — никогда не «как это заблокировать», а «на каком уровне это физически можно остановить и кому этот уровень принадлежит». Эта страница отвечает на него применительно к арендованному Linux-серверу: что гасит сеть над вами ещё до того, как вы что-то заметите, что реально помогает внутри самой машины, и почему самый эффективный ход обычно — вообще перестать быть адресуемым.

Защита от объёмных атак в этой сети включена всегда, и включать вам ничего не нужно: флуд ниже 10 Gbps гасится незаметно, 10–100 Gbps фильтруется на стороне транзитного провайдера, а всё, что выше, — анонсируется на выделенный путь скраббинга; пороги записаны в документации, а не оставлены маркетинговым прилагательным. Это честный предел того, что хостер способен вам дать. И это же, если говорить о трафике, который реально кладёт небольшие серверы, — менее интересная половина проблемы.

Потому что VPS за $5 надёжно убивают не монстры на 340 Gbps, о которых пишут в новостях. Его убивают 40 000 пакетов в секунду мелких SYN, заполняющих таблицу состояний, в которую вы никогда не заглядывали, или 300 запросов в секунду на единственный URL сайта, выполняющий запрос к базе данных, — трафик, который приходит на совершенно здоровый интерфейс, по каналу, далёкому от переполнения, и который ни один апстрим-скраббер не отличит от ваших пользователей. Это ваша забота, и решается она примерно двадцатью строками конфигурации — при условии, что вы сначала разобрались, с какой из трёх атак имеете дело.

Дальше предполагается Debian 13 или Ubuntu 24.04, nftables и nginx, а также то, что вы уже прошли харденинг первого часа, — сервер, который всё ещё отвечает на портах, которые он не обслуживает, защищать ещё рано.

Три атаки, одно имя

«DDoS» описывает намерение, а не механизм, а у механизмов почти нет ничего общего. Правильно их разделить — это не педантизм: это и есть вся суть работы, потому что каждая останавливается ровно на одном уровне и невидима на всех остальных.

Объёмные атаки нацелены на вашу полосу пропускания. UDP-амплификация — DNS, NTP, memcached, а в последнее время всё, что отвечает большим ответом на маленький запрос, — позволяет атакующему превратить свой 1 Gbps в 50 Gbps, направленные на вас. Цель — канал, а не сервер. Ваш процессор всё это время будет скучать.

Атаки на протокол и состояние нацелены на таблицу конечного размера у вас в ядре. SYN-флуд пытается исчерпать очередь accept; обобщённый флуд мелкими пакетами пытается исчерпать conntrack. Обе измеряются в пакетах в секунду, а не в битах в секунду, и обе способны убить машину на канале, загруженном всего на 3%. Именно этот класс атак кладёт небольшие серверы, и именно его пропускает большинство руководств.

Атаки уровня приложений нацелены на ваш процессор или базу данных и используют запросы, неотличимые от настоящих, потому что они и есть настоящие. Сто запросов в секунду на поисковый эндпоинт — ничто для сети и смерть для PHP-приложения. Ни один апстрим-скраббер не отфильтрует это за вас: снаружи это выглядит в точности как успех.

Есть и четвёртая вещь, которая маскируется под все три сразу и вообще не является атакой: ссылка, где-то «выстрелившая», собственный клиент с багом или невоспитанный краулер. Исключить это в первую очередь ничего не стоит, и до неловкого часто именно это оказывается ответом.

Часть, которую не исправить изнутри машины

Если на ваш адрес направлено 60 Gbps, а ваш порт — 1 Gbps, решение об этих пакетах принимается роутером выше вас, за несколько хопов до всего, чем вы администрируете. Ваш файрвол их никогда не увидит. И не может: их отбросили, чтобы защитить канал, который принадлежит не вам. Это самый важный структурный факт об объёмных атаках, и именно поэтому совет «укрепите файрвол против DDoS» — по большей части бессмыслица.

Поэтому единственные значимые вопросы — что ваш провайдер делает автоматически и где проходят его пороги. Наши не обещаны, а опубликованы: ниже 10 Gbps гасится без заметного эффекта, 10–100 Gbps фильтруется на транзитном провайдере, и вы можете заметить небольшой рост задержки, а выше 100 Gbps префикс анонсируется на выделенный путь скраббинга — задержка растёт заметнее, но сервис остаётся доступным. Null-route адреса рассматривается только как крайняя мера при устойчивых атаках, угрожающих всему PoP, и мы сообщаем об этом в течение нескольких минут, если это происходит. Покупать и включать тут нечего.

О скраббинге стоит знать две вещи, о которых поставщики обычно умалчивают. Первая — вас защищает не только собственная оборона, но и соседство: атака на клиента, делящего с вами вышестоящий /20, гасится ещё до того, как достигнет чьего-либо префикса, — флуд около 340 Gbps, направленный на соседний диапазон в Париже, прошёл без заметного влияния на наш, а это именно тот исход, который никто не замечает. Вторая — скрабберы работают на эвристиках, а эвристики иногда ошибаются: на этой же сети скраббер однажды девять минут слал TCP-resets по легитимным соединениям, пока фильтровал атаку на соседа. Оба случая занесены в публичный журнал инцидентов. Если ваши сессии обрываются похоже на активный reset, а не на таймаут, во время чужой атаки — это реальный сбой, и о нём стоит сообщить, а не час отлаживать это у себя.

Девяносто секунд измерений, прежде чем вы что-то поменяете

Любая неверная реакция на DDoS начинается с изменения, внесённого до того, как кто-либо понял, что вообще происходит. Сначала получите четыре числа. Они умещаются на одном экране и сами называют вам уровень атаки.

# 1. bits vs packets  — which axis is saturated?
sar -n DEV 1 5            # or: ifstat -i eth0 1

# 2. socket states     — SYN-RECV piling up means a SYN flood
ss -s
ss -tan state syn-recv | wc -l

# 3. kernel drop counters (these are the ones that matter)
nstat -az | grep -E 'ListenDrops|ListenOverflows|SyncookiesSent|TCPReqQFullDrop'
dmesg -T | tail -20       # look for: nf_conntrack: table full, dropping packet

# 4. connection tracking headroom
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max

Читайте их вместе. Высокий битрейт, низкий пакетрейт, низкая загрузка CPU — это объёмная атака, и ваша задача — подтвердить её и перестать что-либо печатать. Высокий пакетрейт при низком битрейте — то есть множество крошечных пакетов — это атака на состояние; сразу смотрите на SyncookiesSent и счётчик conntrack. Скромный трафик по обеим осям при загруженном под завязку CPU — это L7, и ответ находится в вашем веб-сервере и базе данных, а не в файрволе.

Стоит усвоить одну контринтуитивную вещь: во время настоящего объёмного флуда ваш интерфейс может выглядеть почти спокойно. Вы видите выживших — то, что поместилось в уже переполненный канал, или то, что пропустил апстрим-скраббер. Интерфейс, показывающий 900 Mbps на порту 1 Gbps, пока пользователи жалуются на полную недоступность, — это не довод против атаки. Это её форма.

Затем проверьте, что трафик не является просто настоящим. Запустите tail -f на access-логе на десять секунд: один и тот же путь, повторяемый тысячами разных адресов без referrer, — это атака; разброс обычных путей от обычных браузеров — это аудитория, и если вы примените к ней rate limit, вы сделаете за атакующего его работу.

Уменьшайте цель до атаки, а не во время неё

Каждый открытый порт — это очередь, которую атакующий может заполнить, а каждый пакет, который приходится рассматривать вашей гостевой системе, стоит вам CPU и записи в conntrack, даже если вы его дропаете. Самый дешёвый пакет — тот, который ваша машина вообще не получила, и именно поэтому фильтрация выше гостевой системы ценнее, чем фильтрация внутри неё.

Здесь два уровня, и это не одно и то же. Фильтр на границе — опциональный, настраивается отдельно для каждого сервера, отслеживает состояние соединений и запрещает всё по умолчанию, применяется на уровне гипервизора — трафик, отклонённый там, вообще не доходит до вашей виртуальной NIC, а значит, не стоит вам ни CPU, ни памяти, ни записи в таблице состояний. Файрвол внутри гостевой системы — ваш набор правил nftables — целиком ваш, и мы никогда его не трогаем. Схема, которая выживает при столкновении с атакой, — выразить грубую, стабильную истину четвёртого уровня на границе («эта машина обслуживает 80, 443 и SSH вот с этих адресов, и больше ничего не существует»), а гостевой набор правил оставить для тонкой настройки, которая меняется вместе с приложением. Оба уровня описаны в документации по файрволу.

Затем сузьте то, что осталось. SSH, ограниченный адресами, с которых вы реально администрируете сервер, — это не косметика харденинга, а способ полностью убрать с машины целый класс атак на исчерпание соединений. База данных, привязанная к 127.0.0.1 или адресу WireGuard, вообще не может быть зафлужена из интернета. А если у вас чисто внутренняя control plane, спрячьте её за интерфейсом WireGuard, а не за публичным портом с паролем.

SYN-флуд и очередь accept

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

У Linux есть чистое решение ещё с девяностых, и оно включено по умолчанию. SYN cookies позволяют ядру вообще перестать выделять память под полуоткрытые соединения: оно кодирует состояние соединения в номер последовательности, который отправляет в ответ, и восстанавливает его, если клиент завершает хендшейк. Проверьте это, а не считайте само собой разумеющимся, и дайте очередям достаточно места, чтобы пережить всплеск:

# /etc/sysctl.d/99-flood.conf
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 8192
net.core.somaxconn = 8192
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_abort_on_overflow = 0

sysctl --system

tcp_synack_retries = 2 значит больше, чем кажется: значение по умолчанию — пять — означает, что поддельный хендшейк занимает ядро около трёх минут, а двойка сокращает это примерно до семи секунд. Увеличение somaxconn — только половина дела: глубина очереди — это минимум из этого значения и того, что запросило приложение, так что nginx нужна директива listen 443 ssl backlog=8192; и reload, прежде чем настройка ядра хоть что-то значит. Это классический случай sysctl, который вроде бы применился и ничего не делает.

Если вы хотите останавливать флуд ещё до того, как он вообще доберётся до приложения, nftables может завершать хендшейки от имени ядра и передавать дальше только те соединения, которые оказались настоящими:

table inet filter {
  chain input {
    type filter hook input priority filter; policy drop;
    ct state established,related accept
    iif lo accept
    tcp dport { 80, 443 } ct state new synproxy mss 1460 wscale 7 timestamp sack-perm
    tcp dport { 80, 443 } accept
    ip protocol icmp icmp type echo-request limit rate 5/second accept
  }
}

Протестируйте synproxy на машине, к которой у вас всё ещё есть доступ через консоль, прежде чем на него полагаться: при неверной настройке это отличный способ отрезать файрволом самого себя от собственного сервера. Большинству людей достаточно блока sysctl выше, и честный порядок приоритетов такой: сначала syncookies, потом backlog, и только если вы измерили, что первых двух не хватило, — synproxy.

Таблица, на которую никто не смотрит, пока она не переполнится

Вот на чём попадаются даже опытные люди. Файрвол с отслеживанием состояния обязан помнить каждый увиденный поток, и эта память — nf_conntrack, хеш-таблица фиксированного размера, заданного при загрузке. Как только она заполняется, ядро дропает новые соединения — все подряд, и атакующего, и клиента, — и пишет одну-единственную строку в dmesg, которую никто не читает:

nf_conntrack: table full, dropping packet

Причина её эффективности — простая арифметика. Таблица по умолчанию на небольшом VPS вмещает где-то в районе десятков тысяч записей, и каждый пакет с нового адреса-источника создаёт новую запись — включая UDP-пакеты, включая пакеты, которые вы дропаете, включая сам флуд. Двадцать тысяч пакетов в секунду с подделанных адресов заполняют её меньше чем за две секунды, на канале, несущем всего несколько мегабит. Ваш график полосы пропускания не покажет вообще ничего.

# /etc/sysctl.d/99-conntrack.conf
net.netfilter.nf_conntrack_max = 262144
net.netfilter.nf_conntrack_buckets = 65536
net.netfilter.nf_conntrack_tcp_timeout_established = 3600
net.netfilter.nf_conntrack_tcp_timeout_syn_recv = 20
net.netfilter.nf_conntrack_udp_timeout = 20

Таймауты приносят больше пользы, чем размер таблицы. Время жизни установленного TCP-потока по умолчанию — пять дней, а значит, сервер, проработавший неделю, всё ещё держит состояние для соединений, закончившихся во вторник; часа хватает для всего, что не является простаивающей SSH-сессией, а net.ipv4.tcp_keepalive_time = 600 держит такие сессии живыми осознанно, а не по случайности. При расчёте размера закладывайте примерно 300 байт памяти ядра на запись: 262 144 записи — это около 80 МБ, что нормально на машине с 4 ГБ и совсем не нормально, если вы поставите десять миллионов, потому что так посоветовали на форуме.

Если вы гоняете что-то по-настоящему stateless и высоконагруженное — авторитетный DNS-сервер, публичный игровой сервер, узел Tor, — правильнее вообще отключить для него отслеживание. Connection tracking для сервиса без осмысленных соединений — это чистые издержки:

table ip raw {
  chain prerouting {
    type filter hook prerouting priority raw; policy accept;
    udp dport 51820 notrack
    tcp dport 9001 notrack
  }
}

Помните, что такой трафик после этого обходит ваши правила ct state established, так что ему нужны явные accept-правила в цепочке filter. Это и есть обмен: вы отказываетесь от отслеживания состояния для этого порта и получаете таблицу, которую невозможно заполнить.

L7: флуд, который выглядит точь-в-точь как ваши клиенты

Самая эффективная атака на небольшой сервер — это вообще не флуд. Это несколько сотен грамотно подобранных HTTP-запросов в секунду, каждый из которых абсолютно валиден и бьёт в тот самый единственный эндпоинт, который выполняет запрос без индекса или рендерит некешируемую страницу. Экономика здесь беспощадная: запрос стоит атакующему несколько сотен байт, а вам — 200 миллисекунд CPU и соединение с базой данных. Вы проигрываете эту гонку при любом объёме, который способны себе позволить обслуживать.

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

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

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=app:64m max_size=2g inactive=60m;

server {
  location / {
    proxy_cache app;
    proxy_cache_valid 200 301 302 10m;
    proxy_cache_lock on;              # one origin request per key, not a thousand
    proxy_cache_lock_timeout 5s;
    proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
    proxy_cache_background_update on; # serve stale, refresh behind it
    add_header X-Cache $upstream_cache_status always;
    proxy_pass http://127.0.0.1:8080;
  }
}

proxy_cache_lock и use_stale — это две строки, которые решают, переживёте вы всплеск трафика или нет. Без них в момент истечения горячего ключа каждый запрос «в полёте» превращается в запрос к origin-серверу — давка, которая превращает управляемую атаку в даунтайм. С ними бэкенд обслуживает один запрос на ключ за интервал, а все остальные получают слегка устаревшую страницу, и это правильный компромисс в любой ситуации, где альтернатива — вообще никакой страницы.

Затем отдельно ограничьте частоту запросов к дорогим путям — щедро и так, чтобы отказ был заметен:

limit_req_zone $binary_remote_addr zone=general:16m rate=20r/s;
limit_req_zone $binary_remote_addr zone=costly:16m  rate=2r/s;
limit_conn_zone $binary_remote_addr zone=conn:16m;
limit_req_status 429;
limit_conn_status 429;

server {
  limit_req  zone=general burst=40 nodelay;
  limit_conn conn 24;

  location /search { limit_req zone=costly burst=5 nodelay; proxy_pass http://127.0.0.1:8080; }
  location /login  { limit_req zone=costly burst=3 nodelay; proxy_pass http://127.0.0.1:8080; }
}

burst без nodelay ставит лишние запросы в очередь вместо того, чтобы их отклонять, а во время атаки это означает, что nginx вежливо держит для вас открытыми тысячи соединений — вы превратили флуд запросов в флуд соединений. Используйте nodelay, сразу возвращайте 429 и дайте клиенту самому с этим разбираться. И если у вас за спиной прокси, помните: $binary_remote_addr — это адрес самого прокси, если вы не настроили правильно set_real_ip_from и real_ip_header; лимит, привязанный к адресу вашего собственного фронтенда, при первом же срабатывании заблокирует сразу всех пользователей.

Атаки в стиле Slowloris — множество соединений, каждое из которых сочится запросом по байту за раз, — почти не беспокоят событийную модель nginx, но таймауты всё равно стоит подтянуть: client_header_timeout 8s; client_body_timeout 8s; send_timeout 10s; reset_timedout_connection on;. В сочетании с limit_conn это и есть вся защита.

Почему fail2ban — не инструмент против DDoS

fail2ban по-настоящему полезен, но нацелен на другую задачу. Он читает лог-файлы, принимает решение после N неудач за M минут и добавляет правило в файрвол. Каждая часть этого процесса — неподходящая форма для флуда.

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

Если вам нужна динамическая блокировка, делайте её в data plane, где поиск — это хеш, а истечение срока происходит автоматически. Динамический set в nftables работает за O(1) независимо от размера и сам себя забывает:

table inet filter {
  set flooders {
    type ipv4_addr
    flags dynamic, timeout
    timeout 10m
    size 65535
  }

  chain input {
    type filter hook input priority filter; policy drop;
    ct state established,related accept
    iif lo accept
    ip saddr @flooders drop
    tcp dport { 80, 443 } ct state new add @flooders { ip saddr limit rate over 50/second burst 100 packets } drop
    tcp dport { 80, 443 } accept
  }
}

Читайте это правило внимательно, потому что отрицание здесь многих сбивает с толку: адрес добавляется в set и дропается только тогда, когда скорость превышает лимит. Оставьте fail2ban для того, в чём он хорош, — медленный подбор паролей по SSH и формам входа приложений, где бан на человеческом масштабе времени по конкретному адресу — это именно то, что нужно.

Не становитесь оружием: амплификация, которую вы хостите

Каждую крупную объёмную атаку питают серверы, чьи владельцы даже не подозревали, что участвуют в ней. Механизм — это UDP-сервис, который отвечает на маленький поддельный запрос большим ответом: открытый DNS-резолвер возвращает в 50 раз больше, чем у него запросили, неправильно настроенные NTP и memcached ещё хуже, а кандидат — любой протокол, отвечающий раньше, чем успеет проверить, кто спрашивает.

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

# bind a recursive resolver to localhost, never 0.0.0.0
# unbound: interface: 127.0.0.1  +  access-control: 0.0.0.0/0 refuse

# what is actually listening on a public address?
ss -ulpn
ss -tlpn

# from another machine, confirm you do not answer strangers
dig @your.server.ip example.com +short          # must time out
ntpq -c rv your.server.ip                        # must fail

Если вы сознательно держите публичный UDP-сервис — игровой сервер, авторитетный nameserver, эндпоинт WireGuard, — правило то же, что и везде: ограничивайте частоту ответа, а не запроса. И bind, и knot реализуют response rate limiting; используйте его, потому что это и есть разница между обслуживанием своих пользователей и рассылкой чужой атаки.

Самая эффективная защита — не быть найденным

Всё, что было выше, — это устранение последствий атаки, которая уже нашла ваш адрес. Это стоит делать, но это лишь второе по эффективности решение. Origin, имя которого никто не знает, вообще нельзя атаковать на третьем или четвёртом уровне, и разделение адреса, который обслуживает трафик, и адреса, который знает весь мир, — самое действенное решение на этой странице.

Origin-адреса почти никогда не утекают из-за хитрых атак. Они утекают из-за истории и из-за небрежности, и список причин короткий и хорошо известный. Старые DNS-записи — самый обычный виновник: архивы passive DNS навсегда запоминают A-запись, которая была у вас до того, как вы поставили фронтенд. Логи certificate transparency публичны и постоянны, так что сертификат, выпущенный на хостнейм, который когда-то указывал прямо на origin, — это подписанная, помеченная временем запись о том, где вы раньше жили. Исходящая почта впечатывает адрес отправляющего сервера в заголовки каждого письма. И тихий вариант: ваш веб-сервер, отвечающий на голом IP, — из-за него любой, кто просканирует адресное пространство в поисках HTML вашего сайта, найдёт вас по содержимому за один вечер.

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

server {
  listen 80 default_server;
  listen 443 ssl default_server;
  ssl_reject_handshake on;      # nginx 1.19.4+: no certificate, no fingerprint
  return 444;                   # close without a response
}

Затем выберите фронт. Коммерческий CDN — очевидный ответ, и у него есть реальная цена, которая здесь важна: вы добавляете компанию, которая терминирует ваш TLS, видит ваш plaintext, знает ваш origin и может получить судебный запрос в юрисдикции, которую вы не выбирали, — это перечёркивает добрую часть смысла платить за сервер криптовалютой без привязки к имени. Если вы всё же идёте этим путём, учтите: origin никогда не должен был быть публичным, и вся схема ломается в момент, когда кто-то находит старую запись.

Альтернатива, сохраняющая то свойство, за которое вы платили, — быть собственным фронтом. Инстанс за $5 во втором регионе, на котором nginx работает как reverse proxy, а фильтр на границе у origin принимает трафик только с этого одного адреса и ни с какого другого, — даёт вам расходуемый адрес, который можно поменять за пять минут, и control plane, отвечающий только вам. Это та же архитектура, только без третьей стороны. А там, где аудитория готова этим пользоваться, onion-сервис Tor вообще убирает IP из уравнения — флудить там нечего, хотя у onion-сервисов есть своя поверхность атаки на introduction points, и именно поэтому современный Tor поставляется с защитой на основе proof-of-work именно для этого случая.

Блокировка стран, ASN и формы трафика

Рано или поздно кто-нибудь предложит заблокировать целую страну. Это грубо, изредка правильно и по большей части — способ создать видимость бурной деятельности.

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

Блокировка по ASN точнее. Атакующий трафик, исходящий от горстки хостинг-провайдеров — дешёвых диапазонов VPS, арендуемых почасово, — можно дропать по префиксу с намного меньшими потерями, чем при блокировке страны, потому что обычные пользователи не сёрфят из дата-центра. Диапазоны дата-центров, что удобно, — это ещё и источник скрейпинга и credential stuffing.

Но по-настоящему устойчивая версия этой идеи — вовсе не география: это фильтрация по форме трафика, а не по его происхождению. У атаки обычно есть что-то общее структурно — один и тот же user agent, один и тот же отсутствующий заголовок, один и тот же URL с одним и тем же параметром запроса, один и тот же TLS-отпечаток, полное отсутствие второго запроса, который настоящий браузер делает всегда. Найдите общее свойство — и вы напишете одно правило, которое ничего не стоит и переживёт смену адресов атакующим, а он сменит их быстрее, чем вы успеете их перечислить.

# in an attack: what do these requests have in common?
awk '{print $1}'  /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20   # addresses
awk -F'"' '{print $6}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20   # user agents
awk '{print $7}'  /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20   # paths

Если на один user agent приходится 90% запросов, вы в девяноста секундах от решения. Если на каждый из топ-двадцати адресов приходится по 2% трафика, перестаньте искать адреса — это распределённая атака, и ответ на неё — кеширование и rate limit.

Когда адрес сгорел

Иногда атака целенаправленная, устойчивая и направлена лично на вас, а не на случайный адрес, и правильный ответ — перестать защищать адрес и бросить его. Это не поражение; для небольшого сервиса это часто самый дешёвый из возможных исходов, и болезненным он бывает только тогда, когда вы это не отрепетировали.

То, что делает переезд быстрым, решается заранее. Держите TTL DNS-записей на уровне 300 секунд постоянно — цена этого ничтожна, а выгода в том, что переехать можно за пять минут, а не за день. Держите развёртывание воспроизводимым, потому что сервер, который можно пересобрать, — это сервер, который можно перенести; если пересборка означает «вспомнить, что вы делали в марте», у вас не план, а заложник. Держите восстанавливаемый бэкап в другом регионе и знайте по результатам настоящей тренировки, сколько времени занимает восстановление.

Тогда сам переезд короток: разверните сервер в другом регионе, восстановитесь, проверьте новый адрес через подмену хостнейма ещё до того, как тронете DNS, переключите запись и подержите старый сервер работающим достаточно долго, чтобы его догнали последние резолверы. Руководство по миграции подробно разбирает последовательность действий, и она одна и та же — переезжаете вы ради производительности или потому, что кто-то на вас разозлился.

Модель оплаты делает репетицию практически бесплатной: списания идут посуточно с баланса, так что вторая машина, которую вы развернули, протестировали и уничтожили за один вечер, обойдётся в центы, — при этом нет ни контракта, ни карты, ни продления, которое нужно отменять. Нет никакой причины, чтобы ваша первая смена адреса случилась именно во время атаки.

Чего всё это не даёт

Два честных финальных замечания, потому что весь остальной текст этой страницы оптимистичен по построению.

Первое — ни один хостер не гасит абсолютно всё, и любой провайдер, утверждающий обратное, просто продаёт. Ёмкость конечна, ёмкость скраббинга — ещё конечнее, и при достаточно большом масштабе экономически рациональное действие для сети, защищающей тысячи клиентов, — перестать анонсировать один-единственный адрес. Мы применяем это как крайнюю меру против устойчивых атак, угрожающих всему PoP, и сообщаем вам об этом, а не оставляем разбираться самостоятельно, — но обещание, что такого никогда не случится, было бы ложью, и любому хостеру, который его даёт, стоит доверять меньше.

Второе — ответный удар не входит в меню. Booter- и stresser-сервисы — это атаки по найму, переодетые в инструменты тестирования, они незаконны в большинстве значимых юрисдикций, находятся под плотным наблюдением и приведут к тому, что ваш сервер здесь заблокируют — по политике, которая разрешает почти всё остальное. Асимметрия, которая делает DDoS привлекательным для атакующего, делает его бесполезным для защищающегося: вы не перефлудите того, кому нечего терять. Гасите, кешируйте, переезжайте — и дайте этому наскучить, а тому, кто платит за трафик, который сам же и отправляет, рано или поздно наскучивает всегда.

  1. Определите уровень за девяносто секунд

    Прежде чем менять любую настройку, получите две оси и два счётчика. Биты высоко, пакеты низко — объёмная атака; пакеты высоко, биты низко — атака на состояние; обе величины скромные при загруженном CPU — L7.

    sar -n DEV 1 5                       # bits/s and packets/s per interface
    ss -s                                # socket summary; watch synrecv
    nstat -az | grep -E 'ListenDrops|ListenOverflows|SyncookiesSent'
    cat /proc/sys/net/netfilter/nf_conntrack_count
    dmesg -T | grep -i conntrack | tail

    Затем исключите вариант «это просто успех» и самоповреждение: пролистайте access-лог за десять секунд. Если пути выглядят как обычное использование сайта, у вас аудитория или баг, а не атакующий.

  2. Дёшево защитите ядро от атаки

    Два файла, один reload. Это безопасно для любого сервера общего назначения и убирает два самых частых способа, которыми валится небольшая машина.

    cat > /etc/sysctl.d/99-flood.conf <<'EOF'
    net.ipv4.tcp_syncookies = 1
    net.ipv4.tcp_max_syn_backlog = 8192
    net.core.somaxconn = 8192
    net.ipv4.tcp_synack_retries = 2
    net.ipv4.tcp_keepalive_time = 600
    net.netfilter.nf_conntrack_max = 262144
    net.netfilter.nf_conntrack_buckets = 65536
    net.netfilter.nf_conntrack_tcp_timeout_established = 3600
    net.netfilter.nf_conntrack_tcp_timeout_syn_recv = 20
    EOF
    
    sysctl --system
    sysctl net.ipv4.tcp_syncookies net.netfilter.nf_conntrack_max   # verify, do not assume

    Затем приведите приложение в соответствие: nginx нужна директива listen 443 ssl backlog=8192;, иначе очередь ядра, которую вы только что увеличили, будет ограничена меньшим числом, которое запросил процесс.

  3. Закройте всё, что не обслуживаете, — выше гостевой системы

    Перечислите, что реально слушает порты, решите, что должно быть на публичном адресе, и вынесите грубые правила на фильтр на границе — он работает на гипервизоре, — чтобы трафик вообще не доходил до вашей виртуальной NIC.

    ss -tlpn; ss -ulpn        # every public listener, with the process that owns it

    В разделе Server → Network → Firewall настройте фильтр на границе на запрет по умолчанию и разрешите только те порты, которые обслуживаете, ограничив SSH адресами, с которых вы администрируете сервер. Держите набор правил nftables внутри гостевой системы как второй уровень — это «ремень и подтяжки», а не «одно вместо другого», — и привязывайте базы данных и админ-интерфейсы к 127.0.0.1 или адресу WireGuard, а не к файрволу на публичном порту.

  4. Поставьте кеш перед дорогим путём

    Это самое ценное изменение из всех для всего, что рендерит страницы. Добавьте proxy_cache с proxy_cache_lock on и proxy_cache_use_stale, чтобы тысяча одновременных промахов кеша превращалась в один запрос к origin, а все остальные получали чуть устаревшую страницу.

    nginx -t && systemctl reload nginx
    curl -sI https://example.com/ | grep -i x-cache     # MISS, then HIT

    Проверьте hit rate, прежде чем ему поверить. Кеш, который никогда не срабатывает из-за Set-Cookie в каждом ответе, — самое частое ложное чувство защищённости во всём этом упражнении: проверьте это двумя вызовами curl -sI и посмотрите на X-Cache.

  5. Ограничивайте дорогие для вас пути, а не посетителя

    Примените щедрый глобальный лимит и жёсткий — на эндпоинтах, которые обращаются к базе данных. Всегда nodelay, всегда с кодом статуса, никогда — молчаливая очередь.

    limit_req_zone $binary_remote_addr zone=general:16m rate=20r/s;
    limit_req_zone $binary_remote_addr zone=costly:16m  rate=2r/s;
    limit_req_status 429;

    Если nginx стоит за каким-либо прокси, сначала настройте set_real_ip_from и real_ip_header. Лимит, привязанный к адресу вашего фронтенда, не тормозит атакующего — он тормозит сразу всех, при первом же срабатывании.

  6. Уберите адрес origin из публичного поля зрения

    Сделайте голый IP бесполезным, а затем решите, что будет стоять перед ним. Отказ занимает две строки и навсегда закрывает путь «просканировать интернет в поисках вашего HTML».

    server {
      listen 80 default_server;
      listen 443 ssl default_server;
      ssl_reject_handshake on;
      return 444;
    }

    Затем проверьте утечки в том порядке, в котором они обычно случаются: исторические A-записи в архивах passive DNS, хостнеймы в логах certificate transparency, которые когда-то резолвились в origin, и заголовки исходящей почты. Если хоть один из них всё ещё называет ваш адрес, фронтенд вас не спасёт — сначала смените адрес, потом ставьте фронт.

  7. Сообщите сетевому оператору — с цифрами

    Защита от объёмных атак автоматическая и не требует тикета, но отчёт с доказательствами позволяет человеку подтвердить, что сделала автоматика, и заметить сбои, которые она сама не видит, — например, скраббер, сбрасывающий легитимные сессии.

    Укажите адрес назначения, время начала в UTC, протокол и порты назначения, скорость по обеим осям, как вы её измерили, и показывают ли ваши собственные счётчики, что пакеты приходят или пропадают выше по потоку. Пятьдесят строк tcpdump -ni eth0 -c 200 стоят больше, чем абзац описания. «Сайт тормозит» — по этому нельзя ничего сделать; «185.x.x.x, 14:02 UTC, UDP на 443, ~1.2 Mpps, на интерфейсе — 40 Kpps на входе» — можно.

  8. Отрепетируйте смену адреса, пока всё в порядке

    Выставьте TTL DNS-записей в 300 секунд уже сегодня и оставьте их там. Затем один раз проведите тренировку от начала до конца на машине, которую уничтожите через час.

    # deploy a second instance in another region, restore, verify by hostname override
    curl --resolve example.com:443:<new-ip> https://example.com/ -sI

    Запишите, сколько минут это заняло. Это число и есть ваш настоящий план на случай DDoS — куда важнее любого правила из этого руководства, — потому что именно оно говорит, насколько долго целенаправленная атака способна продержать вас офлайн. Списание идёт посуточно с баланса, так что вся репетиция обойдётся в центы.

Сравнение

Где каждую атаку реально можно остановить

Каждый уровень что-то останавливает и слеп ко всему остальному. Ошибка, которая стоит людям выходных, — защищаться на том уровне, на котором атака вообще никогда не была видна.
УровеньОстанавливаетНе останавливаетВо что вам обходитсяКто им управляет
Скраббинг выше по потокуОбъёмные флуды — гасятся ниже 10 Gbps, фильтруются до 100, выше — уводятся через BGPВсё, что укладывается в обычные объёмы трафика: атаки на состояние, L7Ничего. Всегда включено, без тикетов, иногда — задержка во время увода трафикаМы, автоматически
Фильтр на границе (на гипервизоре)Всё, что идёт на закрытый порт, — ещё до вашей виртуальной NIC: ни CPU, ни записи в таблице состоянийАтаки на порты, которые вы обязаны держать открытымиНичего, кроме правил, которые вы сами пишете; с отслеживанием состояния, запрет по умолчаниюВы, для каждого сервера отдельно
Файрвол внутри гостевой системы (nftables)SYN-флуд через synproxy, лимиты пакетов на источник, нежелательные протоколыТрафик, который уже забил канал выше вас, — он просто не доходитCPU и запись в conntrack на каждый пакет, включая отброшенныеПолностью вы
Тюнинг ядра (sysctl)Исчерпание accept-очереди и conntrack — классический убийца небольших серверовЛюбое валидное, полностью установленное соединениеОколо 80 МБ RAM при разумном размере conntrack. Два файлаПолностью вы
Приложение (кеш + rate limit)L7-флуд, давку запросов, дорогие эндпоинтыАтаки на уровне пакетов — они просто не доходят до веб-сервераСлегка устаревшие страницы и 429-е для пользователей, которых вы неверно оценилиПолностью вы
Фронтенд на reverse proxyПрямые атаки на origin — целиться просто не во что, публичного адреса нетАтаки на сам фронт и всё, что происходит после утечки вашего origin IP$5/мес и ещё одна машина, которую нужно патчитьВы, если держите его сами
Коммерческий CDNКрупные атаки уровней 3/4 и 7, в промышленных масштабах, с очередью поддержкиOrigin, который хоть раз был публичным или отвечает на голом IPТерминацию TLS третьей стороной, которая знает, кто вы, и может получить судебный запросОни
Onion-сервис TorЛюбую атаку, завязанную на IP, — в протоколе просто нет адресаФлуд introduction points, поэтому современный Tor поставляется с защитой на основе proof-of-workЗадержку и аудиторию, готовую пользоваться TorВы и сеть
FAQ

Ответы на актуальные вопросы

Нужно ли включать защиту от DDoS на моём сервере?

Нет. Защита от объёмных атак работает выше по потоку от PoP-ов и включена всегда — тут нечего покупать и нечего переключать. Ниже 10 Gbps гасится без заметного эффекта; 10–100 Gbps фильтруется на транзитном провайдере, где возможен небольшой рост задержки; выше 100 Gbps префикс анонсируется на выделенный путь скраббинга, задержка растёт заметнее, но сервис остаётся доступным. Пороги опубликованы в документации. Что не автоматизировано — так это всё выше четвёртого уровня: исчерпание таблиц состояний и флуд на уровне приложений выглядят как обычный трафик со стороны апстрима и должны обрабатываться на вашей машине.

Сделаете ли вы null-route моего IP, если меня атакуют?

Только как крайняя мера, и только при устойчивых атаках, угрожающих всему PoP, а не только вашему серверу, — и мы сообщаем об этом в течение нескольких минут, а не оставляем вас узнавать об этом самостоятельно. Любой хостер, обещающий, что такого не случится никогда, описывает маркетинг, а не сеть: при достаточном масштабе защита тысяч клиентов рано или поздно означает отключение одного адреса. Реалистичная защита — сделать себя непривлекательной целью: держать origin-адрес неопубликованным, иметь отрепетированный план смены адреса и знать своё время восстановления.

Мой сайт лежит, но график полосы пропускания выглядит нормально. Что происходит?

Почти наверняка исчерпание состояния — классический случай переполненной таблицы nf_conntrack. Двадцать тысяч мелких пакетов в секунду с подделанных адресов заполнят таблицу по умолчанию за секунды, использовав всего несколько мегабит, так что график не покажет ничего, а каждое новое соединение будет отбрасываться. Проверьте dmesg -T | grep conntrack на предмет «table full, dropping packet» и сравните nf_conntrack_count с nf_conntrack_max. Другая возможность — противоположное прочтение: канал выше вас уже переполнен, и интерфейс показывает вам выживших, а не саму атаку.

Переживёт ли атаку более мощный тариф там, где не справится маленький?

Иногда — и не по той причине, которую обычно ожидают. Больше vCPU и RAM реально помогает против L7-флуда и исчерпания состояния, потому что именно эти атаки съедают CPU, память и место в таблицах. Против объёмной атаки тариф почти не имеет значения — пакеты отбрасываются выше вашего порта независимо от того, что за ним стоит, и порт на 2.5 Gbps не спасёт от 40 Gbps. Прежде чем апгрейдиться, почините кеширование и размер conntrack: это дешевле, и обычно именно это и оказывается настоящей проблемой.

Можно ли поставить Cloudflare или другой CDN перед VPS без KYC?

Технически — да, и это работает хорошо. Но прежде чем идти на этот компромисс, поймите его цену: CDN терминирует ваш TLS и видит plaintext, знает ваш origin-адрес, ведёт аккаунт, который идентифицирует вас по email и часто по способу оплаты, и может получить судебный запрос в юрисдикции, которую вы не выбирали. Для сервера, купленного без привязки к имени, это заново вводит в игру именно ту сторону, которой вся схема должна была избежать. Если ваша модель угроз — это даунтайм, а не раскрытие личности, это разумный выбор, но origin никогда не должен был быть публичным, иначе одна старая DNS-запись сводит всё на нет. Если в вашу модель угроз входит вопрос «кто знает, где вы находитесь», вместо этого поднимите собственный reverse proxy на второй машине и разрешите origin принимать трафик только от него.

Достаточно ли fail2ban, чтобы остановить DDoS?

Нет, и дело не в том, что он слабый, — просто это не тот инструмент. Он реагирует в человеческом масштабе времени после разбора логов, а значит, запросы уже обошлись вам во всё, во что должны были обойтись; он банит по одному адресу за раз, что бесполезно против десяти тысяч источников и превращает ваш файрвол в линейный перебор, если дать ему попытаться; и он зависит от логов, которые атакующий может залить флудом. Используйте его там, где он по-настоящему хорош, — медленный подбор паролей по SSH и формам входа, — а для всего, что происходит на скорости флуда, используйте динамический set nftables с таймаутом.

Стоит ли блокировать целые страны или ASN?

Блокировка по странам оправдана только тогда, когда ваша аудитория действительно ограничена и вы готовы терять пользователей в поездках и тех, кто странно маршрутизируется. Против современного ботнета, размазанного по домашним подключениям повсюду, включая вашу собственную страну, это почти бесполезно. Блокировка ASN дата-центров точнее — обычные люди не сёрфят из хостинг-диапазонов — и заодно ловит скрейпинг и credential stuffing. Более верный инстинкт — фильтровать по форме трафика: общий user agent, отсутствующий заголовок, один и тот же повторяющийся путь. Такое правило продолжает работать и после того, как атакующий сменит адреса, — а он сделает это в течение нескольких минут.

Делает ли меня onion-сервис Tor неуязвимым?

От атак, завязанных на IP, — да: в протоколе просто нет адреса, на который можно направить флуд, и это по-настоящему другой уровень защиты, а не постепенное улучшение. Но в целом неуязвимости нет: onion-сервисы можно атаковать через их introduction points, и именно поэтому современный Tor поставляется с защитой на основе proof-of-work, которая делает флуд дорогим для клиента. Это также стоит задержки и ограничивает вас аудиторией, готовой пользоваться Tor. Многие держат оба варианта сразу — clearnet-фронт ради охвата и onion-адрес, который продолжает работать, пока clearnet-версия под атакой.

Deploy your offshore server.

Выберите регион. Выберите план. Вставьте ключ. Оплатите. Следующие 47 секунд — за нами.