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:
@@ -860,6 +860,17 @@ with the message, and the upstream receives them with the mail. Copy the
|
||||
`antispam/rspamd/` directory next to the fragment; without the file accepted
|
||||
mail is still recorded, with no verdict.
|
||||
|
||||
The same file makes rspamd **sign** each message, in a third header,
|
||||
`X-SelfPost-Filter`. The filter is attached so that mail still flows when it
|
||||
is down, and then a sender could write a verdict of his own into those
|
||||
headers; SelfPost therefore records a verdict only next to a signature it can
|
||||
recompute. The key is `data/antispam/filter.key`: SelfPost creates it on its
|
||||
first start with the inbound relay on, and the sidecar reads it from the
|
||||
directory it already mounts - there is nothing to configure. If Inbound › Log
|
||||
shows *selfpost* for mail the filter did judge, the sidecar is running without
|
||||
this file or started before the key existed: copy `antispam/rspamd/` again and
|
||||
restart the sidecar.
|
||||
|
||||
The same directory holds
|
||||
[multimap.conf](../deploy/antispam/rspamd/multimap.conf), the rules for the
|
||||
**allow and deny lists** kept in the panel. SelfPost does not apply the lists
|
||||
|
||||
Reference in New Issue
Block a user