From dd9d1f330ed62ea2c017974e5da0350a39aa0429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Fri, 22 May 2026 11:09:17 +0300 Subject: [PATCH] =?UTF-8?q?docs(security):=20server-hardening=20setup-?= =?UTF-8?q?=D0=B4=D0=BE=D0=BA=20+=20SEC-1..7=20=D1=81=D1=82=D0=B0=D1=82?= =?UTF-8?q?=D1=83=D1=81=D1=8B=20=E2=86=92=20=D1=84=D0=B0=D0=BA=D1=82=20?= =?UTF-8?q?=D0=B4=D0=B5=D0=BF=D0=BB=D0=BE=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Привожу документацию в порядок после фактического развёртывания серверного слоя защиты на боевом тест-сервере 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 --- cspell-words.txt | 12 ++ docs/security/server-hardening-setup.md | 203 ++++++++++++++++++++++++ docs/Открытые_вопросы_v8_3.md | 15 +- tools/observer-chain-map.json | 11 +- 4 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 docs/security/server-hardening-setup.md diff --git a/cspell-words.txt b/cspell-words.txt index b17a1169..0050c0ef 100644 --- a/cspell-words.txt +++ b/cspell-words.txt @@ -1589,3 +1589,15 @@ lemed ретраит шеринге unactivated + +# Серверный слой защиты SEC-1..7 (2026-05-22) +бэкапа +баны +алертинг +алертингом +htpasswd +ignoreip +libnginx +crs +coraza +usr diff --git a/docs/security/server-hardening-setup.md b/docs/security/server-hardening-setup.md new file mode 100644 index 00000000..6bdfbf6a --- /dev/null +++ b/docs/security/server-hardening-setup.md @@ -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=