audit 4: findings of the two reviews
test / test (push) Canceled after 0s

- Two inbound domains on one upstream could never change its TLS mode,
  each save refused for the other (medium): SetTransportAs changes the
  mode for every domain on the upstream when the one saving may change
  them all; a domain user who may not is refused with the mode, not the
  other tenant's domain name (security F1: the first version named it).
- A deletion's leftover DKIM key is removed by Resync (Reload
  configuration, a restore's first start) for names no domain has.
- The backup's write deadline moves before Check.
- hasUnsavedEdit skips password and file fields and compares a multiple
  select's options; htmx.config.timeout 30 s, so a refresh never answered
  ends and is tried again; a failed refresh's promise is handled.
- An empty filtered Outbound log says no message matches the filters.
- The dummy bcrypt hash is made at start; a DMARC part over 8 MiB is
  refused for its size like the other two limits.
- Health's Configuration box says what Reload writes (design first,
  32dfe4a).
- Docs: CHANGELOG, guide § Inbound, security.md (the shared upstream's
  mode, the profile form's "username taken"), architecture.md (owed to an
  address), the plan's notes corrected.
Tests: the shared upstream (service, handler), the leftover key, all eight
Health boxes ordered, the subject from before migration 0011, the new JS
cases.

Tested on the test server: go vet + go test of 25 packages, make e2e (all
pass, 636 s) on this tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-15 12:46:52 +03:00
co-authored by Claude Opus 5.5
parent 32dfe4a2cf
commit eaa7de0754
23 changed files with 359 additions and 83 deletions
+4 -1
View File
@@ -401,7 +401,10 @@ is sent; the operator's view is guide § Notifications.
who are still recipients. A user who comes to hear of a subject only while
it is open is not mailed about it, and its ending goes to those who were
told. A subject opened before `2.5.4` has no such record, and its ending
goes to every recipient, as it did.
goes to every recipient, as it did. It is owed to an address: a user who
changes their e-mail while a subject is open is a new address, told
neither its follow-up nor its end - accepted, a subject lasts hours to
days.
- **The notifier's own mail is not a subject.** A notification waiting for a
mailbox that does not answer, or its bounce coming back to
`postmaster@<hostname>` - which receives no mail - is left out of the
+30 -2
View File
@@ -1200,7 +1200,8 @@ command whose leftover is the owner's alone.
would have been cut at 60 s, so `HandleBackup` moves its write deadline
to two hours through `http.ResponseController` (no wrapper of the
ResponseWriter in the chain; e2e checks the log has no failure to move
it). A test pins the four values.
it). A test checks the four are set and long enough for a form and a
DNS page.
- [x] **Polling recovery**: schedule from `htmx:afterRequest` on the box (or pass
`source`), handle `sendError`/`timeout` with back-off; `RequireAuth`
answers an `HX-Request` with `HX-Redirect: /login` instead of a 303. A
@@ -1261,7 +1262,9 @@ command whose leftover is the owner's alone.
poll, the pages, the rows' links and the message's way back;
`SendLogFilter.Status`, `Since`, `Until` (whole UTC days, as the
Inbound log's). A status not in the list is no narrowing. Plans: a
domain user's filtered page still goes by `idx_send_log_domain_id`; the
domain user's filtered page goes by `idx_send_log_domain_id` (with a day
filter by `idx_send_log_domain_id_created_at` and a sort - not pinned by
the plan test); the
global role's status-only count walks the table (a page view or a 5 s
poll while the log is open on a status - tens of thousands of rows at
the retention limit, milliseconds), accepted.
@@ -1388,6 +1391,31 @@ command whose leftover is the owner's alone.
saved before keep the first by domain name, as Postfix did). Tests in
inbound and postfix (`TestRenderInboundMapsOneTLSLinePerNextHop`).
*Changed by the reviews of stage 4 (2026-10-15; technical and security):*
**two domains on one upstream could never change its TLS mode** - each
save was refused for the other (medium); `inbound.Service.SetTransportAs`
now changes the mode for every domain on the upstream when the one saving
may change them all, and refuses a domain user who may not with the mode,
not the other tenant's name (the security review's F1: the first version's
refusal named it). The deletion's leftover key is removed by Resync (Reload
configuration, a restore's start) for names no domain has. The backup's
write deadline moves before Check. `hasUnsavedEdit` skips password and file
fields (a password manager filling the export password made Recalculate
ask) and compares a multiple select's options. `htmx.config.timeout` 30 s,
so a refresh that is never answered ends and is tried again; the rejected
promise of a failed refresh is handled. A filtered empty Outbound log says
so. The dummy hash is made at start. A DMARC part over 8 MiB is refused for
its size like the others. The Configuration box on Health says what Reload
writes (`design:` first). Tests: the shared upstream (service and handler),
the leftover key, all eight Health boxes ordered, the legacy subject.
Accepted, with the reason: a notification is owed to an address
(architecture.md § Notifications); SERVFAIL from every resolver is Unknown
- it is not an answer about the record; Reload repeats the restore's list
of steps by hand (`HandleReload` and `mailStack.Resync`, both commented); a
failed `SetNotifyAddressees` is logged and the subject stays said, the next
follow-up finds nobody owed; the profile form's "username taken" is
documented in security.md.
### Stage 5 — image and Postfix hardening (M17–M19, M21, L30–L32) → `2.6.0`
- [ ] **Supervisor socket**: a group of its own (`supervisor`), panel only;
+4 -3
View File
@@ -1016,9 +1016,10 @@ reports](#dmarc-reports).) Outbound 465/587 is unchanged.
**Panel** (`/inbound/domains`): the global role adds and deletes domains. On a
domain's page, set the upstream host/port and TLS to that hop (opportunistic /
required / off), and choose recipients. An upstream has one TLS mode: Postfix
keeps the TLS policy per next hop, not per domain, so a second domain sent to
the same host and port with another mode is refused, naming the domain that
already uses it. Recipients are an allow-list, or any address at that
keeps the TLS policy per next hop, not per domain, so the mode saved for one
domain is the mode of every domain forwarded to the same host and port, and
they are changed with it; a *domain* user who does not manage all of them is
refused, and told the mode in use. Recipients are an allow-list, or any address at that
domain. Inbound domains are assigned to a user with the *domain* role
separately from outbound ones (see [Users](#users)); such a user sees and
changes the upstream and recipients of the inbound domains assigned to them,
+9 -2
View File
@@ -610,7 +610,12 @@ deferred item from the roadmap.
panel itself makes no request to the upstream. Trigger to revisit: `domain`
users who are not trusted operators — the same condition as for CSRF tokens
— at which point an allow-list of upstream networks, or restricting the
field to the global role, is the fix.
field to the global role, is the fix. Since `2.5.4` an upstream has one TLS
mode for every inbound domain forwarded to it (Postfix keys the policy by
the next hop): a `domain` user saving one that another tenant's domain
uses with another mode is refused and told that mode - not the domain -
so the form tells whether some domain forwards to a given host and port.
Accepted with the rest of this item.
- **Account's profile (username, e-mail) is saved without the password.**
Saving the profile does not ask for the current password, as changing the
password does. Since 2.3.0 the e-mail is where notifications go, so a stolen
@@ -619,7 +624,9 @@ deferred item from the roadmap.
certificate expires, sender addresses of old queued mail - and the user
stops hearing it. Nothing in a notification gives access. A `domain` user
can likewise point signed mail of this server at a third party's address,
a dozen messages a day per assigned domain at most. It
a dozen messages a day per assigned domain at most. The profile form also
says when a username is taken, so a signed-in user can tell which
usernames exist; signing in does not (`2.5.4`). It
becomes a requirement to ask for the current password when changing
`users.email` as soon as [password-reset](roadmap.md#password-reset) mails a
link to it — from then on whoever can change the address could take the