Пошук уроків, статей та іншого контенту
Вивчіть стратегії протидії DDoS-атакам: кешування, балансування, rate limiting і розподілення трафіку.
DDoS — розподілена атака на відмову в обслуговуванні. Зловмисник використовує багато пристроїв або джерел, щоб створити надмірне навантаження на сервіс.
Мета атаки — вичерпати один або кілька ресурсів:
пропускну здатність мережі;
кількість доступних TCP-з’єднань;
CPU або пам’ять балансувальника;
кількість worker-процесів;
пул з’єднань до бази даних;
ліміти зовнішніх API;
час обробки HTTP-запитів.
DDoS не обов’язково означає величезний обсяг трафіку. Повільні, але дорогі запити до пошуку, авторизації або генерації звітів також можуть перевантажити систему.
Захист має бути багаторівневим. Відбивати атаку лише на рівні застосунку часто запізно: трафік уже витратив ресурси мережі або балансувальника.
Типова послідовність фільтрації:
Мережева інфраструктура провайдера або edge-рівень
фільтрація небажаного трафіку;
поглинання великих обсягів атаки;
блокування очевидних мережевих шаблонів.
CDN або reverse proxy
кешування відповідей;
обмеження частоти запитів;
перевірка підозрілих клієнтів.
Load balancer
розподілення запитів між інстансами;
health checks;
обмеження кількості з’єднань.
Застосунок
rate limiting для конкретних endpoint-ів;
захист дорогих операцій;
контроль розміру запитів;
швидке відхилення некоректних запитів.
Залежності
захист бази даних, черг і зовнішніх сервісів;
обмеження пулів з’єднань;
використання кешу та черг для зменшення пікового навантаження.
Кешування зменшує кількість запитів до origin-сервера — сервера, де працює основний застосунок.
Найкраще кешуються:
статичні файли;
зображення;
JavaScript і CSS;
публічні сторінки;
результати запитів, які однакові для багатьох користувачів.
Якщо CDN має кешовану відповідь, запит не доходить до вашого сервера. Це зменшує навантаження на:
мережу між CDN та origin;
вебсервери;
базу даних;
CPU застосунку.
Кеш не є універсальним захистом:
динамічні персоналізовані відповіді часто не можна кешувати спільно;
запити з унікальними параметрами можуть створювати cache miss;
зловмисник може навмисно запитувати некешовані URL;
CDN все одно має обробити вхідний трафік.
Тому важливо:
кешувати лише безпечні публічні відповіді;
використовувати коректні Cache-Control заголовки;
не додавати зайві унікальні параметри до URL;
окремо захищати endpoint-и, які завжди звертаються до origin.
Наприклад, публічний каталог товарів можна кешувати на короткий час, а профіль користувача — ні, якщо відповідь залежить від авторизації.
Load balancer розподіляє запити між кількома інстансами застосунку.
Без балансування один сервер може стати вузьким місцем. З балансуванням можна:
збільшити кількість інстансів;
ізолювати несправний інстанс;
виконувати rolling deployment;
розподіляти навантаження між регіонами або зонами доступності.
Поширені алгоритми:
Round robin — запити по черзі надходять до різних серверів.
Least connections — новий запит спрямовується серверу з найменшою кількістю активних з’єднань.
Weighted routing — інстанси отримують різну частку трафіку залежно від їхньої потужності.
Geographic routing — користувача спрямовують до найближчого регіону.
Балансування саме по собі не зупиняє DDoS-атаку. Якщо всі інстанси отримують шкідливі запити, атака просто розподіляється між ними. Тому балансувальник потрібно поєднувати з кешуванням, rate limiting і фільтрацією на edge-рівні.
Балансувальник має перевіряти доступність інстансів. Health check повинен бути простим і дешевим:
не виконувати складний запит до бази даних;
швидко повертати успішну відповідь;
показувати, чи може інстанс приймати трафік.
Якщо health check залежить від перевантаженої бази даних, під час атаки всі інстанси можуть бути помилково позначені як несправні.
Rate limiting обмежує кількість запитів за певний проміжок часу.
Обмеження можна застосовувати за:
IP-адресою;
ідентифікатором користувача;
API-ключем;
сесією;
маршрутом;
комбінацією кількох ознак.
Наприклад, для різних endpoint-ів можуть бути різні правила:
публічне читання: більший ліміт;
авторизація: дуже малий ліміт;
відправлення email: ще суворіший ліміт;
генерація звіту: один або кілька запитів на хвилину.
У fixed window кількість запитів рахується в межах фіксованого інтервалу.
Наприклад:
60 запитів на хвилину;
на початку кожної хвилини лічильник обнуляється.
Переваги:
проста реалізація;
низька вартість;
зрозуміла поведінка.
Недолік — сплеск на межі вікон. Клієнт може виконати майже 60 запитів наприкінці одного вікна і ще 60 на початку наступного.
Sliding window оцінює запити за рухомим часовим інтервалом. Це точніше, але складніше.
Token bucket представляє ліміт як набір токенів:
кожен запит витрачає один токен;
токени поступово додаються;
коли токенів немає, запит відхиляється або чекає.
Token bucket дає змогу дозволити короткі сплески, але контролювати середню швидкість запитів.
Локальний лічильник у пам’яті одного процесу не підходить для системи з кількома інстансами:
кожен інстанс бачить лише частину запитів;
після перезапуску лічильники зникають;
різні інстанси можуть мати різні ліміти.
Для спільного ліміту потрібне централізоване або edge-сховище, наприклад Redis чи функціональність API gateway. Операції перевірки та збільшення лічильника мають бути атомарними.
Нижче наведено навчальний HTTP-сервер на Node.js. Він дозволяє до 5 запитів за 10 секунд для однієї IP-адреси.
const http = require('node:http');
const PORT = 3000;
const WINDOW_MS = 10_000;
const MAX_REQUESTS = 5;
const clients = new Map();
function getClientIp(request) {
// Для навчального прикладу беремо адресу TCP-з'єднання.
// Не довіряємо X-Forwarded-For без правильно налаштованого reverse proxy.
return request.socket.remoteAddress || 'unknown';
}
function checkRateLimit(ip) {
const now = Date.now();
const current = clients.get(ip);
if (!current || now >= current.resetAt) {
const next = {
count: 1,
resetAt: now + WINDOW_MS
};
clients.set(ip, next);
return {
allowed: true,
remaining: MAX_REQUESTS - next.count,
retryAfter: 0
};
}
if (current.count >= MAX_REQUESTS) {
return {
allowed: false,
remaining: 0,
retryAfter: Math.ceil((current.resetAt - now) / 1000)
};
}
current.count += 1;
return {
allowed: true,
remaining: MAX_REQUESTS - current.count,
retryAfter: 0
};
}
const server = http.createServer((request, response) => {
const ip = getClientIp(request);
const limit = checkRateLimit(ip);
response.setHeader('X-RateLimit-Limit', String(MAX_REQUESTS));
response.setHeader('X-RateLimit-Remaining', String(limit.remaining));
if (!limit.allowed) {
response.statusCode = 429;
response.setHeader('Content-Type', 'application/json');
response.setHeader('Retry-After', String(limit.retryAfter));
response.end(JSON.stringify({
error: 'Забагато запитів',
retryAfterSeconds: limit.retryAfter
}));
return;
}
response.statusCode = 200;
response.setHeader('Content-Type', 'application/json');
response.end(JSON.stringify({
message: 'Запит прийнято'
}));
});
server.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});Цей приклад демонструє принцип, але не є готовим розподіленим рішенням для production:
лічильники зберігаються лише в пам’яті одного процесу;
пам’ять поступово зростатиме, якщо не видаляти старі IP-адреси;
кілька інстансів матимуть незалежні ліміти;
IP-адреса не завжди є надійним ідентифікатором користувача.
У реальній системі rate limiting бажано виконувати якомога ближче до edge-рівня, а критичні endpoint-и додатково обмежувати в самому застосунку.
Розподілення трафіку зменшує залежність від одного сервера, регіону або мережевого маршруту.
Можливі стратегії:
Балансувальник розподіляє запити між інстансами. Це захищає від відмови окремого сервера, але не від перевантаження всього регіону.
Інстанси розміщують у різних зонах. Відмова або перевантаження однієї зони не повинні зупинити весь сервіс.
Користувачів можна спрямовувати до найближчого або найменш завантаженого регіону. Це допомагає розподілити трафік, але додає складності:
синхронізація даних;
маршрутизація;
узгодженість сесій;
відмінності між регіонами.
Origin не повинен бути доступним для всього Інтернету, якщо весь трафік має проходити через CDN або reverse proxy.
Бажано:
дозволити до origin лише адреси довіреного edge-рівня;
не публікувати прямий IP origin у DNS або конфігураціях клієнта;
мати окремі правила доступу для адміністративних endpoint-ів;
перевіряти, що запит справді прийшов через очікуваний proxy.
Якщо зловмисник знаходить прямий IP origin і обходить CDN, кешування та rate limiting втрачають значну частину користі.
Не всі endpoint-и споживають однакову кількість ресурсів. Один запит до health check може бути дешевим, а один запит до пошуку — виконувати складний join у базі даних.
Тому ліміти потрібно налаштовувати з урахуванням вартості операції:
дешеві GET-запити можуть мати більший ліміт;
пошук і фільтрація — менший ліміт;
авторизація та відновлення пароля — суворіший ліміт;
створення звіту краще передавати у фонову чергу;
повторні запити можна обслуговувати з кешу.
Також корисно відхиляти запит якомога раніше:
перевіряти метод і формат даних;
обмежувати розмір body;
встановлювати timeout;
не створювати з’єднання з базою для очевидно некоректного запиту.
Під час атаки система має не лише приймати або відхиляти запити, а й зберігати пріоритет для важливих операцій.
Можна застосувати такі правила:
Дозволяти трафік до health check і критичних endpoint-ів.
Обмежувати дорогі або другорядні функції.
Тимчасово вимикати функції, які не потрібні для основного сценарію.
Віддавати кешовану або спрощену відповідь.
Відхиляти нові запити, якщо система наближається до граничного навантаження.
Не дозволяти одному клієнту зайняти весь пул з’єднань.
Це називають graceful degradation — контрольованим погіршенням сервісу замість повної відмови.
Захист неможливо налаштувати без метрик. Варто відстежувати:
кількість запитів за секунду;
кількість активних TCP-з’єднань;
частку відповідей 4xx і 5xx;
частку відповідей 429;
latency на різних percentiles;
використання CPU та пам’яті;
кількість з’єднань до бази даних;
cache hit ratio;
трафік за IP, ASN, регіоном і endpoint-ом.
Важливо відрізняти DDoS від звичайного сплеску популярності. Наприклад, рекламна кампанія може збільшити трафік легітимних користувачів, тому блокування всіх запитів за IP може нашкодити реальним клієнтам.
Один із практичних варіантів виглядає так:
Користувачі
|
v
Edge / CDN
|
|-- кеш статичних і публічних відповідей
|-- rate limiting
|-- фільтрація підозрілого трафіку
v
Load balancer
|
|-- health checks
|-- обмеження з'єднань
v
Інстанси застосунку
|
|-- endpoint-specific rate limiting
|-- валідація і timeouts
v
Кеш / черга / база данихКожен наступний рівень має отримувати лише той трафік, який не було відфільтровано раніше.
Якщо атака вже займає всю пропускну здатність або таблицю з’єднань балансувальника, код застосунку може не отримати шанс обробити запит.
Правильно: фільтрувати трафік на edge-рівні, а в застосунку мати додатковий захист.
Балансувальник розподіляє навантаження, але не робить шкідливі запити дешевшими. Атака може перевантажити всі інстанси одночасно.
Один ліміт для всіх endpoint-ів або користувачів часто є надто грубим. Дешеві операції не повинні мати такі самі правила, як авторизація чи генерація звіту.
Таке рішення не узгоджується між інстансами та втрачає стан після перезапуску.
X-Forwarded-ForЦей заголовок можна підробити, якщо reverse proxy не перезаписує його коректно. Потрібно чітко визначити довірені proxy і лише після цього використовувати передану ними IP-адресу.
Великі body та повільні з’єднання можуть споживати ресурси навіть за невеликої кількості запитів.
Якщо origin доступний за прямою IP-адресою, зловмисник може обійти CDN і правила edge-рівня.
Блокування за однією IP-адресою може зачепити багатьох користувачів із корпоративної мережі, мобільного оператора або NAT.
DDoS може перевантажити мережу, балансувальник, застосунок або його залежності.
Захист має бути багаторівневим: edge, CDN, load balancer, застосунок і база даних.
Кешування зменшує кількість запитів до origin, але не захищає некешовані дорогі операції.
Load balancer підвищує доступність, але сам по собі не зупиняє DDoS.
Rate limiting потрібно налаштовувати окремо для різних клієнтів і endpoint-ів.
Для кількох інстансів ліміти мають зберігатися у спільному сховищі або застосовуватися на edge-рівні.
Розподілення трафіку між інстансами, зонами та регіонами зменшує вплив локального перевантаження.
Origin потрібно захищати від прямого доступу, а систему — доповнювати моніторингом і контрольованим погіршенням функціональності.