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>
This commit is contained in:
@@ -1589,3 +1589,15 @@ lemed
|
||||
ретраит
|
||||
шеринге
|
||||
unactivated
|
||||
|
||||
# Серверный слой защиты SEC-1..7 (2026-05-22)
|
||||
бэкапа
|
||||
баны
|
||||
алертинг
|
||||
алертингом
|
||||
htpasswd
|
||||
ignoreip
|
||||
libnginx
|
||||
crs
|
||||
coraza
|
||||
usr
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
# Серверный слой защиты боевого сервера (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 ~25–50 ₽/мес.
|
||||
|
||||
**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`.
|
||||
File diff suppressed because one or more lines are too long
@@ -37,5 +37,14 @@
|
||||
"claude-md-management:revise-claude-md": ["L12"],
|
||||
"billing-audit": ["L13"],
|
||||
"pest": ["L13"],
|
||||
"ru-tax-accounting": ["L13"]
|
||||
"ru-tax-accounting": ["L13"],
|
||||
"security-go-live": ["L15"],
|
||||
"pdn-152fz-audit": ["L15"],
|
||||
"threat-model": ["L15"],
|
||||
"nuclei": ["L15"],
|
||||
"ward": ["L15"],
|
||||
"owasp-zap": ["L15"],
|
||||
"gitleaks": ["L15"],
|
||||
"semgrep": ["L15"],
|
||||
"trailofbits": ["L15"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user