# Серверный слой защиты боевого сервера (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`.
