.. This file is part of PEPSI. Copyright (C) 2026 GNUnet e.V. PEPSI is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. =============== pepsi-whitelist =============== *Manage the sender whitelist by hand.* 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 :doc:`pepsi-stage-check-whitelist` and populated automatically by :doc:`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: :manpage:`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. Features ======== * **add** — insert an entry: a ``whitelist_name`` group and a ``whitelist_regex`` (a POSIX extended regular expression, stored verbatim — matched case-insensitively by the check stage against the addr-spec parsed out of the ``From:`` header, never against the raw header text). ``--no-dkim-required`` clears the per-row ``dkim_required`` flag (set by default); ``--signature-required`` sets ``signature_required`` (clear by default); ``--sealer DOMAIN`` additionally requires that the message's *validated* ARC chain was sealed by that domain. Idempotent: re-adding an existing ``(name, match field, regex)`` entry is a no-op and leaves its flags unchanged. * **add-list** / **remove-list** — the same, for a mailing list identified by the ``List-Id`` its postings carry rather than by a ``From:`` pattern, which is what matches every posting whoever wrote it. ``--pattern`` treats the argument as a POSIX ERE instead of one exact identifier. A ``List-Id`` is a header anybody can copy, so pair it with ``--sealer``. * **list** — list entries ordered by name, match field and 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 *and* whose header block carries no delivery-trace field (``Received:``, ``Return-Path:``, ``Delivered-To:``, ``X-Original-To:``) 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``, with ``--imap-user`` / ``--imap-password-file``) and Dovecot's own formats (``--doveadm``, ``--doveadm-path``); only header blocks are read, never bodies. ``--dry-run`` reports what would be added and changes nothing; ``--own-address`` narrows what counts as mail *you* sent; ``--max-entries`` / ``--max-messages`` bound the run; and ``--user`` / ``--all-users`` let root seed other accounts, each into its own namespace. ``--include-delivered`` also reads received mail and is marked unsafe for the obvious reason: a forged ``From:`` would then seed the whitelist. * **import --auto-wildcard** — offer to replace the individual addresses at a domain with one ``*@domain`` entry once at least ``WILDCARD_THRESHOLD`` (10, overridable per run with ``--threshold``) 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``, overridable with ``--hosters`` and defeatable with ``--no-hosters``) 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. Whitelist namespaces ==================== A ``whitelist_name`` is either *global* (one segment, e.g. ``correspondents`` — the operator's) or *owned by a user*: ``/``, e.g. ``alice/correspondents``. Segments are letters, digits, ``_``, ``.`` and ``-`` (a dot because ``alice.smith`` is an ordinary login); the separator is ``/`` because no POSIX-ish system allows it in a login name, whereas ``-``, ``_``, ``.`` and a trailing ``$`` all are. A segment made of nothing but dots is refused, so ``..`` — and therefore any relative path — is 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 :doc:`pepsi-stage-check-whitelist`'s ``WHITELIST_NAME``. 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: :doc:`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 :manpage:`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 standalone ``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 :doc:`pepsi-stage-auto-whitelist` (which escapes an address into an anchored pattern), ``add`` stores the ``whitelist_regex`` exactly as given, so the operator controls the matching directly. Configuration ============= ``[pepsi-whitelist]`` (all optional): ``HOSTERS_FILE`` (default ``${DATADIR}/hosters.txt``), ``WILDCARD_THRESHOLD`` (default 10), ``MAX_ENTRIES`` (the cap on rows one ``import`` may add, default 5000), ``SCAN_HELPER`` (default ``pepsi-helper-mailbox-scan``) and ``MAILBOX`` (the default mailbox to import from), plus the shared locality options (``LOCAL_DOMAINS``, ``TARGETS``, ``RECIPIENT_DELIMITER``) that decide which addresses count as ours. The database connection comes from ``[pepsi-postgres]``. See :manpage:`pepsi-whitelist(1)`. See also ======== :doc:`pepsi-stage-check-whitelist`, :doc:`pepsi-stage-auto-whitelist`, :doc:`pepsi-queue`, :doc:`../architecture`, :manpage:`pepsi-whitelist(1)`, :manpage:`pepsi-helper-mailbox-scan(1)`, :manpage:`pepsi.conf(5)`.