70.1.19. pepsi-stage-check-whitelist¶
mark trusted senders’ mail as non-spam
- Manual section:
1
70.1.19.1.1. Name¶
pepsi-stage-check-whitelist - the sender-whitelist stage of the Pepsi pipeline.
70.1.19.1.2. Synopsis¶
pepsi-stage-check-whitelist [GLOBAL-OPTIONS] worker
70.1.19.1.3. Description¶
This stage defaults to FUSION = yes: when stage fusion is enabled ([pepsi]
ALLOW_FUSION, the default) and this stage is folded into the unified pepsi
binary, a predecessor may run it in its own worker process instead of
dispatching it separately. Because this stage needs the message’s header block
(for List-Id:), only a predecessor that loaded the headers or the whole
message can fuse it; a predecessor that loaded envelope metadata alone dispatches
it normally. See pepsi-dispatch(1) and pepsi.conf(5).
pepsi-stage-check-whitelist is a stage program run by pepsi-dispatch(1)
as a persistent worker reading message ids on standard input. It loads that pepsi.ingress row
(refusing to act unless its status is running), reads its
[stage-<stage>] section, and consults a named whitelist of trusted
senders. If the message matches the whitelist, the stage
records state.spam = false so a later pepsi-stage-anti-spam(1) lets it
through without payment. Either way the message then advances to NEXT_STAGE —
this stage never drops, bounces or pauses a message.
The whitelist lives in the pepsi.whitelist table, populated out of band by
the operator (pepsi-whitelist(1)), by the outbound
pepsi-stage-auto-whitelist(1), or by a user importing their own mailbox
(pepsi-whitelist import). Each row pairs a whitelist_name (a group label)
with a whitelist_regex, the match_field naming what that expression is
matched against, and the conditions that gate the match. The stage’s
WHITELIST_NAME option selects the group or groups; the stage SELECTs every
row of them and matches each one. The
whitelist_regex is an extended POSIX regular expression, matched
case-insensitively (it is evaluated by PostgreSQL’s ~* operator, so the
dialect is exactly POSIX ERE). The expression is unanchored; anchor it with
^/$ to match the whole subject.
70.1.19.1.3.1. What is matched¶
match_field selects the subject of the match:
from(the default): the addr-spec parsed out of the message’sFrom:header — not the raw header value, so a whitelisted address written into the display-name position cannot spoof a match. A message with noFrom:header matches nothing.list-id: the RFC 2919 list identifier of the message’sList-Id:header, reduced to the bracketed token, trimmed and lower-cased (List-Id: Rust users <users.rust-lang.org>matches againstusers.rust-lang.org). A message with noList-Id:matches nothing.
A list-id row exists because a mailing list’s postings carry arbitrary
From: addresses. No pattern over From: can say “anything that came through
this list”, so a subscriber would otherwise have to whitelist every individual
poster — or pay the gate for every message the list relays. Matching List-Id:
says it once. Because that header is unauthenticated text anyone can copy, pair
such a row with sealer_domain (below).
70.1.19.1.3.2. Several groups, and per-user whitelists¶
WHITELIST_NAME is a comma-separated list, and an entry may be a template
containing {localpart} or {login}, which is expanded per envelope
recipient:
WHITELIST_NAME = correspondents, {localpart}/correspondents
That consults the operator’s shared list and each recipient’s own. The
<login>/... namespace is the one pepsi-whitelist(1) lets an ordinary user
manage and seed from their mailbox, so this is what makes a per-user whitelist take
effect — without it, such a name is only reachable through a per-address
pepsi.settings override (pepsi-settings(1)).
{localpart} expands to the recipient’s local part with any sub-address stripped
(alice+lists@ → alice). {login} first resolves the recipient to a passwd
login, which is what lines up with the namespace when the address is not itself a
login (first.last@ → alice); it therefore needs the recipient to resolve
locally, so place the stage after pepsi-stage-aliases(1) when using it. A
recipient a template cannot be expanded for is skipped, as is any expansion outside
the whitelist-name grammar (letters, digits, _ and - per /-separated
segment).
All applicable names are checked in one query, and a match in any of them marks
the message non-spam — the verdict is per message, and a queue row is one message.
The locality options LOCAL_DOMAINS, TARGETS and RECIPIENT_DELIMITER are
read only to expand these placeholders.
A row may further restrict when it counts as a match:
dkim_required: the row matches only if the message’sFrom:domain is authenticated — that is, ingress recordedstate.auth.dkim = pass, or pepsi-stage-arc(1) recordedstate.auth.arc = passand DMARC also passed (state.auth.dmarc = pass). An ARC pass on its own is only chain integrity — a sender controlling any single domain can mint a valid one-hop ARC chain over a forgedFrom:— so it is accepted only when DMARC confirms theFrom:alignment.signature_required: the row matches only ifstate.signature_verifiedistrue, which pepsi-stage-decrypt(1) sets for a message carrying a signature it verified against a trusted key.sealer_domain: the row matches only if the message arrived carrying an ARC chain that validated (state.auth.arc = pass) and the named ADMD is one of that chain’s sealers (state.auth.arc_sealers, recorded by pepsi-stage-arc(1)).
sealer_domain is what makes a rule about an intermediary safe to write, and
the reasoning is worth stating because neither half suffices alone. List-Id:
proves nothing: it is text a sender copies out of a genuine posting. A valid ARC
chain proves nothing either — RFC 8617 §8.4 is explicit that it conveys no
trust, only that the ADMDs named in it really did handle these bytes, which is
exactly why anyone can mint one over a forged message. Naming the sealer is the
local policy the protocol deliberately leaves to the receiver: the operator (or
the user) states which intermediary they accept, and the chain then proves that
intermediary handled the message. It applies to from rows too, where it
narrows a sender pattern to mail relayed through a known forwarder.
Note that the domains in state.auth.arc_sealers are recorded whether or not
the chain validated, because they are a record of what was claimed; the stage
discards all of them unless state.auth.arc is pass, so a forged chain can
never satisfy a sealer_domain row.
If any row matches (subject to its conditions), the stage merges
spam: false into the message state and advances. Otherwise it advances the
message unchanged.
70.1.19.1.4. Configuration¶
Options live in the stage’s own [stage-<name>] section
(PROGRAM = pepsi-stage-check-whitelist): the required WHITELIST_NAME list of
groups to consult, a NEXT_STAGE, and — only when a WHITELIST_NAME entry uses
a placeholder — the locality options LOCAL_DOMAINS / TARGETS /
RECIPIENT_DELIMITER. They are documented in pepsi.conf(5); the
whitelist rows themselves are managed with pepsi-whitelist(1).
70.1.19.1.5. State¶
Inputs: state.auth.dkim / state.auth.arc / state.auth.dmarc (for
dkim_required rows), state.signature_verified (for
signature_required rows), which pepsi-stage-decrypt(1) sets — and only
for its valid signature verdict, so a sound signature by a key that could not
be anchored (valid-untrusted) does not open the gate — and
state.auth.arc / state.auth.arc_sealers (for sealer_domain rows),
both written by pepsi-stage-arc(1). The From: header
comes from the from_header column, not from state; List-Id: is read
from the message’s header block, which is why this stage loads it (the body is
never loaded).
Outputs: on a match the stage merges spam: false into state (the key
pepsi-stage-anti-spam(1) reads to skip the payment gate) and advances; on no
match state is left untouched. The state layout is described in
pepsi.state(7).
Transitions: always advances to NEXT_STAGE, whether or not the sender
matched the whitelist (a match additionally merges spam: false). There is no
branch; the stage never pauses, fails, reroutes or finishes — the actual
payment/drop decision is left to a later stage (pepsi-stage-anti-spam(1)).
70.1.19.1.6. Commands¶
- worker
Run as a persistent pepsi-dispatch(1) worker, reading message ids on standard input.
70.1.19.1.7. Global Options¶
- -c FILE, –config FILE
Read the configuration from FILE instead of searching the default locations.
- -L LOGLEVEL, –log LOGLEVEL
Set the logging verbosity (default
info).- -v, –verbose
Show log messages from all sources.
- -h, –help; -V, –version
Print a usage summary / the version and exit.
70.1.19.1.8. Exit Status¶
- 0
The message was processed (advanced, with or without a whitelist match).
- 1
An error occurred (message not found or not
running, misconfigured stage — e.g. a missing WHITELIST_NAME — or a database error, including a malformedwhitelist_regex). The reason is written to the log.
70.1.19.1.9. Examples¶
Check message 42 against the whitelist (it must be running):
pepsi-stage-check-whitelist -c /etc/pepsi/pepsi.conf 42
A pipeline section that whitelists trusted senders before the payment gate:
[stage-check-whitelist]
PROGRAM = pepsi-stage-check-whitelist
NEXT_STAGE = anti-spam
WHITELIST_NAME = trusted-senders
Consulting the shared list and each recipient’s own, so that a whitelist a user
seeded with pepsi-whitelist import is honoured:
[stage-check-whitelist]
PROGRAM = pepsi-stage-check-whitelist
NEXT_STAGE = anti-spam
WHITELIST_NAME = trusted-senders, {localpart}/correspondents
Whitelisting a partner domain, but only when its mail is DKIM/ARC-signed:
INSERT INTO pepsi.whitelist (whitelist_name, whitelist_regex, dkim_required)
VALUES ('trusted-senders', '@partner\.example\.org>?[[:space:]]*$', TRUE);
Letting a mailing list past the payment gate, whoever posted to it, but only when the list’s own ADMD sealed the message’s ARC chain:
pepsi-whitelist add-list alice/lists users.rust-lang.org \
--sealer mail.rust-lang.org
Deliberately trusting the header alone, for a list that does not ARC-seal — this
is what an attacker only has to copy a List-Id: to satisfy, so scope it to
one user’s own whitelist rather than a shared one:
pepsi-whitelist add-list alice/lists announce.example.org --no-dkim-required
70.1.19.1.10. See Also¶
pepsi-config(1), pepsi-whitelist(1), pepsi-stage-anti-spam(1), pepsi-stage-arc(1), pepsi-settings(1), pepsi-dispatch(1), pepsi.conf(5), pepsi.state(7), pepsi-setup(1)
70.1.19.1.11. Bugs¶
Report bugs to the Pepsi issue tracker.