Стандартне встановлення Nginx розкриває версію сервера і не вмикає захисні HTTP-заголовки. Hardening займає 20 хвилин і закриває цілий клас вразливостей.
nginx.conf — базові налаштування
http {
server_tokens off; # Приховати версію
client_max_body_size 10m;
client_body_timeout 12;
client_header_timeout 12;
keepalive_timeout 15;
large_client_header_buffers 2 1k;
}
Захисні HTTP-заголовки
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
Rate Limiting
http {
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
}
server {
location /api/ { limit_req zone=api burst=20 nodelay; }
location /wp-login.php { limit_req zone=login burst=3; }
}
Заборона прихованих і небезпечних файлів
location ~ /\. { deny all; access_log off; log_not_found off; }
location ~* \.(bak|conf|sql|log|sh|ini)$ { deny all; }
Конфігурація SSL/TLS
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305';
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
Обмеження HTTP-методів
location / {
limit_except GET HEAD POST { deny all; }
}
Content-Security-Policy-Report-Only перед бойовим застосуванням.
Розбір типових помилок після hardening
nginx: [emerg] invalid number of arguments in "add_header" directive
nginx: [warn] conflicting server name "example.com" on 0.0.0.0:443
403 Forbidden після увімкнення limit_except
nginx: [emerg] "ssl_ciphers" directive is duplicate
| Повідомлення | Причина | Рішення |
|---|---|---|
| invalid number of arguments in "add_header" | У значенні заголовка пропущені лапки, і Nginx розбив рядок на кілька аргументів за пробілами | Огортати значення заголовка подвійними лапками цілком, включно з директивами на кшталт max-age=31536000 |
| conflicting server name на 443 | Той самий server_name оголошено у двох server-блоках, що слухають один порт | Перевірити nginx -T | grep -A2 server_name, залишити оголошення лише в одному файлі |
| 403 Forbidden після limit_except | Метод запиту (часто PUT або PATCH від API-клієнта) не входить у список дозволених | Додати потрібний метод у limit_except GET HEAD POST лише там, де він справді використовується |
| "ssl_ciphers" directive is duplicate | Директива SSL задана і в http {}, і повторно у конкретному server {} | Залишити директиву в одному місці — зазвичай на рівні http, якщо сертифікати й вимоги однакові для всіх сайтів |
Порівняння рівнів hardening
| Захід | Ризик, який закриває | Складність впровадження |
|---|---|---|
| server_tokens off і захисні заголовки | Витік версії сервера, clickjacking, MIME-sniffing | Низька, безпечна для будь-якого сайту |
| Rate Limiting на логін і API | Брутфорс паролів, перебір ендпоінтів | Середня, потрібно підібрати ліміти під реальний трафік |
| Заборона прихованих і службових файлів | Витік .env, .git, бекапів .sql через прямий URL | Низька, але легко забути нове розширення при додаванні сервісів |
| Content-Security-Policy | XSS через інжект стороннього JS | Висока, потребує інвентаризації всіх зовнішніх скриптів сайту |
Коли цього hardening недостатньо
- Атака йде через вразливість у самому застосунку (SQL-ін'єкція, небезпечна десеріалізація) — заголовки Nginx її не закривають, потрібен патч коду або WAF на рівні застосунку
- Потрібен захист від розподіленого перебору з тисяч IP — rate limiting по одній адресі тут безсилий, потрібен аналіз поведінки на рівні CDN/проксі перед сервером
- Сайт використовує сторонні вбудовані віджети (чати, аналітику, платіжні форми) — суворий CSP без періоду Report-Only майже гарантовано зламає частину з них при першому увімкненні
- Потрібен захист даних під час передавання між внутрішніми сервісами поза HTTP — hardening із цієї статті стосується лише публічного вебтрафіку, а не внутрішньої мережі
Як перевірити, що захист справді працює
# Синтаксис конфігурації перед застосуванням на бойовому сервері
sudo nginx -t
# Заголовки відповіді сервера
curl -sI https://yourdomain.com/ | grep -Ei 'x-frame|x-content|strict-transport|referrer'
# Версія сервера не світиться в заголовку і в службових сторінках
curl -sI https://yourdomain.com/ | grep -i server
- Виконуйте
nginx -tперед кожнимreload— синтаксична помилка на проді валить увесь сайт, а не лише застосовувану директиву - Запитайте заголовки через
curl -Iі переконайтеся, що X-Frame-Options, X-Content-Type-Options і Strict-Transport-Security справді присутні у відповіді - Перевірте, що заголовок
Serverне видає версію Nginx — підтвердження, щоserver_tokens offзастосувався - Прожене сайт через зовнішній сервіс перевірки заголовків (наприклад, той, що згаданий у пораді вище) і досягніть оцінки не нижче A, починаючи з CSP у режимі Report-Only
Окремий розбір Rate Limiting і лімітів підключень — у статті Fail2ban для SSH і Nginx, а базове налаштування брандмауера перед вебсервером — у матеріалі UFW на VPS.
Часті питання
Що робити, якщо після Rate Limiting на /api/ почали масово падати легітимні запити фронтенду?
Перевірте, чи ліміт не нижчий за реальну кількість AJAX-запитів однієї сторінки — зона rate=10r/s із burst=20 може бути затісною для SPA, яка надсилає десятки запитів одразу під час завантаження. Піднімайте burst або додавайте nodelay лише після аналізу логів, а не навмання, інакше ризикуєте просто послабити захист без користі.
Чому сторінка з CSP у режимі Report-Only все одно ламається у частини відвідувачів?
Report-Only лише фіксує порушення політики в лог, але не блокує завантаження ресурсів, тож сама по собі вона нічого зламати не може. Поломка майже завжди повʼязана не з CSP, а з іншим заголовком чи кешем браузера або CDN, який віддає стару версію сторінки. Спершу очистіть кеш і звірте заголовки через curl, перш ніж змінювати директиви CSP.
Чи потрібно повторювати всі заголовки безпеки для кожного віртуального хоста вручну?
Ні, директиви add_header і ssl_ciphers логічніше задати один раз у спільному конфігу і підключати через include в усі server-блоки. Повторення одних і тих самих рядків у кожному файлі хоста швидко призводить до розсинхронізації: один сайт оновили, про решту забули, і рівень захисту розповзається без єдиного місця правки.
Як перевірити, що обмеження HTTP-методів не зламало API, який використовує PUT чи DELETE?
Перевірте логи на 403 одразу після застосування limit_except і повторіть реальні запити клієнта через curl із потрібним методом. Якщо API на інших шляхах легітимно використовує PUT, PATCH чи DELETE, обмеження варто задавати точково для конкретних location, а не глобально для всього сайту — інакше будь-яке відхилення від GET, HEAD, POST сервер відхилить.