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:
+10
-3
@@ -24,9 +24,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
||||
a connection's later messages had no client address; two changes of the
|
||||
filter lists at once could leave the filter's files behind the page; a held
|
||||
message was logged twice; after an unclean stop of the panel the refusals of
|
||||
its last seconds were logged twice. Recorded as accepted in
|
||||
[docs/security.md](docs/security.md): the filter's verdict in the log is the
|
||||
sender's own while the filter is down; an allow entry for a sender address
|
||||
its last seconds were logged twice; with the spam filter down, a sender's
|
||||
own verdict headers were recorded as the filter's. Recorded as accepted in
|
||||
[docs/security.md](docs/security.md): an allow entry for a sender address
|
||||
trusts an address anyone can claim; the list files are readable by anyone on
|
||||
the host.
|
||||
- `INBOUND_ANTISPAM_MILTER` and `INBOUND_JOURNAL_MILTER_SOCKET` are checked
|
||||
@@ -91,6 +91,13 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
||||
relay on.
|
||||
- Help: *Inbound log* and *Filter lists*.
|
||||
- New setting `ANTISPAM_DIR` (default `/data/antispam`).
|
||||
- **The spam filter signs what it judged.** rspamd adds `X-SelfPost-Filter` to
|
||||
every message it lets through - an HMAC of the message's queue id under a
|
||||
key SelfPost creates in `data/antispam/filter.key` - and the inbound log
|
||||
shows a verdict only next to a signature that fits. Nothing to configure,
|
||||
but the sidecar needs the new `antispam/rspamd/milter_headers.conf` and a
|
||||
restart after SelfPost's first start; until then accepted mail is logged
|
||||
as *selfpost*, with no verdict.
|
||||
- A full backup carries the inbound log, the filter lists and their files
|
||||
under `data/antispam/`. It does not carry the `antispam/` directory with the
|
||||
compose fragment and the rspamd configuration - copy that from
|
||||
|
||||
Reference in New Issue
Block a user