Чек-лист безопасности веб-сервера: от HTTPS до WAF за один день

Исходная ситуация: медиа-портал без защиты

В марте 2026 года к нам обратилась редакция онлайн-издания НовостиСегодня — новостной портал с 50 000 уникальных посетителей в день. Стек: Nginx, PHP-FPM 8.2, PostgreSQL, Ubuntu 22.04. Причина обращения — серия атак: дефейс главной страницы, SQL-инъекция в комментариях и подозрительные POST-запросы к admin-панели.

Мы провели экспресс-аудит и обнаружили удручающую картину:

  • SSL Labs оценка: C (устаревший TLS 1.0/1.1, слабые шифры)
  • Security-заголовки: отсутствуют полностью (securityheaders.com показал F)
  • WAF: не установлен
  • PHP: display_errors = On, allow_url_include = On
  • Директории /backup/ и /test/ доступны извне с листингом файлов
  • Версия Nginx и PHP светились в заголовках ответов

Задача была ясна: поднять безопасность до продакшен-уровня за один рабочий день, не сломав работу портала.

HTTPS: путь к A+ в SSL Labs

Начали с основы — TLS-конфигурации. Оценка C в SSL Labs означала, что трафик пользователей мог быть перехвачен. Переписали SSL-блок Nginx:

# /etc/nginx/snippets/ssl-params.conf

# Только TLS 1.2 и 1.3 — отключаем устаревшие версии
ssl_protocols TLSv1.2 TLSv1.3;

# Серверные шифры имеют приоритет
ssl_prefer_server_ciphers on;

# Современные шифры — исключаем CBC, RC4, 3DES
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305';

# ECDH curve
ssl_ecdh_curve X25519:secp384r1;

# SSL session cache — ускоряем повторные подключения
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;

# OCSP Stapling — проверка сертификата без дополнительного запроса
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;

# DH parameter — 4096 бит
ssl_dhparam /etc/nginx/dhparam.pem;
# Генерируем DH параметры (занимает 2-5 минут)
openssl dhparam -out /etc/nginx/dhparam.pem 4096

# Проверяем конфигурацию
nginx -t && systemctl reload nginx

# Тестируем локально
openssl s_client -connect novostisegodnya.ru:443 -tls1_3
# New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384 — отлично

После применения конфигурации проверили на SSL Labs — оценка A+. Ключевой момент: ssl_session_tickets off и OCSP Stapling — без них максимум A.

Security-заголовки: CSP, HSTS, X-Frame-Options

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

# /etc/nginx/snippets/security-headers.conf

# HSTS — принудительный HTTPS на 1 год + включение в preload list
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

# Защита от clickjacking — запрет вставки в iframe
add_header X-Frame-Options "SAMEORIGIN" always;

# Защита от MIME-sniffing
add_header X-Content-Type-Options "nosniff" always;

# XSS-фильтр (для старых браузеров)
add_header X-XSS-Protection "1; mode=block" always;

# Referrer Policy — не передаём полный URL при переходе на другие сайты
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# Permissions Policy — запрещаем доступ к камере, микрофону, геолокации
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;

# Content Security Policy — самый мощный заголовок
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://mc.yandex.ru https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://mc.yandex.ru; frame-ancestors 'self'; base-uri 'self'; form-action 'self'" always;

CSP заслуживает отдельного внимания. Мы разрешили:

  • script-src 'self' 'unsafe-inline' — скрипты только со своего домена (inline пришлось оставить из-за CMS)
  • https://mc.yandex.ru — Яндекс.Метрика
  • frame-ancestors 'self' — дублирует X-Frame-Options, но для CSP-совместимых браузеров
  • form-action 'self' — формы отправляются только на свой домен (защита от фишинг-перехвата)
# Подключаем заголовки в конфигурацию сайта
server {
    listen 443 ssl http2;
    server_name novostisegodnya.ru;
    
    include snippets/ssl-params.conf;
    include snippets/security-headers.conf;
    
    # ...
}

Результат на securityheaders.com: A+ (было F).

ModSecurity WAF: защита от инъекций и XSS

Security-заголовки работают на стороне браузера. Для защиты на стороне сервера нужен WAF (Web Application Firewall). Установили ModSecurity с OWASP Core Rule Set:

# Установка ModSecurity для Nginx
apt install -y libmodsecurity3 libmodsecurity-dev
apt install -y libnginx-mod-http-modsecurity

# Или компиляция connector для Nginx
git clone https://github.com/owasp-modsecurity/ModSecurity-nginx.git

# Включение ModSecurity в Nginx
# /etc/nginx/conf.d/modsecurity.conf
modsecurity on;
modsecurity_rules_file /etc/nginx/modsecurity/main.conf;
# /etc/nginx/modsecurity/main.conf
Include /etc/nginx/modsecurity/modsecurity.conf
Include /etc/nginx/modsecurity/crs/crs-setup.conf
Include /etc/nginx/modsecurity/crs/rules/*.conf

# Кастомные исключения для CMS (чтобы не ломать редактор статей)
# /etc/nginx/modsecurity/custom-exclusions.conf
SecRule REQUEST_URI "@beginsWith /admin/articles/edit" \
    "id:1001,phase:1,pass,nolog,\
    ctl:ruleRemoveById=941100,\
    ctl:ruleRemoveById=941160,\
    ctl:ruleRemoveById=942100"

# Режим работы: DetectionOnly для тестирования, On для блокировки
SecRuleEngine On

# Логирование заблокированных запросов
SecAuditEngine RelevantOnly
SecAuditLog /var/log/nginx/modsec_audit.log
SecAuditLogFormat JSON
# Скачиваем OWASP Core Rule Set
cd /etc/nginx/modsecurity/
git clone https://github.com/coreruleset/coreruleset.git crs
cp crs/crs-setup.conf.example crs/crs-setup.conf

# Настраиваем порог аномалий (по умолчанию 5)
# /etc/nginx/modsecurity/crs/crs-setup.conf
SecAction "id:900110,phase:1,pass,nolog,\
    setvar:tx.inbound_anomaly_score_threshold=5,\
    setvar:tx.outbound_anomaly_score_threshold=4"

nginx -t && systemctl reload nginx

Важно: первые 3 дня мы работали в режиме DetectionOnly, анализируя логи. CMS-редактор статей отправлял HTML в POST-запросах, что ModSecurity считал XSS. Добавили исключения для admin-панели, после чего переключили в режим блокировки.

Nginx: харденинг конфигурации

Nginx по умолчанию раскрывает много информации и позволяет вещи, которые в продакшене опасны:

# /etc/nginx/nginx.conf — глобальные настройки безопасности

# Скрываем версию Nginx из заголовков и страниц ошибок
server_tokens off;

# Ограничиваем размер тела запроса (защита от upload-бомб)
client_max_body_size 10m;

# Ограничиваем размер буферов (защита от buffer overflow атак)
client_body_buffer_size 1k;
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;

# Таймауты — не даём держать соединение бесконечно
client_body_timeout 10;
client_header_timeout 10;
keepalive_timeout 15;
send_timeout 10;
# Конфигурация сайта — закрываем дыры
server {
    listen 443 ssl http2;
    server_name novostisegodnya.ru;
    
    # Запрет листинга директорий
    autoindex off;
    
    # Запрет доступа к скрытым файлам (.htaccess, .git, .env)
    location ~ /\. {
        deny all;
        return 404;
    }
    
    # Запрет доступа к бэкапам и конфигам
    location ~* \.(sql|bak|backup|log|ini|conf|env)$ {
        deny all;
        return 404;
    }
    
    # Блокировка сканеров
    if ($http_user_agent ~* (nmap|nikto|wikto|sqlmap|bsqlbf|w3af|acunetix|havij|appscan|dirbuster|gobuster)) {
        return 403;
    }
    
    # Защита от host-header injection
    if ($host !~ ^(novostisegodnya.ru|www.novostisegodnya.ru)$) {
        return 444;
    }
    
    # Rate limiting для API и формы логина
    location /admin/ {
        limit_req zone=admin burst=5 nodelay;
        # ...
    }
}

# Rate limiting зоны
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=admin:10m rate=2r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;

Отдельно закрыли директории /backup/ и /test/, которые до этого были доступны с листингом. Удалили тестовые файлы phpinfo.php и test.php из document root.

PHP/Python харденинг и права доступа

PHP-FPM по умолчанию настроен для удобства разработки, а не безопасности. Исправляем:

# /etc/php/8.2/fpm/php.ini — ключевые параметры безопасности

# Не показывать ошибки пользователям (раскрывают пути и структуру)
display_errors = Off
log_errors = On
error_log = /var/log/php/error.log

# Запрет опасных функций
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source,eval

# Запрет включения удалённых файлов
allow_url_include = Off
allow_url_fopen = Off

# Не раскрывать версию PHP
expose_php = Off

# Ограничение доступа к файловой системе
open_basedir = /var/www/html:/tmp

# Сессии — безопасные cookie
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = Strict
session.use_strict_mode = 1

# Лимиты
upload_max_filesize = 5M
post_max_size = 5M
max_execution_time = 30
max_input_time = 30
memory_limit = 128M
# PHP-FPM pool — изоляция процессов
# /etc/php/8.2/fpm/pool.d/www.conf

[www]
user = www-data
group = www-data

# Только один entry point — index.php
security.limit_extensions = .php

# Ограничение процессов
pm = dynamic
pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 10
pm.max_requests = 500

# Slowlog для обнаружения подозрительных запросов
slowlog = /var/log/php/slow.log
request_slowlog_timeout = 10s
request_terminate_timeout = 30s
# Права доступа к файлам
# Владелец — root, группа — www-data, запись только владельцу
chown -R root:www-data /var/www/html
find /var/www/html -type f -exec chmod 640 {} \;
find /var/www/html -type d -exec chmod 750 {} \;

# Директория uploads — единственная, куда www-data может писать
chown -R www-data:www-data /var/www/html/uploads
chmod 750 /var/www/html/uploads

# Конфигурационные файлы — только чтение
chmod 440 /var/www/html/config.php

Ключевой принцип: минимальные привилегии. Процесс PHP может читать код (root:www-data, 640), но не может его изменять. Записывать может только в uploads/.

Автоматическое сканирование: Nikto и OWASP ZAP

Безопасность — не одноразовое действие. Мы настроили регулярное автоматическое сканирование двумя инструментами:

# Nikto — быстрое сканирование на известные уязвимости
apt install -y nikto

# Запуск сканирования
nikto -h https://novostisegodnya.ru -output /var/log/nikto/scan_$(date +%F).html -Format html

# Пример вывода (до харденинга):
# + Server: nginx/1.24.0  ← версия раскрыта
# + /backup/: Directory indexing found
# + /test/phpinfo.php: PHP info file found
# + X-Frame-Options header not set
# + X-Content-Type-Options header not set
# + 7 item(s) reported on remote host

# Пример вывода (после харденинга):
# + No findings reported  ← чисто
# OWASP ZAP — глубокое сканирование (Docker-версия)
docker run -v /var/log/zap:/zap/wrk:rw \
    ghcr.io/zaproxy/zaproxy:stable \
    zap-baseline.py \
    -t https://novostisegodnya.ru \
    -r scan_report.html \
    -l WARN \
    -I

# Для полного сканирования (с активными атаками — только на staging!)
docker run -v /var/log/zap:/zap/wrk:rw \
    ghcr.io/zaproxy/zaproxy:stable \
    zap-full-scan.py \
    -t https://novostisegodnya.ru \
    -r full_scan_report.html
# Автоматизация через cron — еженедельное сканирование
# /etc/cron.d/security-scan
0 3 * * 1 root /opt/scripts/weekly_security_scan.sh

# /opt/scripts/weekly_security_scan.sh
#!/bin/bash
DATE=$(date +%F)
LOG_DIR="/var/log/security-scans"
mkdir -p "$LOG_DIR"

# Nikto scan
nikto -h https://novostisegodnya.ru \
    -output "$LOG_DIR/nikto_$DATE.html" \
    -Format html 2>&1

# ZAP baseline scan
docker run --rm -v "$LOG_DIR:/zap/wrk:rw" \
    ghcr.io/zaproxy/zaproxy:stable \
    zap-baseline.py \
    -t https://novostisegodnya.ru \
    -r "zap_$DATE.html" \
    -l WARN 2>&1

# SSL Labs проверка через CLI
ssllabs-scan --grade novostisegodnya.ru > "$LOG_DIR/ssl_$DATE.json" 2>&1

# Отправляем сводку в Telegram
NIKTO_ISSUES=$(grep -c 'item(s) reported' "$LOG_DIR/nikto_$DATE.html" || echo "0")
curl -s -X POST "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \
    -d chat_id="${TG_CHAT}" \
    -d text="Security scan $DATE: Nikto=$NIKTO_ISSUES issues"

Сканирование запускается каждый понедельник в 3:00. Результаты сохраняются в /var/log/security-scans/ и сводка уходит в Telegram. Если Nikto обнаруживает новые проблемы — администратор узнаёт об этом утром в понедельник.

Итоги и чек-лист для клиентов itfresh.ru

За один рабочий день мы подняли безопасность портала НовостиСегодня с уровня «открытая дверь» до продакшен-стандарта:

ПараметрДоПосле
SSL LabsCA+
Security HeadersFA+
WAFНетModSecurity + OWASP CRS
Nikto findings7 проблем0 проблем
Доступ к /backup/ОткрытЗаблокирован
PHP ошибки видныДаТолько в логах
Rate limitingНет2 req/s admin, 10 req/s general

Минимальный чек-лист безопасности для любого веб-сервера:

  1. HTTPS: TLS 1.2+, A+ в SSL Labs, HSTS с preload
  2. Заголовки: CSP, X-Frame-Options, X-Content-Type-Options, Permissions-Policy
  3. WAF: ModSecurity + OWASP CRS (3 дня в DetectionOnly, потом блокировка)
  4. Nginx: server_tokens off, autoindex off, блокировка скрытых файлов
  5. PHP: display_errors Off, disable_functions, open_basedir
  6. Права: файлы 640, директории 750, uploads — единственная writable
  7. Сканирование: Nikto + ZAP еженедельно, алерты в Telegram

Часто задаваемые вопросы

Let's Encrypt выдаёт бесплатные сертификаты, которых достаточно для A+. Ключевые настройки: TLS 1.2/1.3 only, сильные шифры (ECDHE+AESGCM), OCSP Stapling, DH параметры 4096 бит, ssl_session_tickets off и заголовок HSTS с includeSubDomains.
Начните с Content-Security-Policy-Report-Only — этот заголовок не блокирует, а только логирует нарушения. Добавьте report-uri для сбора отчётов. Через неделю вы увидите все необходимые источники (аналитика, CDN, шрифты) и составите точную политику. Только после этого переключайте на Content-Security-Policy.
Включите SecRuleEngine DetectionOnly на 3-5 дней и анализируйте логи /var/log/nginx/modsec_audit.log. Для CMS-редакторов, которые отправляют HTML, добавьте исключения по URI и rule ID через ctl:ruleRemoveById. Не отключайте правила глобально — только для конкретных URL.
Нет. Nikto проверяет известные уязвимости и мисконфигурации (пассивное сканирование). OWASP ZAP выполняет активное тестирование — SQL-инъекции, XSS, CSRF. Для полной картины нужны оба инструмента плюс ручной пентест хотя бы раз в год.
Добавьте whitelist по User-Agent или IP-диапазонам. Для Googlebot проверяйте IP через reverse DNS (host IP → *.googlebot.com). В Nginx: создайте map с белым списком и применяйте limit_req только для остальных. Или используйте geo-модуль для whitelist по IP-подсетям.

Нужна помощь с проектом?

Специалисты АйТи Фреш помогут с архитектурой, DevOps, безопасностью и разработкой — 15+ лет опыта

📞 Связаться с нами
#безопасность веб-сервера#ssl labs a+#csp заголовок#hsts настройка#modsecurity waf nginx#nginx безопасность#php харденинг#rate limiting nginx
Комментарии 0

Оставить комментарий

загрузка...