release: 2.5.3: audit stage 3 - persistence and restore
test / test (push) Canceled after 0s

Cut without asking, as the owner said on 2026-10-15: "Вопросы про резку не
задавай, просто режь."

A restore is ended by its Resync: the manifest stays until the first start
has regenerated the maps (M1). Every archive ends with data/contents.json and
the first start after a restore compares /data with it before it opens the
database: half an archive, a damaged file, a journal left beside the
database is refused, named. The entrypoint hands every map under
/data/postfix to the panel (a restore unpacked as root left the DMARC maps
unreadable - found by the review). Migration 0010: the send log's sweep,
refusal-line check, level-2 count and pages each by its index, pinned by
EXPLAIN tests on the statements the store runs (M2). The backup CLI refuses
a missing database (M3) and a password under 12 (M5), writes -o whole,
checks -decrypt's password before the output (L1), warns of what it leaves
out and refuses an empty certs/ (L2). An encrypted file may ask scrypt for
128 MiB at most, r <= 64, p <= 2, and nothing may follow its last chunk
(M4, L3). TRUSTED_PROXY_CIDR's bare ::ffff:a.b.c.d is one host (L4). Maps
are durable across a power loss: the directory is synced (L7). Commits
8e3241e, 508ac96, 242515c, 76b21b3.

Rolled out to the test server as 2.5.2-post.76b21b3 (the same code):
migration 0010 applied, schema 10, the four new indexes present; all
programs RUNNING, Postfix with its three milters, nothing in the logs. On
the live container: a CLI backup ends with data/contents.json, a wrong
SELFPOST_DB_PATH is refused and creates nothing, a short password is
refused.

CHANGELOG cut; pin 2.5.3 in deploy/docker-compose.yml, README, guide (and
its sample restore message); development.md "from pin" and the stage's row;
roadmap row and recommended order; stage 3 marked shipped in the plan. No
tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-15 10:44:08 +03:00
co-authored by Claude Opus 5.5
parent 76b21b3597
commit 48832d223a
7 changed files with 39 additions and 29 deletions
+25 -15
View File
@@ -5,7 +5,31 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
## [Unreleased] ## [Unreleased]
Migration `0010` (indexes only). ## [2.5.3] - 2026-10-15
Stage 3 of the 2026-10-12 audit: persistence and restore. A restore is
ended by its Resync and refuses an archive unpacked in part; the send log
answers its frequent questions from indexes; the backup CLI refuses a
missing database and a short password and writes its file whole; an
encrypted file is bounded in what it may ask before its password is
checked. Migration `0010` (indexes only).
### Security
- **An encrypted file could make the panel allocate 4 GiB before its
password was checked.** The key-derivation parameters an encrypted backup
or export carries were capped so loosely that a crafted header, uploaded
to *Import a domain*, asked scrypt for 4 GiB and sixteen times the work.
A header may now ask for 128 MiB at most - four times what SelfPost
writes, with the rest of scrypt's memory under 32 KiB - and is refused as
damaged otherwise. Bytes after an envelope's
last block, such as a second file appended to it, are refused too (audit
2026-10-12, M4, L3).
- **`TRUSTED_PROXY_CIDR=::ffff:10.0.0.1` trusted the wrong addresses.** A
bare IPv4-mapped address written as IPv6 was given "/32", which made it
`::/32`: every address beginning with 32 zero bits, `::1` among them, and
not the proxy meant. A bare address is now one host however it is written
(audit 2026-10-12, L4).
### Fixed ### Fixed
@@ -38,20 +62,6 @@ Migration `0010` (indexes only).
error, the panel's log), and a `certs/` with no file in it is refused: the error, the panel's log), and a `certs/` with no file in it is refused: the
archive would restore a server without its certificate (audit archive would restore a server without its certificate (audit
2026-10-12, L2). 2026-10-12, L2).
- **`TRUSTED_PROXY_CIDR=::ffff:10.0.0.1` trusted the wrong addresses.** A
bare IPv4-mapped address written as IPv6 was given "/32", which made it
`::/32`: every address beginning with 32 zero bits, `::1` among them, and
not the proxy meant. A bare address is now one host however it is written
(audit 2026-10-12, L4).
- **An encrypted file could make the panel allocate 4 GiB before its
password was checked.** The key-derivation parameters an encrypted backup
or export carries were capped so loosely that a crafted header, uploaded
to *Import a domain*, asked scrypt for 4 GiB and sixteen times the work.
A header may now ask for 128 MiB at most - four times what SelfPost
writes, with the rest of scrypt's memory under 32 KiB - and is refused as
damaged otherwise. Bytes after an envelope's
last block, such as a second file appended to it, are refused too (audit
2026-10-12, M4, L3).
- **A power loss could bring back a map the panel had just replaced.** The - **A power loss could bring back a map the panel had just replaced.** The
files the panel writes for Postfix and OpenDKIM were synced, but not the files the panel writes for Postfix and OpenDKIM were synced, but not the
directory their rename changed, so the old file could return while the directory their rename changed, so the old file could return while the
+3 -3
View File
@@ -112,8 +112,8 @@ outbound mail will not reach the real internet without DNS, PTR, and port 25.
> local image and pulls nothing: > local image and pulls nothing:
> >
> ```sh > ```sh
> docker build -f build/Dockerfile --build-arg VERSION=2.5.2 \ > docker build -f build/Dockerfile --build-arg VERSION=2.5.3 \
> -t ghcr.io/mixeme/selfpost:2.5.2 . > -t ghcr.io/mixeme/selfpost:2.5.3 .
> ``` > ```
```sh ```sh
@@ -122,7 +122,7 @@ docker run --rm -d --name selfpost-try \
-e SELFPOST_HOSTNAME=mail.local.test \ -e SELFPOST_HOSTNAME=mail.local.test \
-e PANEL_COOKIE_SECURE=false \ -e PANEL_COOKIE_SECURE=false \
-v selfpost-try-data:/data \ -v selfpost-try-data:/data \
ghcr.io/mixeme/selfpost:2.5.2 ghcr.io/mixeme/selfpost:2.5.3
``` ```
**Get the setup URL** (pick one): **Get the setup URL** (pick one):
+1 -1
View File
@@ -23,7 +23,7 @@
services: services:
selfpost: selfpost:
image: ghcr.io/mixeme/selfpost:2.5.2 image: ghcr.io/mixeme/selfpost:2.5.3
restart: unless-stopped restart: unless-stopped
environment: environment:
SELFPOST_HOSTNAME: "${SELFPOST_HOSTNAME:?set the mail/panel hostname, e.g. mail.example.com}" SELFPOST_HOSTNAME: "${SELFPOST_HOSTNAME:?set the mail/panel hostname, e.g. mail.example.com}"
+1 -1
View File
@@ -1072,7 +1072,7 @@ those after it move up one patch number each.
CHANGELOG. - **Sonnet** CHANGELOG. - **Sonnet**
*Done 2026-10-15* with the port (`2cf118c` the help mockup, `3584adc`). *Done 2026-10-15* with the port (`2cf118c` the help mockup, `3584adc`).
### Stage 3 — persistence and restore (M1–M5, L1–L4, L7) → `2.5.3` ### Stage 3 — persistence and restore (M1–M5, L1–L4, L7) → `2.5.3` (shipped 2026-10-15)
- [x] **Consume the manifest after the resync succeeds** (`CheckRestore` reports; - [x] **Consume the manifest after the resync succeeds** (`CheckRestore` reports;
`run` deletes after `resyncAfterRestore` returns nil). Test: Resync fails → `run` deletes after `resyncAfterRestore` returns nil). Test: Resync fails →
+2 -2
View File
@@ -107,7 +107,7 @@ than the default «meaningful step» rule in [§ Commits and release build](#com
2. Git tag `vX.Y.Z` and publish the GitHub Release only on explicit request (see 2. Git tag `vX.Y.Z` and publish the GitHub Release only on explicit request (see
[§ Release image](#release-image)). [§ Release image](#release-image)).
**Planned version cuts** (from pin `2.5.2`; adjust if semver changes mid-track): **Planned version cuts** (from pin `2.5.3`; adjust if semver changes mid-track):
| Stage | ID | Cut | | Stage | ID | Cut |
|---|---|---| |---|---|---|
@@ -134,7 +134,7 @@ than the default «meaningful step» rule in [§ Commits and release build](#com
| 21 | audit-2026-10-12 stages 1 and 2 | `2.5.0` | | 21 | audit-2026-10-12 stages 1 and 2 | `2.5.0` |
| 22 | audit-2026-10-12 stage 2b: the log tailer | `2.5.1` | | 22 | audit-2026-10-12 stage 2b: the log tailer | `2.5.1` |
| 23 | audit-2026-10-12 stage 2c: the sender check reads From: as OpenDKIM does | `2.5.2` | | 23 | audit-2026-10-12 stage 2c: the sender check reads From: as OpenDKIM does | `2.5.2` |
| 24 | audit-2026-10-12 stage 3 | `2.5.3` | | 24 | audit-2026-10-12 stage 3: persistence and restore | `2.5.3` |
| 25 | audit-2026-10-12 stage 4 | `2.5.4` | | 25 | audit-2026-10-12 stage 4 | `2.5.4` |
| 26 | audit-2026-10-12 stage 5 | `2.6.0` | | 26 | audit-2026-10-12 stage 5 | `2.6.0` |
+4 -4
View File
@@ -190,14 +190,14 @@ cat ./data/setup-token
#### Fixed image tag #### Fixed image tag
`deploy/docker-compose.yml` pins an explicit version (`ghcr.io/mixeme/selfpost:X.Y.Z`), `deploy/docker-compose.yml` pins an explicit version (`ghcr.io/mixeme/selfpost:X.Y.Z`),
deliberately never `:latest`. The current pin is `2.5.2`. **No 2.x image is deliberately never `:latest`. The current pin is `2.5.3`. **No 2.x image is
published on ghcr.io yet** (the newest there is `1.3.0`); until one is, build published on ghcr.io yet** (the newest there is `1.3.0`); until one is, build
it from the checkout under the pinned name, and Compose uses the local image it from the checkout under the pinned name, and Compose uses the local image
instead of pulling: instead of pulling:
```sh ```sh
docker build -f build/Dockerfile --build-arg VERSION=2.5.2 \ docker build -f build/Dockerfile --build-arg VERSION=2.5.3 \
-t ghcr.io/mixeme/selfpost:2.5.2 . -t ghcr.io/mixeme/selfpost:2.5.3 .
``` ```
`VERSION` must equal the tag: it is the version the backup check compares `VERSION` must equal the tag: it is the version the backup check compares
@@ -857,7 +857,7 @@ A version mismatch at step 5 refuses to start and leaves `/data` untouched —
the panel exits with a message naming the tag to use, e.g.: the panel exits with a message naming the tag to use, e.g.:
``` ```
backup: this backup was created by SelfPost 2.5.1 but this image is 2.5.2 — restore into the matching image (selfpost:2.5.1) backup: this backup was created by SelfPost 2.5.2 but this image is 2.5.3 — restore into the matching image (selfpost:2.5.2)
``` ```
Fix the tag in `docker-compose.yml` (build that tag as under [Fixed image Fix the tag in `docker-compose.yml` (build that tag as under [Fixed image
+3 -3
View File
@@ -42,7 +42,7 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md).
| from-header-binding | Hold a message's `From:` header against the application that sends it | shipped `2.4.0` | 9/9 | [plans/from-header-binding.md](plans/from-header-binding.md) | | from-header-binding | Hold a message's `From:` header against the application that sends it | shipped `2.4.0` | 9/9 | [plans/from-header-binding.md](plans/from-header-binding.md) |
| panel-redesign | Panel redesign (accepted mockups, design contract) | shipped `2.0.0` | 27/27 | [plans/panel-redesign.md](plans/panel-redesign.md) | | panel-redesign | Panel redesign (accepted mockups, design contract) | shipped `2.0.0` | 27/27 | [plans/panel-redesign.md](plans/panel-redesign.md) |
| schema-squash | Squash SQLite migrations into a 2.x baseline | built in panel-redesign (stage 1) | — | [plans/panel-redesign.md](plans/panel-redesign.md) | | schema-squash | Squash SQLite migrations into a 2.x baseline | built in panel-redesign (stage 1) | — | [plans/panel-redesign.md](plans/panel-redesign.md) |
| audit-2026-10-12 | Follow-ups of the 2026-10-12 full-tree code audit, seven stages | in progress (stage 0 shipped `2.4.2`, stages 1+2 `2.5.0`, 2b `2.5.1`, 2c `2.5.2`; next stage 3 → `2.5.3`) | 55/81 | [audit-2026-10-12.md](audit-2026-10-12.md) | | audit-2026-10-12 | Follow-ups of the 2026-10-12 full-tree code audit, seven stages | in progress (stage 0 shipped `2.4.2`, stages 1+2 `2.5.0`, 2b `2.5.1`, 2c `2.5.2`, 3 `2.5.3`; next stage 4 → `2.5.4`) | 55/81 | [audit-2026-10-12.md](audit-2026-10-12.md) |
**Recommended order** (not binding; owner, 2026-10-09: feature order follows **Recommended order** (not binding; owner, 2026-10-09: feature order follows
development convenience): **panel-redesign** shipped as `2.0.0` (2026-10-11) and development convenience): **panel-redesign** shipped as `2.0.0` (2026-10-11) and
@@ -51,8 +51,8 @@ kit — styling its two screens once instead of twice. **delivery-log-storage**
shipped as `2.2.0` the same day, **panel-notifications** as `2.3.0` and shipped as `2.2.0` the same day, **panel-notifications** as `2.3.0` and
**from-header-binding** as `2.4.0`. Stage 0 of **from-header-binding** as `2.4.0`. Stage 0 of
[audit-2026-10-12](audit-2026-10-12.md) shipped as `2.4.2` (2026-10-14), its [audit-2026-10-12](audit-2026-10-12.md) shipped as `2.4.2` (2026-10-14), its
stages 1 and 2 as `2.5.0` the same day, stage 2b as `2.5.1`, and stage 2c as stages 1 and 2 as `2.5.0` the same day, stage 2b as `2.5.1`, stage 2c as
`2.5.2` (2026-10-15). `2.5.2` (2026-10-15), and stage 3 as `2.5.3` the same day.
Nothing is agreed after it: every remaining Nothing is agreed after it: every remaining
item is a candidate. panel-docs shipped in item is a candidate. panel-docs shipped in
[CHANGELOG.md](../CHANGELOG.md) `[1.8.0]`; dmarc-reports in `[1.7.0]` [CHANGELOG.md](../CHANGELOG.md) `[1.8.0]`; dmarc-reports in `[1.7.0]`