57. pepsi-whitelist

Manage the sender whitelist by hand.

57.1. Role

pepsi-whitelist manages the pepsi.whitelist table — the named sets of sender patterns that mark a message as trusted (non-spam). It connects through the shared [pepsi-postgres] section and reads the optional [pepsi-whitelist] section. The same table is read by pepsi-stage-check-whitelist and populated automatically by pepsi-stage-auto-whitelist; this tool lets an administrator add, list and remove entries directly, and lets each user seed a whitelist of their own from the mail they have already sent. It is not a stage. Reference: pepsi-whitelist(1).

Unlike the other operator tools, which simply become the pepsi service account when started as root, this one keeps the invoking identity and switches only its effective ids around each database call: import --user and --all-users need root in order to scan another user’s mailbox under that user’s credentials.

57.2. Features

  • add — insert an entry: a whitelist_name group and a whitelist_regex (a POSIX extended regular expression, stored verbatim — matched case-insensitively against the From: header by the check stage). --no-dkim-required clears the per-row dkim_required flag (set by default); --signature-required sets signature_required (clear by default). Idempotent: re-adding an existing (name, regex) pair is a no-op and leaves its flags unchanged.

  • list — list entries ordered by name then regex, optionally restricted to a single whitelist_name, as a table or --json.

  • remove — delete the entry identified by its exact whitelist_name and whitelist_regex.

  • import — seed a whitelist from a mailbox: every message whose From:/Sender: is at a local domain is one the user sent, so its To:/Cc:/Bcc: addresses are people they correspond with and are added as entries. Received mail is ignored (whitelisting whoever wrote to you would defeat the purpose), as are recipients at a local domain. Reads mbox, Maildir (including its sub-folders), IMAP (--imap) and Dovecot’s own formats (--doveadm); only header blocks are read, never bodies. --user / --all-users let root seed other accounts, each into its own namespace.

  • import –auto-wildcard — offer to replace the individual addresses at a domain with one *@domain entry once at least WILDCARD_THRESHOLD (10) distinct addresses were seen there. Proposals are confirmed one at a time on a terminal, all accepted with --yes, and all declined when there is no terminal — an unattended run never widens a whitelist to a whole domain by itself. Public e-mail hosters (HOSTERS_FILE, /usr/share/pepsi/hosters.txt) are never proposed, however many correspondents a user has there: “everyone at gmail.com” is not a set of people the user knows, and it would hand every spammer with a free account a bypass of the payment gate. Their addresses are still imported individually.

57.3. Whitelist namespaces

A whitelist_name is either global (one segment, e.g. correspondents — the operator’s) or owned by a user: <login>/<segment>, e.g. alice/correspondents. Segments are letters, digits, _ and -; the separator is / because no POSIX-ish system allows it in a login name, whereas -, _, . and a trailing $ all are. A dot is excluded from segments, which makes .. — and therefore any relative path — unrepresentable.

Ordinary users may read and write only their own namespace, keyed on the real uid’s passwd login (never $USER or an argument). That includes list: the shared whitelist records who the organisation corresponds with, and a program every user may run must not print it. root and the pepsi/pepsi-owner/ pepsi-whitelist accounts may manage everything.

For a per-user whitelist to matter, a check stage must consult it — see the {localpart} / {login} placeholders in pepsi-stage-check-whitelist’s WHITELIST_NAME.

57.4. Privilege model

Reaching the shared table needs a database identity ordinary users lack; reading a mailbox needs theirs. So pepsi-whitelist is installed setuid pepsi-whitelist (mode 4755) and immediately lowers its effective ids to the invoking user, raising them again only for the few milliseconds it talks to PostgreSQL (peer authentication keys off the effective uid).

That account is deliberately not the pepsi service account: pepsi-setup grants its role SELECT/INSERT/DELETE on pepsi.whitelist only, so compromising a binary every local user can execute reaches neither the message queue nor the signing keys.

The mailbox parsing — megabytes of attacker-controlled RFC 5322 — happens in pepsi-helper-mailbox-scan(1), spawned with setresuid setting the real, effective and saved ids to the target user. That process has no privileged id left in its credentials, so it cannot become pepsi-whitelist even in principle; it is the only pepsi-helper-* program with no setuid bit at all. Finally, -c/--config is refused for unprivileged callers, and the environment variables that steer configuration discovery (HOME, XDG_CONFIG_HOME), ${DATADIR} expansion (PATH) and libpq (PG*) are removed before the configuration is read.

Unlike pepsi-stage-auto-whitelist (which escapes an address into a boundary-anchored pattern), add stores the whitelist_regex exactly as given, so the operator controls the matching directly.

57.5. See also

pepsi-stage-check-whitelist, pepsi-stage-auto-whitelist, pepsi-queue, Architecture, pepsi-whitelist(1), pepsi-helper-mailbox-scan(1), pepsi.conf(5).