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 <cursoragent@cursor.com>
This commit is contained in:
2026-08-05 22:07:02 +03:00
parent aaf0711326
commit 00983cce39
3 changed files with 36 additions and 13 deletions
+10 -2
View File
@@ -7,6 +7,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
### Added ### 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, - docs: `docs/code-review.md` — full codebase review (architecture, code quality,
documentation, GUI, legacy, risks) with prioritized implementation plan and documentation, GUI, legacy, risks) with prioritized implementation plan and
model routing; cross-links in `implementation-plan.md` and `progress.md`. 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 internal-variable list; `TRUSTED_PROXY_CIDR` wired through
`deploy/docker-compose.yml`. `deploy/docker-compose.yml`.
### Removed
- docs: `security.md` — accepted risk «restore old backup revives session rows»
(not a concern in operator deployment).
### Changed ### Changed
- docs: `implementation-plan.md` trimmed to the sole open v1.x gate — pre-release - 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 `mail.log` rotation (rename + `postfix reload`), `SELFPOST_HOSTNAME` startup
gate, log-tailer known gaps. gate, log-tailer known gaps.
- docs: `development.md` — expanded e2e stack and `release.yml` matrix workflow. - docs: `development.md` — expanded e2e stack and `release.yml` matrix workflow.
- docs: `security.md` — accepted risks for session rows restored from backup and - docs: `security.md` — accepted risk for send-log rows stuck at `queued` after
send-log rows stuck at `queued` after panel restart or container recreate. panel restart or container recreate.
- docs: `roadmap.md` — optional send-log / `mail.log` follow-ups under v1.x tail. - 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: `progress.md`, `documentation-plan.md` — cross-links updated for the new layout.
- docs: `documentation-plan.md` marked closed (D1D9); trimmed to package - docs: `documentation-plan.md` marked closed (D1D9); trimmed to package
+26 -7
View File
@@ -216,15 +216,17 @@ E2E покрывает: bootstrap, SMTP AUTH, DKIM, send-log lifecycle, negative
| Gap | Описание | Документировано | | Gap | Описание | Документировано |
|-----|----------|-----------------| |-----|----------|-----------------|
| Send-log `queued` forever | Log-tailer стартует с EOF; после restart пропущенный хвост не дочитывается | [security.md](security.md), [roadmap.md](roadmap.md) | | 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) | | 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) | | 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` | | 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. 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. 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** | | L1 | **Предрелизный security review** (§ D) — обязательный гейт | **P0** | **Fable** |
| L2 | Send-log gap mitigation (persist read offset / reconcile stuck rows) — опционально | P2 | Opus | | L2 | **Шифрование бэкапа и экспорта домена** (R13) — optional, checkbox + password | P1 | **Opus** + Sonnet |
| L3 | Transaction wrap для rate limit count+insert — опционально | P3 | Opus | | 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 - Fail-open journal-milter vs fail-closed OpenDKIM
- CSRF via Origin (no tokens) - CSRF via Origin (no tokens)
- `__Host-` cookie + duplicate detection - `__Host-` cookie + duplicate detection
- Plaintext passwords in domain export - Plaintext passwords in domain export**mitigation:** R13
- Backup session resurrection
- Send-log queued gap - Send-log queued gap
- SQLite single connection - SQLite single connection
- No in-container TLS - 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 | Достаточно | | **Почему chroot disabled в Postfix** | architecture.md | Достаточно |
| **Порядок supervisord** (opendkim → panel → postfix) | architecture.md | Достаточно | | **Порядок supervisord** (opendkim → panel → postfix) | architecture.md | Достаточно |
| **Почему log-tailer не persist offset** | roadmap optional | Явно в architecture.md § known limitations | | **Почему 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) **Модель:** 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 | | R6 | GUI: visibility-aware HTMX polling | Sonnet |
| R7 | CONTRIBUTING.md | Sonnet | | R7 | CONTRIBUTING.md | Sonnet |
| R8 | ADR для CSRF policy | Sonnet | | R8 | ADR для CSRF policy | Sonnet |
| R13 | Шифрование бэкапа и экспорта домена (checkbox + password) | **Opus** + Sonnet |
### v2.x (roadmap, не начинать без согласования) ### 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) 5. Sonnet: bump compose image tag + Codeberg URLs (в том же release commit)
6. Git tag vX.Y.Z 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 — Doc/code hygiene (P1)
1. Haiku: массовая замена phase-комментариев (mechanical pass) 1. Haiku: массовая замена phase-комментариев (mechanical pass)
-4
View File
@@ -89,10 +89,6 @@ Hardening сверх обязательного (security-заголовки, п
панели, отправит запрос сам — против этого работают автоэкранирование панели, отправит запрос сам — против этого работают автоэкранирование
`html/template` и CSP, поэтому шаблоны не должны содержать `html/template` и CSP, поэтому шаблоны не должны содержать
inline-скриптов и inline-стилей. inline-скриптов и inline-стилей.
- **Восстановление старого полного бэкапа возвращает строки сессий из архива.**
Сессия, разлогиненная уже после снятия бэкапа, может снова стать действительной,
если браузер всё ещё хранит cookie и idle-срок не истёк. См. [architecture.md](architecture.md)
§ Sessions.
- **Send-log может навсегда остаться `queued` после рестарта панели или - **Send-log может навсегда остаться `queued` после рестарта панели или
пересоздания контейнера.** Log-tailer стартует с конца `mail.log`; файл не в пересоздания контейнера.** Log-tailer стартует с конца `mail.log`; файл не в
`/data` и теряется при recreate. Rename-ротация это не лечит. См. [architecture.md](architecture.md) `/data` и теряется при recreate. Rename-ротация это не лечит. См. [architecture.md](architecture.md)