docs: move the accepted security risks into docs/security.md
The plan holds undone work; an accepted risk is a decision, not a task — it has no place in a queue, only a condition for revisiting it. Both risks (POST with neither Sec-Fetch-Site nor Origin, no session-bound CSRF tokens) move verbatim into a new docs/security.md, which also states where D.5 findings land. Section letters and item numbering in the plan stay as they were, since progress.md and the commit history reference them; a note in their place points at the new file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,8 +10,9 @@ signing, and is configured once through the panel. It is **outbound only** — i
|
||||
does not receive mail, provide mailboxes, or offer webmail.
|
||||
|
||||
> **Status: under active development.** See [docs/specification.md](docs/specification.md)
|
||||
> for the full requirements and [docs/implementation-plan.md](docs/implementation-plan.md)
|
||||
> for the phased build plan.
|
||||
> for the full requirements, [docs/implementation-plan.md](docs/implementation-plan.md)
|
||||
> for the phased build plan, and [docs/security.md](docs/security.md) for the
|
||||
> security trade-offs that were accepted knowingly.
|
||||
|
||||
## Requirements (site checklist)
|
||||
|
||||
|
||||
@@ -17,27 +17,10 @@
|
||||
пункт — решение «делаем в v1.x / откладываем в 2.x / оставляем как есть»,
|
||||
принимается пользователем.
|
||||
|
||||
### A. Безопасность — принятые риски
|
||||
|
||||
Hardening сверх обязательного 7.6 закрыт. Здесь остаётся только то, что закрыто
|
||||
**сознательно не было**, чтобы это не потерялось:
|
||||
|
||||
- **Принятый риск: `POST` без `Sec-Fetch-Site` и без `Origin` пропускается.**
|
||||
Клиент, не посылающий ни одного из двух — по-настоящему старый браузер или
|
||||
webview с замороженным движком, — остаётся уязвим к CSRF с любого сайта.
|
||||
Принято сознательно: панель однопользовательская, админ выбирает браузер
|
||||
сам, а строгий режим не «защитил бы» такой клиент, а просто сломал бы в нём
|
||||
панель. Ужесточение — одна строка в `originAllowed`
|
||||
([internal/web/security.go](../internal/web/security.go)): вернуть `false`
|
||||
вместо `true` в ветке «нет обоих заголовков».
|
||||
- **CSRF-токены, привязанные к сессии, не делаются.** Проверка origin
|
||||
закрывает соседний поддомен, но зависит от поведения браузера; токен — нет.
|
||||
Цена — скрытое поле примерно в двух десятках форм. Триггером вернуться к
|
||||
вопросу считать появление требования «устойчиво независимо от браузера».
|
||||
От XSS внутри самой панели не спас бы и токен: код, исполняющийся в origin
|
||||
панели, отправит запрос сам — против этого работают автоэкранирование
|
||||
`html/template` (7.6.7) и CSP, поэтому шаблоны не должны содержать
|
||||
inline-скриптов и inline-стилей.
|
||||
**Раздел A (принятые риски безопасности) переехал в
|
||||
[security.md](security.md)** — риск не задача, а решение, и в плане несделанной
|
||||
работы ему делать нечего. Буквы разделов и сквозная нумерация пунктов ниже
|
||||
оставлены как были: на них ссылаются `progress.md`, коммиты и обсуждения.
|
||||
|
||||
### B. Надёжность и эксплуатация
|
||||
|
||||
@@ -132,7 +115,7 @@ Hardening сверх обязательного 7.6 закрыт. Здесь о
|
||||
|
||||
**Модель — Fable, и это сознательно не Opus:** B и C пишет Opus, а проверка собственной работы систематически слабее независимой. Правило progress.md «безопасность/инфра → Opus» этим не отменяется — оно про написание кода, здесь речь про ревизию. Форма прогона: `/security-review` по изменениям, пока они ещё в ветке (скилл смотрит диф), плюс отдельный ручной проход по 7.6 целиком.
|
||||
|
||||
**Что с находками.** Каждая закрывается явно: правка до тега либо запись в раздел A как принятый риск с обоснованием (там уже два таких). «Посмотрели и ладно» закрытием не считается. **Гейт:** вместе с e2e из C.4 — до тега; находка класса «эксплуатируется снаружи» тег откладывает.
|
||||
**Что с находками.** Каждая закрывается явно: правка до тега либо запись в [security.md](security.md) как принятый риск с обоснованием (там уже два таких). «Посмотрели и ладно» закрытием не считается. **Гейт:** вместе с e2e из C.4 — до тега; находка класса «эксплуатируется снаружи» тег откладывает.
|
||||
|
||||
### E. Указатель на объём 2.x
|
||||
|
||||
|
||||
+4
-3
@@ -2,13 +2,14 @@
|
||||
|
||||
Живой трекер состояния. **Переживает `/clear`** — читается первым при возобновлении работы.
|
||||
План (открытые вопросы + опциональные фазы): [implementation-plan.md](implementation-plan.md).
|
||||
ТЗ: [specification.md](specification.md). История релизов: [CHANGELOG.md](../CHANGELOG.md).
|
||||
ТЗ: [specification.md](specification.md). Принятые риски безопасности: [security.md](security.md).
|
||||
История релизов: [CHANGELOG.md](../CHANGELOG.md).
|
||||
История сделанного по фазам (0→13, все закрыты) — в `git log` и в CHANGELOG, здесь не дублируется.
|
||||
|
||||
## Как возобновить после сброса контекста
|
||||
|
||||
1. Прочитать этот файл (текущее состояние, что дальше).
|
||||
2. Открыть `implementation-plan.md` — там нерешённые вопросы, принятые риски и опциональная линия 2.x.x (Фаза O1+).
|
||||
2. Открыть `implementation-plan.md` — там нерешённые вопросы и опциональная линия 2.x.x (Фаза O1+); принятые риски безопасности — в `security.md`.
|
||||
3. При необходимости — детали в `specification.md`.
|
||||
4. Продолжить с пункта «Следующий шаг».
|
||||
|
||||
@@ -38,7 +39,7 @@
|
||||
|
||||
- **Выполнено и принято:** базовый линейный план 0→11 (v1.0; аудит безопасности ТЗ 7.6 — полное соответствие), Фаза 12 (UI/UX), Фаза 13 (страница `/status`, DNS-проверки домена) и Фаза 14 (security-заголовки, проверка origin, cookie `__Host-` + обнаружение дублей, документация про `/data/setup-token`). Что именно сделано — в `git log` и `CHANGELOG.md`, здесь не дублируется.
|
||||
- **Решено, но ещё не реализовано:** пункты **B.1**, **B.2**, **B.3** и **C.4** плана, именно в этом порядке. B.1 — сессии переезжают в SQLite (скользящий срок бездействия 7 дней, `PANEL_SESSION_IDLE_DAYS`, опросы не продлевают, смена пароля разлогинивает всех). B.2 — ротация `mail.log` уходит с `copytruncate` на «переименовать + `postfix reload`» (правки в `logrotate-mail.conf`, `follow()` в `internal/logtail`, `readLogTail()` в `internal/web`; на стенде проверить, что после reload новый `mail.log` создаётся). B.3 — незаданный `SELFPOST_HOSTNAME` роняет контейнер в `entrypoint.sh` с развёрнутым текстом ошибки плюс синтаксическая проверка значения. C.4 — герметичный контейнерный e2e отдельным Go-модулем `test/e2e/` поверх поставляемого compose, гейт перед публикацией образа по тегу, нативная матрица amd64/arm64 вместо qemu в `release.yml`; делается **после** B.1–B.3, стендовые проверки B.1/B.3 переезжают в него регрессиями. Параметры всех четырёх согласованы полностью, кода нет: делать отдельными шагами. Замыкает очередь **D.5** — предрелизная проверка на уязвимости моделью Fable по всему дифу от `v1.0.0` плюс повторный проход по ТЗ 7.6; вместе с e2e это гейт перед тегом.
|
||||
- **Дальше — то, что перечислено в `implementation-plan.md`:** открытые вопросы закрыты, раздел E теперь только указатель на объём 2.x (входящий релей O1+ и роль администратора домена; 2FA снята с рассмотрения); остаются принятые риски раздела A (`POST` без `Sec-Fetch-Site`/`Origin` пропускается, токенов нет) и опциональная **Фаза O1+** (входящий релей, линия 2.x.x, требует согласования).
|
||||
- **Дальше — то, что перечислено в `implementation-plan.md`:** открытые вопросы закрыты, раздел E теперь только указатель на объём 2.x (входящий релей O1+ и роль администратора домена; 2FA снята с рассмотрения); остаются принятые риски безопасности (переехали в [security.md](security.md): `POST` без `Sec-Fetch-Site`/`Origin` пропускается, CSRF-токенов нет) и опциональная **Фаза O1+** (входящий релей, линия 2.x.x, требует согласования).
|
||||
- **Прод:** `selfpost.example.com`, реальный Let's Encrypt сертификат, живой e2e (DKIM/SPF pass). Контейнер там всё ещё на образе v1.0 — Фаза 14 в него не выкатывалась. При апгрейде: админа один раз разлогинит (сменилось имя cookie), а от reverse-proxy требуется передача исходного `Host` (Apache-фрагмент из `deploy/` это делает).
|
||||
|
||||
## Рабочая петля (dev loop) — ВАЖНО
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Безопасность: принятые риски
|
||||
|
||||
**Что здесь.** Обязательные требования к безопасности — [ТЗ 7.6](specification.md);
|
||||
соответствие им проверено полным аудитом на v1.0 и здесь не пересказывается.
|
||||
Hardening сверх обязательного 7.6 (security-заголовки, проверка origin, cookie
|
||||
`__Host-` с обнаружением дублей — Фаза 14) тоже закрыт, история — в
|
||||
[CHANGELOG.md](../CHANGELOG.md) и `git log`. Этот документ держит третью
|
||||
категорию: то, что закрыто **сознательно не было**, чтобы решение не потерялось
|
||||
и не переоткрывалось заново.
|
||||
|
||||
Здесь, а не в [implementation-plan.md](implementation-plan.md), потому что план —
|
||||
только про несделанную работу, а принятый риск — не работа, а решение: у него нет
|
||||
состояния «в очереди», есть условие, при котором к нему возвращаются.
|
||||
|
||||
## Принятые риски
|
||||
|
||||
- **`POST` без `Sec-Fetch-Site` и без `Origin` пропускается.**
|
||||
Клиент, не посылающий ни одного из двух — по-настоящему старый браузер или
|
||||
webview с замороженным движком, — остаётся уязвим к CSRF с любого сайта.
|
||||
Принято сознательно: панель однопользовательская, админ выбирает браузер
|
||||
сам, а строгий режим не «защитил бы» такой клиент, а просто сломал бы в нём
|
||||
панель. Ужесточение — одна строка в `originAllowed`
|
||||
([internal/web/security.go](../internal/web/security.go)): вернуть `false`
|
||||
вместо `true` в ветке «нет обоих заголовков».
|
||||
- **CSRF-токены, привязанные к сессии, не делаются.** Проверка origin
|
||||
закрывает соседний поддомен, но зависит от поведения браузера; токен — нет.
|
||||
Цена — скрытое поле примерно в двух десятках форм. Триггером вернуться к
|
||||
вопросу считать появление требования «устойчиво независимо от браузера».
|
||||
От XSS внутри самой панели не спас бы и токен: код, исполняющийся в origin
|
||||
панели, отправит запрос сам — против этого работают автоэкранирование
|
||||
`html/template` (7.6.7) и CSP, поэтому шаблоны не должны содержать
|
||||
inline-скриптов и inline-стилей.
|
||||
|
||||
## Как этот список пополняется
|
||||
|
||||
Предрелизная проверка на уязвимости (пункт **D.5** плана, модель Fable) закрывает
|
||||
каждую находку одним из двух способов: правка до тега — либо запись сюда, с
|
||||
обоснованием и условием возврата, как у двух пунктов выше. Третьего варианта
|
||||
(«посмотрели и ладно») нет.
|
||||
Reference in New Issue
Block a user