diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md index f95fbdf..88971c1 100644 --- a/docs/implementation-plan.md +++ b/docs/implementation-plan.md @@ -22,11 +22,11 @@ ### A. Безопасность — hardening сверх обязательного 7.6 -1. **Rate-limit за обратным прокси кеился по `RemoteAddr`** ([internal/web/web.go](../internal/web/web.go) `clientIP`). **Решено:** вариант (б) — парсить `X-Forwarded-For`, но только когда прямой peer (`RemoteAddr`) входит в `TRUSTED_PROXY_CIDR` (список CIDR через запятую, env, по умолчанию пусто); тогда используется последний элемент XFF (адрес, добавленный самим доверенным прокси). Без настройки `TRUSTED_PROXY_CIDR` поведение не меняется (лимит по `RemoteAddr`, глобальный за прокси). См. `deploy/.env.example`. -2. **Нет security-заголовков ответа** — панель не шлёт `Strict-Transport-Security`, `Content-Security-Policy`, `X-Frame-Options`/`frame-ancestors`, `X-Content-Type-Options: nosniff`, `Referrer-Policy`. XSS уже закрыт автоэкранированием `html/template` (7.6.7), CSRF — `SameSite=Lax`, но заголовки — дешёвый второй эшелон (clickjacking, downgrade, sniffing). **Решено:** эмитить из панели (единый мидлварь, ~10 строк), не перекладывать на reverse-proxy. Общий принцип: всю сложность стараемся держать в сервисе, а конфигурация reverse-proxy должна оставаться максимально простой, чтобы её было сложно сломать неудачной правкой. +1. **Rate-limit за обратным прокси кеился по `RemoteAddr`** ([internal/web/web.go](../internal/web/web.go) `clientIP`). **Решено и реализовано:** вариант (б) — парсить `X-Forwarded-For`, но только когда прямой peer (`RemoteAddr`) входит в `TRUSTED_PROXY_CIDR` (список CIDR через запятую, env, по умолчанию пусто); тогда используется последний элемент XFF (адрес, добавленный самим доверенным прокси). Без настройки `TRUSTED_PROXY_CIDR` поведение не меняется (лимит по `RemoteAddr`, глобальный за прокси). См. `deploy/.env.example`. +2. **Нет security-заголовков ответа** — панель не шлёт `Strict-Transport-Security`, `Content-Security-Policy`, `X-Frame-Options`/`frame-ancestors`, `X-Content-Type-Options: nosniff`, `Referrer-Policy`. XSS уже закрыт автоэкранированием `html/template` (7.6.7), CSRF — `SameSite=Lax`, но заголовки — дешёвый второй эшелон (clickjacking, downgrade, sniffing). **Решено:** эмитить из панели (единый мидлварь, ~10 строк), не перекладывать на reverse-proxy. Общий принцип: всю сложность стараемся держать в сервисе, а конфигурация reverse-proxy должна оставаться максимально простой, чтобы её было сложно сломать неудачной правкой. **Реализация — см. Фазу 14.A.** 3. **CSRF — только `SameSite=Lax`, без токенов.** Достаточно для современных браузеров (все мутации — POST, все GET read-only), но не защищает при downgrade до старого браузера/особых прокси и не даёт защиты на уровне «per-request». **Вопрос:** считать `SameSite=Lax` достаточным для single-admin панели (моя рекомендация — да) или добавить double-submit CSRF-токен. 4. **Cookie без префикса `__Host-`.** Сейчас `selfpost_session` (`Secure`/`HttpOnly`/`SameSite=Lax`/`Path=/`). Префикс `__Host-` дал бы браузерный гарант «только HTTPS, только этот хост, без Domain». Мелочь, но бесплатная. **Вопрос:** переименовать (учесть dev-режим `PANEL_COOKIE_SECURE=false` — `__Host-` требует `Secure`, т.е. только когда secure включён). -5. **Setup-ссылка печатается в stdout контейнера.** По ТЗ (7.6.1) — так и задумано, но если логи контейнера уезжают в агрегатор, токен там осядет на 10 минут. Файл `/data/setup-token` (0600) — альтернатива. **Решено:** базовый вариант — stdout (по ТЗ), без изменений в коде. В пользовательской документации отдельно осветить этот вопрос и предложить более защищённые альтернативы (например, файл `/data/setup-token`) — для тех, у кого логи контейнера уезжают в централизованный агрегатор. +5. **Setup-ссылка печатается в stdout контейнера.** По ТЗ (7.6.1) — так и задумано, но если логи контейнера уезжают в агрегатор, токен там осядет на 10 минут. Файл `/data/setup-token` (0600) — альтернатива, уже реализована в коде ([internal/web/setup.go](../internal/web/setup.go) `announce`/token file). **Решено:** базовый вариант объявления — stdout (по ТЗ), код не меняется. Остаётся только осветить это в пользовательской документации и указать на уже существующий файл `/data/setup-token` как более защищённую альтернативу для тех, у кого логи контейнера уезжают в централизованный агрегатор. **Реализация — см. Фазу 14.B.** 6. **Нет 2FA / смены пароля админа / нескольких админов из UI.** ТЗ этого не требует (один админ, secret-link). Смена пароля сейчас — только пересоздание состояния. **Решено:** добавить раздел настроек аккаунта (логин + пароль) — см. Фазу 12 ниже. 2FA и мульти-админ остаются явно 2.x/вне объёма. ### B. Надёжность и эксплуатация @@ -37,7 +37,7 @@ ### C. CI и тесты -10. **CI не гоняет `go test`.** [.github/workflows/release.yml](../.github/workflows/release.yml) на теге только собирает и пушит образ; `go vet` выполняется внутри Dockerfile-сборки, но **юнит-тесты в CI не запускаются** — вся тестовая проверка идёт вручную на dev-сервере. **Рекомендация:** добавить обычный workflow на push/PR (`go vet` + `go test ./...` + `gofmt -l`), чтобы регресс ловился до тега релиза. Небольшая работа, заметно повышает доверие к «зелёному» релизу. +10. **CI не гоняет `go test`.** [.github/workflows/release.yml](../.github/workflows/release.yml) на теге только собирает и пушит образ; `go vet` выполняется внутри Dockerfile-сборки, но юнит-тесты в CI не запускались — вся тестовая проверка шла вручную на dev-сервере. **Реализовано:** добавлен обычный workflow на push/PR (`go vet` + `go test ./...`). 11. **Нет интеграционного/e2e-теста в CI.** Контейнерные e2e каждой фазы прогонялись вручную и задокументированы в git-истории, но не автоматизированы. Для v1.0 — вероятно, оставить ручными; для долгой поддержки — кандидат на smoke-тест (поднять контейнер, setup→login→add domain→auth SMTP) в CI. ### D. Указатель на объём 2.x @@ -241,6 +241,50 @@ password» (строки 17–27) показывает только логин/ --- +## Фаза 14 (v1.x) — Реализация принятых решений по hardening (раздел A) + +**Статус:** запланирована, не начата. + +**Цель:** довести до кода два уже принятых, но пока не реализованных решения из +раздела [A. Безопасность](#a-безопасность--hardening-сверх-обязательного-76) +(остальные пункты раздела A либо уже реализованы — п.1, либо являются открытыми +вопросами без решения — п.3/4, либо уже вынесены в свою фазу — п.6/Фаза 12). + +### A. Security-заголовки ответа (пункт A.2) + +Добавить в панель единый middleware, оборачивающий все ответы (кроме, возможно, +уже застриманных HTMX-фрагментов, где это не мешает), выставляющий: + +- `Strict-Transport-Security` (только когда `PANEL_COOKIE_SECURE`/TLS включён — по аналогии с `__Host-`/`Secure`-логикой, HSTS на голом HTTP в dev-режиме бессмысленен и может быть вреден); +- `X-Content-Type-Options: nosniff`; +- `X-Frame-Options: DENY` (или `Content-Security-Policy: frame-ancestors 'none'` — эквивалент, дублировать не обязательно); +- `Referrer-Policy: same-origin` (или `no-referrer` — выбрать более строгий, поведение панели не зависит от referrer); +- `Content-Security-Policy` — минимальная политика под текущий фронтенд (inline-стили/скрипты, HTMX): нужно свериться с шаблонами ([internal/web/templates](../internal/web/templates)) на предмет `