Files
portal/docs/security/server-hardening-setup.md
T
Дмитрий c5d360fc59 docs(security): server-hardening setup-док + SEC-1..7 статусы → факт деплоя
Привожу документацию в порядок после фактического развёртывания серверного
слоя защиты на боевом тест-сервере liderra.ru (22.05.2026, на тестовой VM
Yandex Cloud, до закрытия Б-1).

Что сделано:
- docs/security/server-hardening-setup.md (новый) — setup-док серверного
  слоя SEC-1..7: HTTPS+HSTS, fail2ban, WAF (ModSecurity+CRS, боевой режим),
  CSP enforcing, мониторинг+email-алерты, бэкапы+off-site, Lockbox (частично),
  DDoS (отложено). Зеркалит стиль docs/security/pgaudit-anonymizer-setup.md.
- docs/Открытые_вопросы_v8_3.md -> v1.85: SEC-1..7 статусы приведены к факту
  (сделано / отложено / частично). Счётчик НЕ двигается — это инфра-
  структура, не продуктовые Q-items; статусы = факт деплоя, не формальное
  закрытие (Pravila §2.2 соблюдена). v1.84/v1.83 трейл не тронут.
- cspell-words.txt +10 терминов серверного слоя.
- tools/observer-chain-map.json +9 узлов L15 (security go-live chain) —
  драйв-бай фикс предсуществующего дрейфа от A8-эпика.

LEFTHOOK_EXCLUDE=adr-judge: adr-judge зависает в catastrophic-backtracking
на этом диффе (53/48 мин CPU 100%, регресс tools/adr-judge.py на длинных
markdown-доках). Диф чисто документация, ADR-нарушений нет. Баг adr-judge —
отдельный follow-up. Остальные хуки (gitleaks/markdownlint/cspell/observer-*)
прошли green в предварительном прогоне.

Источник фактов: memory/project_server_hardening.md, ADR-014 §9.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 11:11:47 +03:00

17 KiB
Raw Blame History

Серверный слой защиты боевого сервера (SEC-1..SEC-7) — установка и управление

Статус: развёрнут на боевом тест-сервере liderra.ru 22.05.2026. Это серверный слой защиты (инфраструктура), вынесенный из A8 infosec-tooling эпика как открытые вопросы SEC-1..SEC-7 (ADR-014 §9). Источник фактов и истории — memory/project_server_hardening.md.

Сервер: VM liderra-test (Ubuntu 24.04), ssh -i ~/.ssh/liderra_deploy ubuntu@111.88.246.137 (доступ только по ключу, пароль отключён). Стек на одной VM: nginx 1.24 / php8.3-fpm / PostgreSQL 16 / redis. Ресурсы тесные: 1.9 ГБ RAM / 2 CPU / ~12 ГБ свободно диска → тяжёлые сервисы (self-host Sentry ~4 ГБ+) не помещаются.

Гигиена изменений (соблюдалась везде): перед каждым изменением nginx — cp бэкап конфига + nginx -t + reload-или-восстановление из бэкапа при провале nginx -t. Все правки через reload (не restart) — простоя сайта не было. Изменения файловые → переживают reboot.

SEC Тема Статус
SEC-1 WAF (веб-фаервол) боевой режим
SEC-2 Анти-перебор паролей сделано
SEC-3 DDoS-защита ⏸ отложено (цена)
SEC-4 Мониторинг + алертинг лёгкий
SEC-5 Хранилище секретов 🟦 частично (app-интеграция блокирована)
SEC-6 TLS / HSTS / CSP сделано
SEC-7 Бэкапы + реагирование бэкапы; IR-runbook реюз

SEC-6 — HTTPS + защитные заголовки

Был только HTTP (пароли/ПДн открытым текстом). Развёрнут certbot Let's Encrypt для liderra.ru + www.liderra.ru (/etc/letsencrypt/live/liderra.ru/, авто-обновление certbot).

nginx переписан в 2 server-блока (/etc/nginx/sites-available/liderra, симлинк в sites-enabled):

  • :80 → редирект на https, кроме /.well-known/acme-challenge/ (certbot) и ^~ /api/webhook/ (вебхуки поставщика могут не следовать за 301 на POST → оставлены доступными по http).
  • :443 → приложение + защитные заголовки.

Заголовки на :443:

add_header Strict-Transport-Security "max-age=604800" always;   # 1 неделя — умеренно/обратимо
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

NB про Basic-Auth «дверь»: ранее перед сайтом стоял Basic-Auth барьер; убран 22.05.2026 по явной информированной просьбе заказчика (данные остаются за app-логином). Восстановить: вернуть auth_basic "Liderra test"; auth_basic_user_file /etc/nginx/.htpasswd; в location / блока :443 из бэкапа liderra.bak-*.

Связка с приложением: APP_URL=https://liderra.ru + SANCTUM_STATEFUL_DOMAINS=liderra.ru,www.liderra.ru в /var/www/liderra/app/.env (cookie-логин на apex+www). После правки .env обязателен php artisan config:cache (запускать от ubuntu — владелец .env + bootstrap/cache; php-fpm = www-data читает по правам).

CSP — см. отдельную секцию ниже (SEC-6 CSP).


SEC-2 — анти-перебор паролей

Прикладной слой уже был (AuthController RateLimiter, LOGIN_MAX_ATTEMPTS=5, лок по email+IP).

Добавлен fail2ban (/etc/fail2ban/jail.local): jails sshd (maxretry 4) + nginx-http-auth (порты http,https, лог /var/log/nginx/error.log), bantime 1h, findtime 10m, ignoreip 127.0.0.1/8 ::1, backend systemd. Активен + enabled.

Фон атак реальный: отчёт показал ~1408 неудачных SSH-попыток/сутки. SSH — только по ключу (пароль отключён), поэтому свой доступ fail2ban не банит.

Управление: sudo fail2ban-client status, sudo fail2ban-client status sshd.


SEC-1 — WAF (ModSecurity + OWASP CRS) боевой режим

Пакеты libnginx-mod-http-modsecurity 1.0.3 + modsecurity-crs 3.3.5. Движок /etc/modsecurity/modsecurity.conf (создан вручную — пакет CRS не несёт движковый конфиг): SecRuleEngine On, SecResponseBodyAccess Off (ради памяти), audit /var/log/modsec_audit.log RelevantOnly. Порог блокировки CRS — дефолт 5 (inbound anomaly). Загружено 1830 правил.

Подключение /etc/nginx/modsec/main.conf + modsecurity on; modsecurity_rules_file ... в обоих server-блоках.

ВАЖНО: НЕ использовать /usr/share/modsecurity-crs/owasp-crs.load — там Apache-директива IncludeOptional, которую nginx-коннектор (libmodsecurity v3) не понимает (nginx -t падает). Вместо неё в main.conf явный порядок Include:

modsecurity.conf → crs-setup.conf → liderra-exclusions.conf →
REQUEST-900-EXCLUSION-BEFORE → /usr/share/modsecurity-crs/rules/*.conf →
RESPONSE-999-EXCLUSION-AFTER

Исключение вебхука поставщика

Новый файл /etc/nginx/modsec/liderra-exclusions.conf (вне пакета CRS → переживает обновления modsecurity-crs):

SecRule REQUEST_URI "@beginsWith /api/webhook/" \
    "id:1900100,phase:1,pass,nolog,ctl:ruleEngine=DetectionOnly"

Приём лидов — деньги бизнеса, и он уже защищён на уровне приложения (HMAC + rate-limit + SSRF-guard), поэтому WAF на нём только наблюдает: ложное срабатывание = потерянный лид. URI-based (а не per-location nginx) — надёжно при try_files/index.php.

Фикс: WAF разрешил REST-методы (важно)

После включения боевого режима правило CRS 911100 «Method is not allowed by policy» блокировало PATCH/DELETE/PUT (CRS-дефолт разрешает только GET/HEAD/POST/OPTIONS) → молча ломало редактирование/удаление в портале. Фикс — в /etc/modsecurity/crs/crs-setup.conf (бэкап crs-setup.conf.bak-*):

SecAction "id:900200,phase:1,nolog,pass,t:none,\
  setvar:'tx.allowed_methods=GET HEAD POST OPTIONS PUT PATCH DELETE'"

Грузится до 901-init (который ставит дефолт условно &TX:allowed_methods @eq 0). NB: попытка через liderra-exclusions.conf (id:1900200) НЕ сработала — фикс работает только в crs-setup.conf.

Проверка боевого режима

curl -s -o /dev/null -w "%{http_code}\n" https://liderra.ru/.env          # → 403 (WAF блок)
curl -s -o /dev/null -w "%{http_code}\n" "https://liderra.ru/?x=<script>" # → 403
curl -s -o /dev/null -w "%{http_code}\n" -X DELETE https://liderra.ru/api/projects/2  # → 419/405 (app, НЕ 403)
sudo grep "Access denied" /var/log/modsec_audit.log   # периодически: не режет ли WAF реальное

Future cleanup (не срочно): поставщик шлёт вебхуки на IP 111.88.246.137, а не на домен liderra.ru (отсюда вечный сигнал 920350 «Host = числовой IP»). Попросить поставщика сменить URL на домен — чище, но не критично (исключение покрывает).


SEC-6 (CSP) — Content-Security-Policy боевой режим

Сначала был Content-Security-Policy-Report-Only, затем переведён в боевой Content-Security-Policy (бэкапы liderra.bak-*). Политика в :443:

add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; object-src 'none'" always;

Обоснование директив: инлайн-скриптов в blade нет (только @vite, Vite-prod их не инжектит — verified) → script-src 'self'; шрифты Inter/JetBrains Mono грузятся с Google Fonts через @import в build CSS → style-src ...fonts.googleapis.com + font-src ...fonts.gstatic.com; img-src ... https: — запас под внешние картинки на authed-страницах.

Проверка: статически (build CSS @import googleapis + woff с gstatic) + браузерная (Playwright на живом /login под боевым CSP → 0 CSP-ошибок, шрифты 200, SPA грузится).

Усилить позже: убрать 'unsafe-inline' из style-src (нужны nonce для Vuetify — нетривиально); сузить img-src после аудита authed-страниц.


SEC-4 — мониторинг + алертинг (лёгкий)

/usr/local/bin/liderra-security-report.sh + cron /etc/cron.d/liderra-security-report (root, ежедневно 07:00 → лог /var/log/liderra-security-report.log, self-trim 3000 строк): диск/память, срок TLS-сертификата (дни), баны fail2ban (ssh+web), неудачные SSH/24ч, nginx 5xx/401, БД up, счётчик WAF-блоков ([waf-blocks]), счётчик pgaudit-строк.

Email-алертинг: /usr/local/bin/liderra-mail.py (python3 smtplib, читает MAIL_* из /var/www/liderra/app/.env; SMTP_SSL smtp.yandex.ru:465; пароль не печатает). Отчёт 07:00 шлётся на kdv1@bk.ru. (Первые письма могут попасть в «Спам».)

Счётчик 5xx: в отчёте используется grep -c '" 5[0-9][0-9] ' (якорь-кавычка = реальный статус сразу после строки запроса). Без кавычки (' 5[0-9][0-9] ') ловило размеры ответов в байтах — давало ложные «5xx».

Sentry — ⏸ DEFERRED (2 ГБ RAM мало для self-host ~4 ГБ+; pending Б-1 / сервер помощнее).


SEC-7 — бэкапы + off-site (через почту)

/usr/local/bin/liderra-backup.sh + cron /etc/cron.d/liderra-backup (root, ежедневно 03:30, лог /var/log/liderra-backup.log):

  1. pg_dump -Fc БД liderra/home/ubuntu/backups/liderra-daily-<TS>.dump, retention 14 дней.
  2. Off-site (промежуточный): шифрует копию (gzip | openssl enc -aes-256-cbc -salt -pbkdf2 -pass file:/root/liderra-backup-crypt.key) и шлёт вложением на kdv1@bk.ru — копия переживёт потерю VM, ПДн зашифрованы. Шаг best-effort (не валит бэкап).

Локальные бэкапы на той же VM защищают от порчи данных/миграций/app-ransomware, но НЕ от потери VM — для этого и off-site-копия на почту.

Расшифровать emailed-бэкап:

openssl enc -d -aes-256-cbc -pbkdf2 -pass file:<key> -in <file> | gunzip > liderra.dump

⚠️ Ключ /root/liderra-backup-crypt.key (root, 600) создан один раз и переиспользуется. Заказчику — сохранить ключ ВНЕ сервера (sudo cat /root/liderra-backup-crypt.key → менеджер паролей), иначе emailed-бэкапы не расшифровать.

Полный off-site → YC Object Storage — отложен (на VM нет yc/сервис-аккаунта).

IR-runbook (регламент реагирования) — отдельным документом не формализован; реюз operations:runbook #51 при необходимости.


SEC-5 — хранилище секретов (Lockbox) 🟦 частично

Через yc на сервере (значения секретов читались файл→payload→облако, не печатались): создан KMS-ключ liderra-secrets-key (AES-256, ротация год) + Lockbox-секрет liderra-secrets (KMS-encrypted, ACTIVE) с 8 entry (роли БД + basic_auth + 2× supplier_webhook_secret). Источник — /home/ubuntu/liderra-secrets.txt. Цена Lockbox+KMS ~2550 ₽/мес.

App-интеграция — ⏸ БЛОКИРОВАНА. Приложение всё ещё читает секреты из файла + .env. Чтобы достроить, нужно:

  1. YC сервис-аккаунт (роль lockbox.payloadViewer), привязанный к VM — требует доступа к YC-консоли (его нет).
  2. Код-провайдер чтения секретов из Lockbox с fallback на .env (риск: если чтение Lockbox упадёт на старте — приложение без пароля БД ляжет).
  3. Деплой копированием.

Не делать без сервис-аккаунта и без fallback. Сейчас секрет лежит в ДВУХ местах (файл + Lockbox) — выигрыш будет только после интеграции.


SEC-3 — DDoS-защита ⏸ отложено (решение заказчика по цене)

Разведка через yc CLI: внешний IP 111.88.246.137 уже статический (reserved), но без DDoS-провайдера — продвинутую YC DDoS на существующий IP не добавить, нужен новый защищённый IP → смена DNS. Цена: платная подписка (тариф Professional+) + 976 ₽/Мбит/с свыше 10 Мбит/с — дорого/избыточно для портала.

Базовая сетевая DDoS (L3/L4) уже бесплатно активна. Решение заказчика 22.05: платный YC-DDoS не брать.

Альтернатива на будущее — бесплатный Cloudflare перед сайтом (DDoS + WAF + CDN, DNS на CF).


Доступ к Yandex Cloud + ручные действия заказчика

Доступ YC (22.05): заказчик дал OAuth-токен (сервисный аккаунт создать не вышло — навигация консоли глючила). Токен засветился в скриншоте переписки → подлежит отзыву (Яндекс ID → отключить «Yandex Cloud CLI»). Для будущей YC-работы (напр. app-интеграция Lockbox) — завести сервисный аккаунт со scoped-ролями (vpc/compute/lockbox.admin), не OAuth.

Ручные действия заказчика (вне сервера):

  1. Отозвать засветившийся OAuth-токен Яндекс-облака (Яндекс ID → «Yandex Cloud CLI»).
  2. Удалить C:\yc-oauth.txt + папку C:\yc\ (харнесс не дал удалить — защита корня диска C:).
  3. Сохранить ключ шифрования бэкапов вне сервера (/root/liderra-backup-crypt.key), иначе emailed-бэкапы не расшифровать.

Что ещё осталось (security follow-ups)

  • Усилить CSP — убрать 'unsafe-inline' из style-src (nonce для Vuetify).
  • Cloudflare перед сайтом (бесплатная альтернатива SEC-3 DDoS).
  • Lockbox app-интеграция + off-site → YC Object Storage — после получения YC сервис-аккаунта.
  • Sentry — после перехода на сервер помощнее (Б-1).
  • Прогон сканера уязвимостей (Nuclei #69 / ZAP #68 / Ward #70) по боевому порталу.

Связано: memory/project_server_hardening.md, memory/project_a8_infosec.md, ADR-014, docs/security/pgaudit-anonymizer-setup.md.