docs: split accepted A.2/A.5 decisions into implementable Phase 14
Security headers and setup-token docs were already decided but had no concrete implementation phase; also mark A.1 rate-limit and CI test workflow as done since they landed in recent commits.
This commit is contained in:
@@ -22,11 +22,11 @@
|
|||||||
|
|
||||||
### A. Безопасность — hardening сверх обязательного 7.6
|
### 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`.
|
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 должна оставаться максимально простой, чтобы её было сложно сломать неудачной правкой.
|
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-токен.
|
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 включён).
|
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/вне объёма.
|
6. **Нет 2FA / смены пароля админа / нескольких админов из UI.** ТЗ этого не требует (один админ, secret-link). Смена пароля сейчас — только пересоздание состояния. **Решено:** добавить раздел настроек аккаунта (логин + пароль) — см. Фазу 12 ниже. 2FA и мульти-админ остаются явно 2.x/вне объёма.
|
||||||
|
|
||||||
### B. Надёжность и эксплуатация
|
### B. Надёжность и эксплуатация
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
### C. CI и тесты
|
### 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.
|
11. **Нет интеграционного/e2e-теста в CI.** Контейнерные e2e каждой фазы прогонялись вручную и задокументированы в git-истории, но не автоматизированы. Для v1.0 — вероятно, оставить ручными; для долгой поддержки — кандидат на smoke-тест (поднять контейнер, setup→login→add domain→auth SMTP) в CI.
|
||||||
|
|
||||||
### D. Указатель на объём 2.x
|
### 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)) на предмет `<script>`/`style="..."`/`onclick` перед тем, как писать политику, чтобы не сломать текущий UI.
|
||||||
|
|
||||||
|
**Готово, когда:** все ответы панели содержат перечисленные заголовки (кроме HSTS в dev/non-secure режиме); UI (включая HTMX-фрагменты и polling) продолжает работать без консольных ошибок CSP; `gofmt`/`vet`/`test` зелёные.
|
||||||
|
|
||||||
|
**Риски:** слишком строгий CSP может тихо сломать inline-скрипты/стили в существующих шаблонах — проверить вручную в браузере (открыть каждую страницу, проверить консоль на CSP-violations) после реализации, а не полагаться только на юнит-тесты.
|
||||||
|
|
||||||
|
**Модель:** Sonnet (небольшой, хорошо специфицированный мидлварь).
|
||||||
|
|
||||||
|
### B. Документация про `/data/setup-token` (пункт A.5)
|
||||||
|
|
||||||
|
Код уже пишет setup-токен в `/data/setup-token` (0600) в дополнение к stdout
|
||||||
|
([internal/web/setup.go](../internal/web/setup.go)) — это не требует изменений.
|
||||||
|
Остаётся только документационная задача:
|
||||||
|
|
||||||
|
- В README (раздел про первый запуск/setup-ссылку) добавить абзац: по умолчанию ссылка печатается в stdout контейнера (spec 7.6.1), но токен также лежит в файле `/data/setup-token` внутри смонтированного `/data` — для тех, у кого логи контейнера уезжают в центральный агрегатор и не хочется, чтобы токен там оседал на 10 минут, безопаснее прочитать файл (`docker exec` / примонтированный volume) вместо просмотра логов.
|
||||||
|
|
||||||
|
**Готово, когда:** README содержит этот абзац рядом с описанием setup-ссылки.
|
||||||
|
|
||||||
|
**Риски:** нет — чисто документация, код не меняется.
|
||||||
|
|
||||||
|
**Модель:** Sonnet (документация).
|
||||||
|
|
||||||
|
**Зависимости:** нет, можно делать независимо от Фаз 12/13.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Опциональные фазы — целевой релиз 2.x.x (вне базового объёма v1.0)
|
## Опциональные фазы — целевой релиз 2.x.x (вне базового объёма v1.0)
|
||||||
|
|
||||||
Эти фазы **не входят** в линейный базис 0→11 и не являются частью поставки v1.0 (v1.x — только исходящий релей). Они отнесены к **релизной линии 2.x.x** и добавлены в дорожную карту как согласуемые расширения. **Реализация — только после явного согласования (ТЗ 12.6):** ТЗ v1.0 раздел 3 явно исключает приём входящей почты из объёма, поэтому включение этой функциональности — сознательное расширение границ проекта (major-релиз 2.0), а не доработка по своей инициативе. Внесение в план фиксирует намерение и дизайн; кодирование начинается отдельным решением.
|
Эти фазы **не входят** в линейный базис 0→11 и не являются частью поставки v1.0 (v1.x — только исходящий релей). Они отнесены к **релизной линии 2.x.x** и добавлены в дорожную карту как согласуемые расширения. **Реализация — только после явного согласования (ТЗ 12.6):** ТЗ v1.0 раздел 3 явно исключает приём входящей почты из объёма, поэтому включение этой функциональности — сознательное расширение границ проекта (major-релиз 2.0), а не доработка по своей инициативе. Внесение в план фиксирует намерение и дизайн; кодирование начинается отдельным решением.
|
||||||
|
|||||||
Reference in New Issue
Block a user