From 00983cce393df7a08a61a76d9f83d0f79e915bca Mon Sep 17 00:00:00 2001 From: Mikhail Yenuchenko Date: Wed, 5 Aug 2026 22:07:02 +0300 Subject: [PATCH] docs: plan backup encryption and drop session-restore risk Update code-review with phase 1.5 (optional .spbk/.spde encryption, checkbox UI). Remove session resurrection from backup as an accepted risk in security.md. Co-authored-by: Cursor --- CHANGELOG.md | 12 ++++++++++-- docs/code-review.md | 33 ++++++++++++++++++++++++++------- docs/security.md | 4 ---- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d71866f..b8151e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version ### Added +- docs: `docs/code-review.md` — phase 1.5 plan for optional password encryption + of full backup (`.spbk`) and domain export (`.spde`); checkbox UI pattern; + remove session-resurrection-from-backup as accepted risk. - docs: `docs/code-review.md` — full codebase review (architecture, code quality, documentation, GUI, legacy, risks) with prioritized implementation plan and model routing; cross-links in `implementation-plan.md` and `progress.md`. @@ -39,6 +42,11 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version internal-variable list; `TRUSTED_PROXY_CIDR` wired through `deploy/docker-compose.yml`. +### Removed + +- docs: `security.md` — accepted risk «restore old backup revives session rows» + (not a concern in operator deployment). + ### Changed - docs: `implementation-plan.md` trimmed to the sole open v1.x gate — pre-release @@ -47,8 +55,8 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version `mail.log` rotation (rename + `postfix reload`), `SELFPOST_HOSTNAME` startup gate, log-tailer known gaps. - docs: `development.md` — expanded e2e stack and `release.yml` matrix workflow. -- docs: `security.md` — accepted risks for session rows restored from backup and - send-log rows stuck at `queued` after panel restart or container recreate. +- docs: `security.md` — accepted risk for send-log rows stuck at `queued` after + panel restart or container recreate. - docs: `roadmap.md` — optional send-log / `mail.log` follow-ups under v1.x tail. - docs: `progress.md`, `documentation-plan.md` — cross-links updated for the new layout. - docs: `documentation-plan.md` marked closed (D1–D9); trimmed to package diff --git a/docs/code-review.md b/docs/code-review.md index 9f3e9b7..acbec9a 100644 --- a/docs/code-review.md +++ b/docs/code-review.md @@ -216,15 +216,17 @@ E2E покрывает: bootstrap, SMTP AUTH, DKIM, send-log lifecycle, negative | Gap | Описание | Документировано | |-----|----------|-----------------| | Send-log `queued` forever | Log-tailer стартует с EOF; после restart пропущенный хвост не дочитывается | [security.md](security.md), [roadmap.md](roadmap.md) | -| Session resurrection from backup | Restore старого backup + valid cookie = old session alive | [security.md](security.md) | | CSRF without tokens | POST без Origin/Sec-Fetch-Site пропускается | [security.md](security.md) | | Fail-open L2 rate limit | DB error → mail проходит | [`internal/milter/ratelimit.go`](../internal/milter/ratelimit.go) | | Shallow SPF check | Не следует `include:`/`redirect=` | README, `internal/dnscheck/spf.go` | +| Plaintext backup/export at rest | DKIM-ключи, SASL, пароли приложений в cleartext `.tar.gz`/`.json` | **Mitigation:** R13 (optional encryption) | + +**Не риск (решение оператора):** «Session resurrection from backup» — снято из [security.md](security.md). ### Потенциальные логические нюансы (низкий приоритет) 1. **Rate limit race:** `CountMessages` + `InsertQueued` не в одной транзакции — при высокой нагрузке возможен overshoot на 1 сообщение. Для differentiated limits — acceptable. -2. **Domain export с plaintext passwords** ([`internal/domain/transfer.go`](../internal/domain/transfer.go)) — by design, но высокий риск утечки файла. +2. **Domain export с plaintext passwords** ([`internal/domain/transfer.go`](../internal/domain/transfer.go)) — by design; **mitigation:** R13 (optional password encryption). 3. **`macro()` dual lookup** ([`internal/milter/milter.go`](../internal/milter/milter.go)) — workaround для Postfix/go-milter; permanent, not a bug. ### Рекомендации @@ -232,8 +234,9 @@ E2E покрывает: bootstrap, SMTP AUTH, DKIM, send-log lifecycle, negative | # | Действие | Приоритет | Модель | |---|----------|-----------|--------| | L1 | **Предрелизный security review** (§ D) — обязательный гейт | **P0** | **Fable** | -| L2 | Send-log gap mitigation (persist read offset / reconcile stuck rows) — опционально | P2 | Opus | -| L3 | Transaction wrap для rate limit count+insert — опционально | P3 | Opus | +| L2 | **Шифрование бэкапа и экспорта домена** (R13) — optional, checkbox + password | P1 | **Opus** + Sonnet | +| L3 | Send-log gap mitigation — опционально | P2 | Opus | +| L4 | Transaction wrap для rate limit count+insert — опционально | P3 | Opus | --- @@ -308,8 +311,7 @@ Go `html/template` + HTMX polling + [`panel.css`](../internal/web/static/panel.c - Fail-open journal-milter vs fail-closed OpenDKIM - CSRF via Origin (no tokens) - `__Host-` cookie + duplicate detection -- Plaintext passwords in domain export -- Backup session resurrection +- Plaintext passwords in domain export → **mitigation:** R13 - Send-log queued gap - SQLite single connection - No in-container TLS @@ -323,7 +325,8 @@ Go `html/template` + HTMX polling + [`panel.css`](../internal/web/static/panel.c | **Почему chroot disabled в Postfix** | architecture.md | Достаточно | | **Порядок supervisord** (opendkim → panel → postfix) | architecture.md | Достаточно | | **Почему log-tailer не persist offset** | roadmap optional | Явно в architecture.md § known limitations | -| **Import domain с plaintext password** | transfer.go comment | Достаточно для v1 | +| **Import domain с plaintext password** | transfer.go comment | Достаточно для v1; шифрование — R13 | +| **Plaintext full backup / domain export at rest** | handlers_backup.go | **R13:** optional AES-GCM envelope (`.spbk`/`.spde`) | **Модель:** Sonnet (дополнить security.md / architecture.md) @@ -349,6 +352,7 @@ Go `html/template` + HTMX polling + [`panel.css`](../internal/web/static/panel.c | R6 | GUI: visibility-aware HTMX polling | Sonnet | | R7 | CONTRIBUTING.md | Sonnet | | R8 | ADR для CSRF policy | Sonnet | +| R13 | Шифрование бэкапа и экспорта домена (checkbox + password) | **Opus** + Sonnet | ### v2.x (roadmap, не начинать без согласования) @@ -380,6 +384,21 @@ Go `html/template` + HTMX polling + [`panel.css`](../internal/web/static/panel.c 5. Sonnet: bump compose image tag + Codeberg URLs (в том же release commit) 6. Git tag vX.Y.Z +### Фаза 1.5 — Шифрование резервных копий (P1, v1.x) + +**Проблема:** полный бэкап и экспорт домена содержат DKIM-ключи, SASL-креды и plaintext-пароли приложений; сейчас `.tar.gz` / `.json` без шифрования. + +**Решение:** опциональное шифрование паролем (чекбокс «Encrypt with password»; поля password + confirm — только при включённой галочке; переключение в `panel.js`, без inline script). + +| Арtefact | Cleartext | Encrypted | +|----------|-----------|-----------| +| Полный бэкап | `.tar.gz` | `.spbk` (**S**elf**P**ost **B**ac**k**up) | +| Экспорт домена | `.json` | `.spde` (**S**elf**P**ost **D**omain **E**xport) | + +**Формат:** magic `SELFPOST1`, type byte, scrypt KDF, AES-256-GCM; manifest внутри ciphertext. + +**Задачи:** E1 crypto envelope → E2 backup/CLI → E3 domain export/import → E4 UI (checkbox) → E5 docs + e2e. **Модель:** Opus (crypto), Sonnet (UI/docs). + ### Фаза 1 — Doc/code hygiene (P1) 1. Haiku: массовая замена phase-комментариев (mechanical pass) diff --git a/docs/security.md b/docs/security.md index 2e3e586..12ccc81 100644 --- a/docs/security.md +++ b/docs/security.md @@ -89,10 +89,6 @@ Hardening сверх обязательного (security-заголовки, п панели, отправит запрос сам — против этого работают автоэкранирование `html/template` и CSP, поэтому шаблоны не должны содержать inline-скриптов и inline-стилей. -- **Восстановление старого полного бэкапа возвращает строки сессий из архива.** - Сессия, разлогиненная уже после снятия бэкапа, может снова стать действительной, - если браузер всё ещё хранит cookie и idle-срок не истёк. См. [architecture.md](architecture.md) - § Sessions. - **Send-log может навсегда остаться `queued` после рестарта панели или пересоздания контейнера.** Log-tailer стартует с конца `mail.log`; файл не в `/data` и теряется при recreate. Rename-ротация это не лечит. См. [architecture.md](architecture.md)