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:
+10
-2
@@ -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 (D1–D9); trimmed to package
|
- docs: `documentation-plan.md` marked closed (D1–D9); trimmed to package
|
||||||
|
|||||||
+26
-7
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user