From 58356fb4d47ef34cc92233ed03f9768780bf4b25 Mon Sep 17 00:00:00 2001 From: Mix Date: Sat, 17 Oct 2026 23:12:55 +0300 Subject: [PATCH] release: 2.14.4: the follow-ups of the security audit of 2026-10-17 Stages 0-6 of docs/security-audit-2026-10-17.md in one cut: the high A2 (the user form asks for the account password), the seven mediums (M1, M2, M3, W1, I1, I2, A1) and the lows and nits the plan names, with three read-only security reviews of the fixes and their findings fixed. Stage H (the test server's host) is the owner's. No migration; schema 13. Upgrading: give the TLS key to group 10997; copy antispam/ again where the sidecar runs (rspamd 4.2.2, a third file); set TRUSTED_PROXY_CIDR behind Apache on the host if unset. make e2e (64 passed) and go vet + go test (26 packages) on the test server for 32f6ed6, the tree this release commit adds only pins and notes to. The pins (compose, README, guide), the guide's backup example, the planned cuts, the roadmap row and the plan's stages marked shipped. No tag. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 28 +++++++++++++++++++++------- README.md | 6 +++--- deploy/docker-compose.yml | 2 +- docs/development.md | 3 ++- docs/guide.md | 8 ++++---- docs/roadmap.md | 4 ++-- docs/security-audit-2026-10-17.md | 14 +++++++------- 7 files changed, 40 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a4b162..cc4b501 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version ## [Unreleased] +## [2.14.4] - 2026-10-17 + +The follow-ups of the security audit of 2026-10-17 +([docs/security-audit-2026-10-17.md](docs/security-audit-2026-10-17.md)), +stages 0-6: its one high (A2), its seven mediums and the lows. No migration; +schema 13. Read *Upgrading* below: the TLS key's group, and copy +`antispam/` again where the sidecar runs. + ### Upgrading - **Give the TLS private key to group 10997** (audit I1): `chgrp 10997 @@ -12,9 +20,22 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version same in whatever renews it. Group 10998, which the guide named until now, has Postfix in it; the backup keeps working with either, but only 10997 keeps the key from Postfix's daemons. +- **With the antispam sidecar, copy `antispam/` again** (audit S4): rspamd + 4.2.2, and a third file, `antispam/rspamd/worker-proxy.inc`, that the + fragment now mounts; `docker compose ... up -d` recreates the sidecar. +- **Behind Apache on the host, set `TRUSTED_PROXY_CIDR`** if it is not set + (audit A1): the guide's install step prints the gateway to put there; the + panel's log says so after the first requests when it is missing. ### Security +- **A stolen `global` session no longer takes a full backup** (security + audit 2026-10-17, A2, high): the user form set the signed-in user's own + password without the current one, which then passed the backup's password + check. Adding a user, setting a password, changing an e-mail or a role on + the user form, and deleting a user now ask for your own account password, + as a backup does, under the same limit on wrong guesses - which now + counts wrong passwords only. - **One stranger behind a shared proxy no longer locks everyone out of signing in** (audit A1, A8, A9): sign-in counts failures per address and username (a hundred per address alone), a right password no longer @@ -65,13 +86,6 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version the internet included - could read it. A new group, `backupkey` (10997), holds the panel alone; the guide, Help and the backup's refusal name it. -- **A stolen `global` session no longer takes a full backup** (security - audit 2026-10-17, A2, high): the user form set the signed-in user's own - password without the current one, which then passed the backup's password - check. Adding a user, setting a password, changing an e-mail or a role on - the user form, and deleting a user now ask for your own account password, - as a backup does, under the same limit on wrong guesses - which now - counts wrong passwords only. - **A domain's rate limit is taken only by its own applications** (audit M1): the level-2 domain limit was read from `MAIL FROM`, which reaches the milter before Postfix holds it to the login, so a client that never diff --git a/README.md b/README.md index a51329b..1d6ddae 100644 --- a/README.md +++ b/README.md @@ -112,8 +112,8 @@ outbound mail will not reach the real internet without DNS, PTR, and port 25. > local image and pulls nothing: > > ```sh -> docker build -f build/Dockerfile --build-arg VERSION=2.14.3 \ -> -t ghcr.io/mixeme/selfpost:2.14.3 . +> docker build -f build/Dockerfile --build-arg VERSION=2.14.4 \ +> -t ghcr.io/mixeme/selfpost:2.14.4 . > ``` ```sh @@ -122,7 +122,7 @@ docker run --rm -d --name selfpost-try \ -e SELFPOST_HOSTNAME=mail.local.test \ -e PANEL_COOKIE_SECURE=false \ -v selfpost-try-data:/data \ - ghcr.io/mixeme/selfpost:2.14.3 + ghcr.io/mixeme/selfpost:2.14.4 ``` This trial runs with Docker's default capabilities and no diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 279259f..e9eb1bd 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -23,7 +23,7 @@ services: selfpost: - image: ghcr.io/mixeme/selfpost:2.14.3 + image: ghcr.io/mixeme/selfpost:2.14.4 restart: unless-stopped environment: SELFPOST_HOSTNAME: "${SELFPOST_HOSTNAME:?set the mail/panel hostname, e.g. mail.example.com}" diff --git a/docs/development.md b/docs/development.md index 764a65c..476b600 100644 --- a/docs/development.md +++ b/docs/development.md @@ -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 [§ Release image](#release-image)). -**Planned version cuts** (from pin `2.14.3`; adjust if semver changes mid-track): +**Planned version cuts** (from pin `2.14.4`; adjust if semver changes mid-track): | Stage | ID | Cut | |---|---|---| @@ -155,6 +155,7 @@ than the default «meaningful step» rule in [§ Commits and release build](#com | 42 | overview-head | `2.14.1` | | 43 | dmarc-day, health-sockets | `2.14.2` | | 44 | label-tag | `2.14.3` | +| 45 | security-audit-2026-10-17, stages 0-6 | `2.14.4` | Docs-only prep (checklists in plans, no product code) uses the same per-step commit rule but **no** version cut until the next product stage ships. diff --git a/docs/guide.md b/docs/guide.md index 8bcd673..3d9880d 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -218,14 +218,14 @@ cat ./data/setup-token #### Fixed image tag `deploy/docker-compose.yml` pins an explicit version (`ghcr.io/mixeme/selfpost:X.Y.Z`), -deliberately never `:latest`. The current pin is `2.14.3`. **No 2.x image is +deliberately never `:latest`. The current pin is `2.14.4`. **No 2.x image is 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 instead of pulling: ```sh -docker build -f build/Dockerfile --build-arg VERSION=2.14.3 \ - -t ghcr.io/mixeme/selfpost:2.14.3 . +docker build -f build/Dockerfile --build-arg VERSION=2.14.4 \ + -t ghcr.io/mixeme/selfpost:2.14.4 . ``` `VERSION` must equal the tag: it is the version the backup check compares @@ -1067,7 +1067,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.: ``` -backup: this backup was created by SelfPost 2.14.2 but this image is 2.14.3 — restore into the matching image (selfpost:2.14.2) +backup: this backup was created by SelfPost 2.14.3 but this image is 2.14.4 — restore into the matching image (selfpost:2.14.3) ``` Fix the tag in `docker-compose.yml` (build that tag as under [Fixed image diff --git a/docs/roadmap.md b/docs/roadmap.md index b693c68..cce5652 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -28,7 +28,7 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md). | ID | Topic | Status | Progress | Plan | |---|---|---|---|---| -| security-audit-2026-10-17 | Follow-ups of the 2026-10-17 security audit: A2 (high), seven mediums, the lows | stages 0-6 built 2026-10-17 for `2.14.4`; stage H (the host, S1-S3, S5) the owner's | 27/29 | [security-audit-2026-10-17.md](security-audit-2026-10-17.md) | +| security-audit-2026-10-17 | Follow-ups of the 2026-10-17 security audit: A2 (high), seven mediums, the lows | stages 0-6 shipped `2.14.4` (2026-10-17); stage H (the host, S1-S3, S5) the owner's | 27/29 | [security-audit-2026-10-17.md](security-audit-2026-10-17.md) | | upstream-tls-verify | Inbound: offer "Verified" TLS to the upstream (Postfix `secure`, the upstream's name checked), beside Required, which checks no certificate (audit M7) | proposed | — | — | | server-dmarc-hint | Server › Settings lists a DMARC record for the server's own name beside its DKIM and SPF (audit S3, product side) | proposed | — | — | | inbound-upstream-result | Inbound › Log shows what the upstream answered: sent, deferred, bounced, with its last reply | shipped `2.10.0` | 7/7 | [plans/inbound-upstream-result.md](plans/inbound-upstream-result.md) | @@ -68,7 +68,7 @@ stages 1 and 2 as `2.5.0` the same day, stage 2b as `2.5.1`, stage 2c as `2.6.0` the same day; stage 6 (documents and tests, no cut) closed the audit. Of the three rows from [own-mta-builds](#own-mta-builds), **port25-bare-newline** shipped as `2.6.1` the same day and -**base-trixie** as `2.7.0`, and **panel-dkim-signing** as `2.8.0` (2026-10-16). **dns-record-collapse**, **settings-save-row** and **notification-mail-recheck** shipped together as `2.9.0` the same day, with `/data/opendkim` removed as panel-dkim-signing planned, then `2.9.1` (log panes on a phone) and **inbound-upstream-result** as `2.10.0`. What is agreed after it is marked so in the index; every other +**base-trixie** as `2.7.0`, and **panel-dkim-signing** as `2.8.0` (2026-10-16). **dns-record-collapse**, **settings-save-row** and **notification-mail-recheck** shipped together as `2.9.0` the same day, with `/data/opendkim` removed as panel-dkim-signing planned, then `2.9.1` (log panes on a phone) and **inbound-upstream-result** as `2.10.0`. The follow-ups of the [security audit of 2026-10-17](security-audit-2026-10-17.md) shipped together as `2.14.4`, stages 0-6 in one cut; its stage H is the owner's host. What is agreed after it is marked so in the index; every other remaining item is a candidate. panel-docs shipped in [CHANGELOG.md](../CHANGELOG.md) `[1.8.0]`; dmarc-reports in `[1.7.0]` (its ingest path first ran in `2.0.0` and was reviewed on 2026-10-11, see diff --git a/docs/security-audit-2026-10-17.md b/docs/security-audit-2026-10-17.md index 94ad79f..97b08db 100644 --- a/docs/security-audit-2026-10-17.md +++ b/docs/security-audit-2026-10-17.md @@ -683,7 +683,7 @@ the argument. "letsencrypt.org"`. Done when `dig +short TXT _dmarc.selfpost.mixfed.ru` answers. -### Stage 0 — the high and the cheap guards (~5 h) → patch release +### Stage 0 — the high and the cheap guards (~5 h) → patch release (shipped `2.14.4`, 2026-10-17) - [x] **A2.** `reauth` on user create, on a user update that carries `password` or changes `role`, and on user delete; the user form gets the @@ -717,7 +717,7 @@ the argument. - [x] **X1.** SECURITY.md's table: `latest 2.x` supported, `1.x` not. — **Sonnet**, 0.25 h. *Done 2026-10-17.* -### Stage 1 — the tenant boundary in the mail path (~7 h) → patch release +### Stage 1 — the tenant boundary in the mail path (~7 h) → patch release (shipped `2.14.4`, 2026-10-17) - [x] **M1.** `overLimit` and `recordRejected` take the domain from the login (`ApplicationByLogin`), return early on an empty login; the reader's two @@ -753,7 +753,7 @@ the argument. - [x] **W3.** A mutex around `Resync`'s read + write + reload. — **Sonnet**, 0.5 h. *Done 2026-10-17:* TestResyncRunsOneAtATime. -### Stage 2 — the key group (~2.5 h) → patch release, then the test server's `certs/` +### Stage 2 — the key group (~2.5 h) → patch release, then the test server's `certs/` (shipped `2.14.4`, 2026-10-17) - [x] **I1.** `groupadd --system --gid 10997 backupkey; usermod -aG backupkey panel` in the Dockerfile; the guide's two `chgrp 10998` become `10997` @@ -767,7 +767,7 @@ the argument. is root and `testFullBackupRestore` checks both uids; the test server's key at the rollout. -### Stage 3 — limits and defaults (~7 h) +### Stage 3 — limits and defaults (~7 h) (shipped `2.14.4`, 2026-10-17) - [x] **A1.** (a) Compose and guide set `TRUSTED_PROXY_CIDR` for the Apache-on-host scenario (the Docker gateway, with the command that prints @@ -797,7 +797,7 @@ the argument. private address - a trusted proxy's peer would have let the internet in again through that proxy; TestHealthzAnswersOnlyLocalProbes. -### Stage 4 — resources and imports (~5 h) +### Stage 4 — resources and imports (~5 h) (shipped `2.14.4`, 2026-10-17) - [x] **I3.** `pids_limit: 512` in every compose fragment; the comment corrected; one e2e run. — **Sonnet**, 0.5 h. *Done 2026-10-17:* 1024 for @@ -832,7 +832,7 @@ the argument. openssl's HMAC, the deny list's 554 message, healthy. The live sidecar at the rollout. -### Stage 5 — supply chain and currency (~2.5 h) +### Stage 5 — supply chain and currency (~2.5 h) (shipped `2.14.4`, 2026-10-17) - [x] **I4.** Actions pinned by SHA with the version in a comment; `govulncheck ./...` in `test.yml` and in `release.yml` before the push (a @@ -855,7 +855,7 @@ the argument. test server both ways. 1.5 h. -### Stage 6 — the rest, and what to accept (~3 h) +### Stage 6 — the rest, and what to accept (~3 h) (shipped `2.14.4`, 2026-10-17) - [x] **M7.** guide: upstream `encrypt` checks no certificate. — **Sonnet**, 0.25 h. *Done 2026-10-17:* guide and Help. (Offering `secure` is hours and a `domain`-user-facing choice;