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

204 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Серверный слой защиты боевого сервера (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`:
```nginx
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`.
### Проверка боевого режима
```bash
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`:
```nginx
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-бэкап:**
```bash
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`.