Code and config: - C-M7: a DMARC report's explicit <pct>0</pct> stays 0; only an absent pct takes RFC 7489 § 6.3's default 100 (Pct *int). - C-D1: docs/development.md now tables the Debian versions the citations were read at (postfix 3.7.11-0+deb12u1, opendkim and libopendkim11 2.11.0~beta2-8+deb12u1, libmilter1.0.1 8.17.1.9-2+deb12u2, libsasl2-2 and sasl2-bin 2.1.28+dfsg-10, supervisor 4.2.5-1, logrotate 3.21.0-1 - read off the image with dpkg-query, 2026-10-15); the e2e stand's new checkCitedPackageVersions fails when the image it built differs. - C-E2: the e2e zone's SOA serial only grows (CoreDNS v1.14.6's file plugin reloads on a grown serial, not on mtime); Corefile comment says so; TestZoneSerialGrows. - C-K8, C-K9: the e2e compose and stage comments say how --project-directory and .env resolve. - D-arch:154: postfix-config.sh's "Postfix insists" comment says what TLS_README advises and that smtpd reads the key as root. - B-G7: inflight.go's reservationTTL comment: a dropped client does give ABORT (smtpd_proto() -> mail_reset() -> milter_abort()); the TTL is for smtpd dying or the milter connection breaking. - D-guide:301: the nginx fragment's first-certificate command now reaches certbot (--entrypoint certbot, --standalone, port 80 free), and the renewal loop passes --webroot -w /var/www/certbot - a standalone certificate would otherwise renew standalone while nginx holds port 80 (certbot docs, "Renewing certificates": the issuing plugin is used "unless you specify other plugins or options", and options of a successful renewal are saved). The register's proposal missed that. Documents: every D-arch, D-guide and D-sec claim the register's Answers E found false or partly true, rewritten - by three Sonnet agents, one per document, then reviewed: two proposals were obsolete after this stage's fixes and not used (a bounce's From: line - extract_from = "smtp" now; "Origin: null" - Referrer-Policy is same-origin now); a stand observation the agent had dated from a commit is now tied to the e2e test that checks it; the SQLite writers paragraph describes _txlock=immediate; the log tailer's rotation paragraph describes the minute's grace; the guide's nginx steps were put in working order (links and modes before `up`, no reconfigure step). "Deferred" is kept for Postfix's queue in the guide too. guide.md's CRLF from the agent normalised. Stand (2026-10-15, make e2e, exit 0, the version gate and zone serial test included); go vet and go test ./... pass on the server (24 packages). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+31
-17
@@ -41,10 +41,12 @@ there since `1.9.0`, that **the level-2 rate limit and an application's
|
||||
client-address list held only for the first message of a connection** - see
|
||||
[Mail path](#mail-path-security-relevant) - which was reproduced on the test
|
||||
stand and fixed. The security one (Fable) found no Critical or High: a client's
|
||||
HELO name could supply the sender, recipient and reply of a journal row and so
|
||||
make a refusal look like the deny list's (the reply now ends at Postfix's own
|
||||
`; from=<`); the journal's row cap was applied only every six hours, between
|
||||
which refusals were written at whatever rate a stranger sent recipients (the
|
||||
HELO name could supply the sender, recipient and reply of a journal row, as the
|
||||
parser then read the line, and so make a refusal look like the deny list's
|
||||
(Postfix itself logs a HELO name with a space, `<>()\";@` and non-printable
|
||||
characters turned into `?`; the reply now ends at Postfix's own `; from=<`);
|
||||
the journal's row cap was applied only every six hours, between which
|
||||
refusals were written at whatever rate a stranger sent recipients (the
|
||||
tailer now cuts to the cap every 500 rows); and two behaviours are recorded
|
||||
under Accepted risks (the allow list keyed on an address anyone can claim, the
|
||||
list files readable by anyone). A third it named - with the filter down, a
|
||||
@@ -88,8 +90,10 @@ The panel is exposed to the internet — the items below are **not optional**.
|
||||
|
||||
- The panel **generates** the password on creation or reissue and shows it
|
||||
**once**.
|
||||
- In `sasldb2` it is stored in the form SASL requires (not plaintext held by the
|
||||
panel); a lost password can only be reissued.
|
||||
- In `sasldb2` it is stored in plaintext - Cyrus keeps `userPassword` as given
|
||||
(Cyrus SASL 2.1.28, lib/server.c `sasl_setpass()`), and a domain export reads
|
||||
it back from there. The panel never shows it again: a lost password is
|
||||
reissued. `sasldb2` is as secret as the exports (§ Backup and domain export).
|
||||
|
||||
### Input and configuration
|
||||
|
||||
@@ -174,7 +178,9 @@ assigned outbound domains and inbound domains **separately**, each list with
|
||||
- Every non-read request first passes the origin check
|
||||
(`originAllowed`, [internal/web/security.go](../internal/web/security.go)):
|
||||
`Sec-Fetch-Site` must be `same-origin`, or `Origin` must name the panel's own
|
||||
host; the session cookie is `SameSite` as well.
|
||||
host (the panel's `Referrer-Policy` is `same-origin`, so a browser puts the
|
||||
panel's own origin on a form post of the panel); the session cookie is
|
||||
`SameSite` as well.
|
||||
- There are **no CSRF tokens**. The decision, its price and the conditions
|
||||
for revisiting it are in the accepted risks and the ADR below.
|
||||
|
||||
@@ -276,8 +282,9 @@ operator can act on it.
|
||||
it fits one queue id. Refusals are
|
||||
parsed from `mail.log` from the head of the line, with the reply ending at
|
||||
Postfix's own `; from=<` so that the HELO name after it - the client's text,
|
||||
logged as written - cannot stand in for the sender, the recipient or the
|
||||
reply; an address with a quote, a bracket or a space in it is not kept.
|
||||
which Postfix logs with non-printable characters and ` <>()\";@` turned into
|
||||
`?` (Postfix 3.7.11, src/smtpd/smtpd.c `helo_cmd()`) - cannot stand in for
|
||||
the sender, the recipient or the reply; an address with a quote, a bracket or a space in it is not kept.
|
||||
Only a recipient at an inbound domain makes a row. The journal is cut to
|
||||
10 000 rows every 500 rows the tailer writes, and by age every six hours.
|
||||
- **Filter lists** reach another container as files: values are whitelisted on
|
||||
@@ -464,10 +471,13 @@ deferred item from the roadmap.
|
||||
- ~~**A row of the inbound log belongs to a domain by name.**~~ Struck in
|
||||
`2.5.0` (audit 2026-10-12, L12): both logs now hold a domain by id, see
|
||||
§ Roles.
|
||||
- **Level-2 in-flight reservations can outlive a dropped SMTP session until TTL.**
|
||||
The journal milter cannot observe connection-close directly through
|
||||
go-milter, so if a client disconnects without an explicit `Abort`, its
|
||||
in-memory reservation is released by TTL expiry, not immediately. The map is
|
||||
- **Level-2 in-flight reservations can outlive a broken SMTP session until TTL.**
|
||||
Postfix tells the milters ABORT when a client drops the connection
|
||||
mid-transaction (Postfix 3.7.11, src/smtpd/smtpd.c `mail_reset()` ->
|
||||
`milter_abort()`), and the journal milter releases the reservation then.
|
||||
When that never arrives - smtpd killed, the milter connection lost - go-milter
|
||||
has no close callback, and the in-memory reservation is released by TTL
|
||||
expiry, not immediately. The map is
|
||||
bounded by key and TTL (currently 10 minutes), so the effect is temporary
|
||||
over-counting on that app/domain key under churn. Accepted deliberately:
|
||||
immediate cleanup would require protocol hooks the current milter interface
|
||||
@@ -477,7 +487,9 @@ deferred item from the roadmap.
|
||||
directory is `2750 postfix:selfpost` and the file is `0640`: `postlogd` (user
|
||||
`postfix`) writes, the panel reads through the shared `selfpost` group, and
|
||||
the file is inaccessible to others. The log holds envelope addresses and
|
||||
client IPs, but neither message bodies nor headers; it is excluded from
|
||||
client IPs and SASL logins, the `Message-ID` of each message and the replies
|
||||
given - for a refused `From:` line the reply quotes it (printable ASCII,
|
||||
cut) - but no message bodies and no other headers; it is excluded from
|
||||
backups (`log/` is skipped) so that a dump stays state rather than
|
||||
diagnostics. Since 2.2.0 the tailer copies the lines of queued outbound
|
||||
messages into the database for the message's page, and those are in a
|
||||
@@ -513,9 +525,11 @@ deferred item from the roadmap.
|
||||
every submission: availability of sending now depends on the panel,
|
||||
which it did not before `2.4.0`. Chosen by the owner over the alternative,
|
||||
a check that is absent whenever the panel is - that one could be switched
|
||||
off by whoever can make the panel slow. Supervisord restarts the panel at
|
||||
once and takes the container down after three failed starts, so the
|
||||
outage is seconds or the whole instance; Server › Health counts the
|
||||
off by whoever can make the panel slow. Supervisord starts a panel that
|
||||
died again at once; one that dies within a second of starting is tried
|
||||
four times in a few seconds, and then SelfPost's `crashexit` listener takes
|
||||
the container down (supervisor 4.2.5, `startretries=3`), so the outage is
|
||||
seconds or the whole instance; Server › Health counts the
|
||||
downtime and the mail deferred for it. Trigger to revisit: those numbers
|
||||
not staying at nothing - then the check moves into a process of its own
|
||||
([plans/from-header-binding.md](plans/from-header-binding.md)).
|
||||
|
||||
Reference in New Issue
Block a user