Closes what the security review left as an accepted risk: with the
spam filter down - it is attached fail-open - nothing removes verdict
headers a sender wrote himself, and the inbound journal recorded his
verdict as the filter's. The owner asked whether rspamd could add a
header that proves the others are ours (2026-10-12) and agreed to a
signature: a fixed mark could be written by a sender as easily as the
verdict.
- inbound.FilterKey: a key of 32 random bytes, 64 hex characters, in
/data/antispam/filter.key, made on the first start with the relay
on. 0644 like the list files beside it: the filter is another user
in another container.
- deploy/antispam/rspamd/milter_headers.conf: a custom routine reads
the key through the directory the sidecar already mounts and adds
X-SelfPost-Filter, the HMAC-SHA256 of the message's queue id, hex;
a header of that name from the sender is removed.
- internal/milter/inbound.go: the verdict headers are kept as they
are read, and at end-of-message - when the queue id is known - they
count only if the signature fits (FilterSignature, hmac.Equal). No
key, no queue id, no signature or a wrong one: the row is that of
mail nobody judged (accept, selfpost, no detail).
The signature goes on to the upstream with the mail and is of no use
there: it fits one queue id.
Tried in a throwaway rspamd 3.11 container on the test server before
the Go side was written: the routine loads, signs
(2aa1ca6d... for key 0123...cdef and queue id 4A1B2C3D5E, the value
python's hmac gives and TestFilterSignatureIsWhatTheFilterWrites
pins), removes a sender's header of the same name, and adds nothing
without a queue id.
Tests: signed, unsigned, made-up, another message's, another key's,
the sender's followed by the real one, the next message on the
connection, no queue id; the key made once, read after, replaced when
damaged, left alone by the lists. e2e: filter.key is there, 644, 65
bytes.
docs/security.md: the accepted risk is replaced by a narrower one (the
key is readable on the host); Mail path describes the signature.
Checked on the test server, uncommitted tree: go vet ./... ok,
go test ./... ok (22 packages), make e2e ok.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -59,7 +59,12 @@ smtpd_milters = { antispam, inbound-journal }
|
||||
Antispam runs first; inbound-journal runs at end-of-message on the **accept**
|
||||
path and reads rspamd-added headers (`X-Rspamd-Action`, and `X-Spamd-Result`
|
||||
for the score and symbols - rspamd has no `X-Rspamd-Score`; the sidecar adds
|
||||
both through `deploy/antispam/rspamd/milter_headers.conf`).
|
||||
both through `deploy/antispam/rspamd/milter_headers.conf`). As built they are
|
||||
read only next to rspamd's signature of the message, `X-SelfPost-Filter`
|
||||
(owner, 2026-10-12: "А rspamd не может дописывать какой-то заголовок, по
|
||||
которому мы будем знать, что это заголовки нашего спам фильтра?") - an HMAC
|
||||
of the queue id, since a fixed mark could be written by a sender as easily as
|
||||
the verdict itself.
|
||||
|
||||
**Antispam rejects** do not reach end-of-message on the journal milter. v1 also
|
||||
tails `mail.log` for inbound `postfix/smtpd` **`milter-reject`** / **`reject`**
|
||||
@@ -317,4 +322,4 @@ Target version cut: **`2.1.0`** (MINOR), after `2.0.0`. One commit per step;
|
||||
- [x] [guide.md](../guide.md), [architecture.md](../architecture.md) — **Composer** (kept up with each step; [security.md](../security.md) follows the security review)
|
||||
- [x] `go vet`, `go test`, e2e if applicable — **Composer** (on the test server for every step; last on `7192c38`: vet ok, 22 packages ok, e2e ok, which relays, refuses, reads the log page and edits a list through the panel)
|
||||
- [x] Technical review of inbound antispam changes — **Opus** (2026-10-12; findings fixed: client address of a connection's later messages, the lists' writer under a lock, the tailer's queue-id memory, hold rows written twice, the cap by time; and, outside the feature, the level-2 limit and client-address list skipped after a connection's first message)
|
||||
- [x] Security review inbound antispam path — **Fable** (2026-10-12; no Critical or High; HELO-forged journal rows and the six-hourly row cap fixed, three accepted risks recorded in [security.md](../security.md))
|
||||
- [x] Security review inbound antispam path — **Fable** (2026-10-12; no Critical or High; HELO-forged journal rows and the six-hourly row cap fixed; a sender's own verdict while the filter is down closed by the filter's signature, the owner's idea; accepted risks recorded in [security.md](../security.md))
|
||||
|
||||
Reference in New Issue
Block a user