.. 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 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. ============ Threat model ============ This chapter states **what Pepsi protects, from whom, and how far** — the claims. The next chapter, :doc:`security`, describes the mechanisms that uphold them (the account split, the database grants, the setuid programs, key custody) and is the one to read after a compromise. Every claim here names the mechanism it rests on, and every residual — a thing Pepsi does *not* protect — is stated as such rather than left to be inferred. .. contents:: On this page :local: :depth: 2 How to read this chapter ======================== A security claim has three parts: an **asset** (what is worth protecting), an **adversary** (who wants it, and what they can already do), and a **mechanism** (what stands between the two). The chapter is organised the same way: * `Assets`_ lists what Pepsi holds and which property of each matters. * `Principals and how far each is trusted`_ lists everybody who legitimately interacts with a deployment, because most adversaries are one of them misbehaving. * `Trust boundaries`_ draws where authority changes hands. * `Adversaries`_ takes each class in turn: what it can do, what stops it, and what it gets anyway. * `End-to-end encryption: two custody modes`_ and `What a security indicator means`_ state the cryptographic claims, which differ with who holds the key. * `Per-address settings are a trust boundary`_, `The network adversary`_ and `Availability`_ cover the three areas where the answer depends on how the deployment is configured. * `The guarantees at a glance`_ condenses all of it into one table. Two words are used strictly. **Holds** means the claim is enforced by a mechanism that the adversary in question cannot switch off. **Residual** means the adversary gets it, and the text says so on purpose. .. _tm-assets: Assets ====== .. list-table:: :header-rows: 1 :widths: 24 38 38 * - Asset - Where it lives - The property that matters * - **Mail in flight** - ``pepsi.workqueue`` (``headers``, ``state``) and ``pepsi.workqueue_body`` (the body, shared by a message's recipient rows), in the clear; deleted on delivery. - Confidentiality and integrity while queued; it must reach the recipient it was addressed to and nobody else. * - **Delivered mail** - The recipient's ``Maildir``, the LMTP server, or the next MTA. Not Pepsi's after delivery. - Pepsi's part: deliver to the right mailbox as the right user, never to another's — and, for a user with a client key, never file readable a message that arrived encrypted (``pepsi-stage-reencrypt``). * - **Stored secure-link messages** - ``pepsi.secure_message.ciphertext``, sealed under a PIN-derived key. - Confidentiality against anybody who does not hold the PIN. * - **End-to-end private keys** - ``crypto_identity.private_wrapped``, AEAD-wrapped under the key-encryption key (KEK) in ``secrets.d``. - Never disclosed; used only by the two crypto stages and ``pepsi-keys``. * - **Domain signing keys and service secrets** - DKIM keys on the filesystem (group ``pepsi``); the SRS key, the ``Pepsi-Origin`` key, the secure-link pepper, the list unsubscribe key, smarthost and LMTP credentials, OAuth tokens — each in its own ``secrets.d`` fragment. - Confidentiality; each readable only by the account that uses it. * - **Sending identity and reputation** - What the keys above let one do: sign for the domain (DKIM, ARC), rewrite senders (SRS), vouch for mail as ours (``Pepsi-Origin``). - Nobody outside the pipeline may send mail that carries our domain's authority; the server must not become an open relay or a backscatter source. * - **Per-user signatures** - OpenPGP/S/MIME signatures ``pepsi-stage-encrypt`` makes with a user's server-held key. - A signature as *alice* is made only on mail *alice* submitted. * - **Security indicators** - ``X-Pepsi-Crypto``, ``Authentication-Results``, the ``[decrypted]`` / ``[verified]`` subject tags, and the ``state`` keys other stages gate on (``state.signature_verified``, ``state.spam``, ``state.local_origin``). - Integrity: a reader or a later stage may believe them. * - **Configuration and policy** - ``pepsi.conf`` (0644, no secrets), ``pepsi.config_override``, ``pepsi.settings``, the whitelist. - Integrity: only the operator decides policy, and each user only their own part of it. * - **Correspondence metadata** - Envelopes in the queue, ``mail_log``, ``event_log``, the whitelist, the peer-key cache, ``dns_address``, ``tls_session``. - Confidentiality against outsiders; see `What Pepsi does not protect`_ for what is visible to whom. * - **Privacy toward third parties** - What leaves the host on its own initiative: key-discovery lookups (DNS, WKD, VKS, LDAP), TLS reports, opt-in telemetry. - Disclose the minimum, and nothing the operator did not opt into. * - **Money** - GNU Taler wallets driven by ``pepsi-helper-auto-pay``; the merchant backend credentials of ``[pepsi-payments]``. - A wallet is spent only by its owner or, automatically, within the budget the operator set, and only against a demand for mail we really sent. * - **Credentials of the administrative surface** - ``admin_account`` (Argon2id), ``api_token`` (digests), ``admin_session``; list accounts in ``list_user``. - Not recoverable from the database; not replayable. * - **Availability** - The pipeline as a whole. - Mail keeps flowing under hostile input; a failure defers rather than loses or discloses. See `Availability`_. .. _tm-principals: Principals and how far each is trusted ====================================== .. list-table:: :header-rows: 1 :widths: 24 76 * - Principal - Trust * - **The operator** (root, or whoever may edit ``/etc/pepsi``) - **Fully trusted**, and stated precisely: the operator configures the pipeline, holds every secret, and sees every message that passes through in gateway custody. No claim in this manual holds against an operator who *modifies* the server. The at-rest claims (wrapped keys, secure-link ciphertext, credential digests) hold against a *passive* reader of stored data — a stolen backup, a database-only administrator — never against somebody who controls the running system. * - **A database administrator** without host root - A **passive** adversary for the purposes of this model: can read and change every table, cannot read ``secrets.d`` or the DKIM keys. What that yields is exactly :ref:`sec-db` — plus, being able to *write*, control of the queue and of every ``state`` key downstream stages believe. * - **The pipeline** (the ``pepsi`` account and every stage it runs) - Trusted to process mail, and therefore part of what an attacker aims for. Every stage can read and rewrite every queued message; see `A compromised component`_ for what one compromised stage is worth. * - **Local shell users** - **Hostile** to each other and to Pepsi. A user may submit mail as themselves (through ``sendmail``, authenticated by ``SO_PEERCRED``), manage their own whitelist namespace, read their own mailbox, and nothing else. * - **Mail-only users** (SMTP submission, IMAP, no shell) - **Hostile** to each other. Their levers are submission (as the identities ``USERNAME_MAP`` allows them), the control addresses (``pepsi-stage-edit-settings``, the wallet self-service, the key commands) and whatever the operator lets them override for their own address. * - **Trusted relay hosts** (``MYNETWORKS``, client certificates) - Trusted to submit mail as local origin. A compromised one is a compromised submitter for every address it may send as. * - **Delegated administrators** (API tokens and console accounts with a narrow scope, including ``own:
``) - **Hostile beyond their scope**: a principal can never mint a credential stronger than itself (``scope_escalation``), and an ``own:`` principal reaches exactly one address. See :doc:`admin-api`. * - **List members, owners and moderators** - Hostile beyond their lists. A separate account system (``pepsi.list_user``) that grants nothing on the administrative side, and vice versa. * - **Remote senders** - **Hostile.** Everything they send — envelope, headers, MIME structure, ciphertext, keys, ``Autocrypt`` headers, DSNs, payment demands — is attacker-controlled input. * - **Remote receiving MTAs** - Untrusted for confidentiality unless TLS is authenticated (DANE or an enforced MTA-STS policy); see `The network adversary`_. * - **Correspondents' key sources** (their DNS, WKD host, a VKS server, an LDAP directory) - Trusted exactly as far as the trust ladder ranks them (:doc:`key-management`). A domain can always speak for its own users. * - **External helpers the operator wires in** (milters, the LMTP server, the smarthost, Taler wallets and exchanges) - Trusted with what the operator hands them: a milter sees the message in the clear, an LMTP server files it, a smarthost relays it. .. _tm-boundaries: Trust boundaries ================ Authority changes hands at a small number of places, and every mechanism in :doc:`security` sits on one of them:: remote MTAs, local users browser / API client senders (sendmail) (operator, delegate) | | | =====|====================|=========== B1: network ==|================= v v v +--------------+ SO_PEERCRED +-----------------------+ | pepsi-ingress|<- B2: submission | pepsi-httpd | | (auth, SPF, | identity | B6: scopes, CSRF | | DKIM, DMARC)| +-----------+-----------+ +------+-------+ | setup_task | workqueue row v (a request) =======|===== B3: queue (pepsi.workqueue) +-----------------------+ v | pepsi-setup apply | +---------------------+ B4: setuid/gid | (root; B7: re-checks)| | stages as `pepsi` |------------------+ +-----------------------+ | (dispatcher, ARC, | | | SRS, dkim-sign ...)| v +----+----------------+ +-----------------------------+ | | helpers: become the user | | B5: key custody | (maildir, .forward, wallet) | v +-----------------------------+ +---------------------+ | encrypt / decrypt | crypto_identity.private_wrapped | (setuid pepsi-crypto)| + KEK in secrets.d +---------------------+ **B1 — the network.** Everything arriving is hostile until ingress has authenticated it, and even then only the *verdict* is trusted, never the content. Outbound, a hop is trusted only as far as its TLS is authenticated. **B2 — the submission identity.** Ingress decides who submitted a message: SASL, a client certificate, ``MYNETWORKS``, or ``SO_PEERCRED`` on the local socket. ``USERNAME_MAP`` then decides which ``From:``/envelope identities that submitter may use. This is the only place the question "may this person send as this address?" is asked. **B3 — the queue.** Everything after ingress is one table that every pipeline stage may read and write. The verdicts ingress recorded (``state.auth``, ``state.local_origin``) and those later stages add (``state.spam``, ``state.signature_verified``) are **unauthenticated data** inside this boundary: they are believed because only pipeline code can write them. That is the design's largest single trust assumption, and it is why `A compromised component`_ treats a ``pepsi`` stage as equivalent to the queue. **B4 — the helpers.** Three pieces of work need to *be* a specific local user (write a ``Maildir``, act on a ``~/.forward``, drive a wallet). Each is a small setuid-root helper reachable only through a setgid stage, which drops fully to the target user before touching anything the user owns. **B5 — key custody.** Only ``pepsi-crypto`` holds both the wrapped private keys and the KEK. The crypto stages cross this boundary on every message; nothing else in the pipeline does. **B6 — the administrative surface.** Every request is authenticated (``SO_PEERCRED``, bearer token, or password + session) and authorised against the endpoint's one scope. **B7 — the privileged applier.** The web tier holds no root authority. It *records* what should happen in ``pepsi.setup_task``; ``pepsi-setup apply``, running as root and started separately, re-validates each task against the requester's authority and performs it. Removing the ``pepsi-httpd-admin`` package removes the applier, which makes this boundary absolute. .. _tm-adversaries: Adversaries =========== .. _tm-adv-passive: A passive network observer -------------------------- **Can:** read traffic between Pepsi and other MTAs, and between clients and Pepsi. **Stopped by:** opportunistic ``STARTTLS`` in both directions, TLS on submission (required, except on the local UNIX socket), HTTPS on the portal and the console. End-to-end encrypted messages stay encrypted on the wire whatever the transport. **Gets anyway:** the transport metadata — which hosts talk, when, and how much — and the plaintext of any hop where the peer offers no TLS at all. .. _tm-adv-active: An active network attacker -------------------------- **Can:** intercept, modify and drop traffic; strip ``STARTTLS``; present their own certificate; and, with a lying resolver on the path, forge DNS. **Stopped by:** only what the deployment and the peer have configured. With the defaults, an active attacker can downgrade an outbound hop to cleartext or intercept it with their own certificate unless the receiving domain publishes an MTA-STS ``enforce`` policy (honoured by default) or DANE ``TLSA`` records (which the default ``DANE = warn`` only logs). See `The network adversary`_ for the hardened profile that closes this. **Gets anyway:** a denial of service, always. With DANE ``strict`` an attack is converted into a deferral rather than a disclosure — the best available answer, not a complete one. .. _tm-adv-remote: A remote sender --------------- **Can:** send any bytes to port 25: forged ``From:`` headers, malformed MIME, hostile ciphertext and signatures, ``Autocrypt`` and ``Autocrypt-Gossip`` headers, forged DSNs, forged payment demands, and forged Pepsi security headers. **Stopped by:** * **Parsers treated as the attack surface.** Every stage parses hostile input; the most sensitive parser, the CMS/S/MIME code in ``pepsi-crypto``, is fuzzed (:doc:`testing`). * **Forged indicators are removed.** Every ``X-Pepsi-*`` header is stripped from inbound mail before the decrypt stage writes its own, and ``[decrypted]`` / ``[verified]`` subject tags are removed before ours are added. * **Harvested keys cannot impersonate our users.** Keys learnt from inbound mail land on the bottom rungs of the trust ladder, and for any address we hold an identity for, our own key pre-empts every cached key (``lookup_own_first``). A forged ``From: alice@our-domain`` carrying an ``Autocrypt:`` header therefore cannot redirect encryption for Alice or make a signature verify as hers. * **Gossip is bounded.** Only from mail that arrived encrypted, only about ``To:``/``Cc:`` addresses, never about our own users, never displacing a better key, and never able to produce a ``valid`` signature verdict. A gossiped key leaves the bottom rung only when its owner advertises the *same* key themselves, which promotes it to the ``inbound`` rung (audited as ``key.peer.promote``, never as a rotation) and still cannot produce ``valid``. * **A key rotation is never silent.** Replacing a correspondent's harvested key with a newer one (Autocrypt's newest-wins) writes an audit row in the same statement and mails the local recipients of the message that did it; under ``ACCEPT_ROTATION = expired`` it is refused while the stored key is alive. * **Loops are broken.** Null-sender mail is never bounced, auto-replies follow RFC 3834 suppression (``pepsi-common::autoreply``), ``.forward`` chains carry a loop guard, hop counts are capped, and a message that has passed through this host's ingress ``MAX_SELF_HOPS`` times is refused. * **No backscatter.** An address at a served domain that nothing on the host vouches for is refused at ``RCPT`` (``VERIFY_RECIPIENTS``), and a bounce goes only to an envelope sender the message authenticated (``BOUNCE_UNAUTHENTICATED``), so a forged sender is not mailed on the spammer's behalf. * **Payment demands are checked.** ``pepsi-stage-auto-pay`` pays only for mail it can prove we sent (the ``Pepsi-Origin`` HMAC), and only within the per-message budget and — when ``MAX_TOTAL_PER_DAY`` is set — the paying wallet's daily one. A correspondent can still price each demand up to what those budgets leave; bounding the loss is the daily limit's job, not the proof's. * **Authentication is recorded, not enforced, by default.** Inbound SPF, DKIM and DMARC are fail-open: only a definite DMARC failure under ``DMARC_ENFORCE`` rejects. A spoofed message is therefore *delivered* with an honest ``Authentication-Results`` rather than refused. **Gets anyway:** code execution in a stage if a parser bug exists (see `A compromised component`_); a message in the recipient's mailbox with a forged ``From:`` if the operator does not enforce DMARC; a key they chose used to encrypt mail to *an address we hold no identity for*, at the trust-on-first-use rungs, unless ``MIN_TRUST`` excludes them — including by **replacing** a correspondent's harvested key with a newer one under a forged ``From:``, which is audited and announced to the recipients but not prevented unless ``ACCEPT_ROTATION = expired``. They also learn, from the ``RCPT`` answers, which addresses at a verified domain exist — as from any MTA that refuses unknown users — at ten guesses per connection (``MAX_INVALID_RECIPIENTS``). Where that address is one of **ours** — a served user we hold no identity for, which a forged ``From:`` can reach whenever DMARC is not enforced on our own domain — the planted key is not prevented either, but it is not invisible: ``pepsi-keys peer unclaimed`` lists every served address whose encryption a harvested or gossiped key decides, as a worklist for the operator to confirm and convert into identities (:ref:`km-unclaimed`). .. _tm-adv-local: A local shell user ------------------ **Can:** run every world-executable program, connect to world-accessible sockets, and read world-readable files — including ``pepsi.conf``, which is why it holds no secrets. **Stopped by:** * **Submission as themselves only.** The local submission socket may be mode ``0666`` because ingress takes the caller's identity from ``SO_PEERCRED`` and feeds it to ``USERNAME_MAP``; a user can only send as the identities mapped to their login. * **No privileged program is reachable from an ordinary account**, except ``pepsi-whitelist``, which is deliberately world-executable and enforces its own per-login namespace. The other setuid/setgid programs are mode ``2550`` or ``4750``, executable only by ``pepsi`` or a dedicated group — and ``2550`` rather than ``2755`` is the whole defence: world-execute would give every account the group that gates a setuid-root helper. * **Setuid programs sanitise before trusting.** ``pepsi-whitelist`` drops to the invoking user immediately, clears the environment variables that steer the configuration and libpq, refuses ``-c``, and re-raises privilege only around its database connection. Mailbox parsing happens in a separate process that can never regain the privilege (``setresuid`` on all three ids). * **Helpers become the user fully.** The Maildir, ``.forward`` and wallet helpers set every uid and gid to the target user and refuse root, so a user's own files are touched with that user's authority and no other. **Gets anyway:** the configuration topology (served domains, the pipeline graph, host names) from the world-readable ``pepsi.conf``. .. _tm-adv-mailonly: A mail-only user ---------------- **Can:** submit mail through an authenticated channel, send control messages to the control addresses, and use the self-service web pages their deployment enables. **Stopped by:** ``USERNAME_MAP`` (which addresses they may send as, and on which they may register a key); the control stages keying every action on the authenticated envelope **sender** of a ``state.local_origin`` message; the ``EDITABLE_STAGES`` allowlist, which bounds what their overrides may touch (see `Per-address settings are a trust boundary`_); and, for a user who is also a Unix account, the helpers acting with that account's authority only. **Gets anyway:** control over the handling of *their own* mail within what the operator made editable — including, for an editable stage, the right to route their own copy of a message somewhere other than the operator's default. .. _tm-adv-password: Someone holding a user's mail password -------------------------------------- **Can:** everything the mail-only user can, as that user: submit mail with a bound ``From:``, send control messages, and usually read the mailbox over IMAP too, since the same password opens it. **The key-registration road, and what closes it.** Submitting as a user is what registers a key as theirs (a submitted ``Autocrypt:`` header, or the ``register`` command), so a stolen password is enough to make the attacker's own key the address's public face: correspondents' *future* mail is then encrypted to it, and signatures made with it verify as the user. The "a new key was registered" notice is a partial mitigation only — the same password reads the mailbox it lands in and can delete it. A user who enrols a **second factor** (``otp enroll``, see :doc:`key-management`) closes that road. From then on every change to their keys that a user can make — a key-registration or retirement command by e-mail, generating, registering or revoking in the web console under ``own:
``, an unprivileged ``pepsi-keys`` — needs a current six-digit TOTP code from their authenticator app, and an ordinary message's ``Autocrypt:`` header registers nothing at all. So does replacing or removing the second factor itself. **Stopped by (with a second factor):** the code (RFC 6238, a 30-second step, ±1 step of skew); replay protection (a code is accepted once, for a step later than the last one used); a lock after **ten consecutive wrong codes** that only the operator clears (``pepsi-keys otp reset``, ``DELETE /api/v1/otp/{address}``); and custody — the secret is wrapped under the key store's KEK and the ``otp_key`` table is granted to ``pepsi-crypto`` alone, so neither ``pepsi-httpd`` nor an ordinary stage can read a secret, reset a counter or delete an enrolment. **Gets anyway:** * **Without a second factor**, everything above. Nothing forces one; it is the user's choice, and until they make it the notice is the only mitigation. * **The first enrolment.** Enrolling needs no code (there is none yet), so an attacker who gets there first owns the second factor and the user needs the operator to reset it. The enrolment reply — which carries the secret — lands in the mailbox, and a later thief of the password who finds it undeleted has both factors: the reply says to delete it. * **A denial of service on self-service key management.** Ten wrong codes lock it, which an attacker can always cause; the operator's reset is the remedy, and the lock never affects mail delivery or encryption. * **The web console's publication flags.** Uploading to a key server, toggling WKD publication and choosing the primary MTA key happen in ``pepsi-httpd`` directly, which cannot check a code, so an ``own:
`` principal changes them without one. None of them changes which key is trusted for the address; the same operations by e-mail or ``pepsi-keys`` do need the code. * Reading the mailbox, and sending mail as the user — the password is the password. .. _tm-adv-delegated: A delegated administrator ------------------------- **Can:** whatever the scopes on their token or account allow — for example ``queue:read`` without ``queue:write``, or ``own:
`` for one address. **Stopped by:** each endpoint's single declared scope, enforced from the table the OpenAPI document is generated from; ``scope_escalation`` on minting a credential; and, for ``own:``, a re-check of the address on the endpoint *and* again in the applier for every privileged task. **Gets anyway:** the data their scope reads. ``queue:read`` sees envelopes and ``state`` (never headers or bodies — the console's queries do not select those columns), ``logs:read`` the correspondence graph, ``keys:read`` every public key. .. _tm-adv-db: Somebody holding a copy of the database --------------------------------------- A backup, a replica, an SQL-injection foothold on a read path. :ref:`sec-db` lists it in full. In short: **gets** the queued mail in the clear and all metadata; **does not get** any private key (wrapped under a KEK outside the database), any secure-link body (PIN and pepper both absent), or any usable credential (Argon2id and digests). Somebody who can *write* the database, as the schema owner or the pipeline role, additionally controls the queue and the ``state`` keys — and so everything B3 says is believed there. .. _tm-adv-secret: Somebody holding the configuration or a secret ---------------------------------------------- ``pepsi.conf`` is reconnaissance. Each ``secrets.d`` fragment is exactly one capability: the SRS key forges SRS addresses, the ``Pepsi-Origin`` key forges "we sent this" (and so aims auto-pay), the pepper removes one of two factors from the secure-link key, the KEK opens the wrapped keys *if* the attacker also has the database. **Write** access to ``pepsi.conf`` is root, because it names the programs the pipeline runs. See :ref:`sec-config`. .. _tm-adv-component: A compromised component ----------------------- The realistic serious case: a parser bug gives code execution in one process. Each account's reach is bounded by its operating-system identity and its database grants (:ref:`sec-privilege`). .. list-table:: :header-rows: 1 :widths: 24 38 38 * - Compromised - Gets - Does not get * - A ``pepsi`` stage (most of them, and the dispatcher) - The whole queue: read, change, redirect, delete, or inject messages. Every ``state`` verdict. The settings, whitelist and peer-key cache. The DKIM keys, so it can sign as the domain. And — by rewriting a queued submission's ``From:`` before ``pepsi-stage-encrypt`` sees it — **a per-user signature from any local user who has an identity**. - Any private end-to-end key or the KEK; the secure-link ciphertext column; the configuration overlay; root's task queue; the administrative credentials; another Unix account. * - ``pepsi-stage-encrypt`` / ``-decrypt`` (``pepsi-crypto``) - Everything above, plus every private end-to-end key and the KEK: it can decrypt all inbound mail to server-held keys and sign as any identity, and it can carry the keys away. - Keys whose private half the user keeps in their own client (``custody = client``). * - ``pepsi-ingress`` - Every message as it arrives, before any stage; the SRS key; the TLS key of the listener; every mailing list's address (the recipient check). It decides B2, so it can admit a message as any local submitter. - Any message already queued (its database role may add to the queue and read nothing of it); settings, whitelists, keys; any private end-to-end key; ``mailbox_quota`` writes. * - ``pepsi-httpd`` - The queue's envelopes and ``state``, and the power to requeue, reroute, delete or inject messages; key metadata; the list and archive tables; the administrative credential tables; the portal's ciphertext (but no PIN). It can *request* any privileged change — switching feature telemetry on included, which takes effect only where the operator runs ``pepsi-telemetry-client``; it learns whether a ``SYSTEM_ID`` exists, never its value. - The headers or body of a queued message; users' settings and whitelists; root, ``/etc``, any private key; the configuration overlay unless ``CONFIG_DB`` is configured. A request it records is re-validated by the applier — and is inert if ``pepsi-httpd-admin`` is not installed. * - ``pepsi-keydisc`` - The peer-key cache and the discovery request queue; so it can plant a correspondent key at any rank. - The queue; our own identities (which pre-empt the cache). * - A setuid helper (maildir, ``.forward``, wallet) - **Root**, while it is compromised before it drops privilege. These are kept small and do their parsing only *after* becoming the user. - — .. important:: **Signatures are only as strong as the pipeline role.** A per-user signature says "this deployment accepted this message from *alice*", and that statement is made by ``pepsi-stage-encrypt`` on the strength of a ``From:`` header that every ``pepsi`` stage may rewrite after ingress checked it. The account split protects the **keys** — a compromised stage cannot carry one away, so the damage ends when the stage is fixed and no re-keying is needed — but not the **use** of them. Domain DKIM signatures are in the same position. This is a known residual, recorded in :doc:`security`. .. _tm-crypto: End-to-end encryption: two custody modes ======================================== "End-to-end" needs an end. Pepsi supports two, per identity, and every claim depends on which one a key is in. .. _tm-gateway: Gateway custody (``custody = local``, the MTA key) -------------------------------------------------- The server generates the key, holds the private half wrapped, and signs and decrypts on the user's behalf. The user reads ordinary mail in any client. **The end is the server.** The protection is between *domains* and *at rest*: * **In transit between deployments:** mail is encrypted from the sending gateway (or client) to the receiving one. A network adversary and every relay between sees ciphertext. * **At rest against a passive reader:** a database copy yields wrapped keys but not the KEK; a stolen ``secrets.d`` yields the KEK but not the keys. * **Not against the operator, root, or a compromised ``pepsi-crypto`` stage:** they see plaintext and use the keys. * **Not against a compromised pipeline for signatures** — see the box above. * **Mail in the queue is plaintext** on both sides of the crypto stages; that is what a pipeline is. Encrypt the database at rest if that matters. * **Mail filed in the mailbox is plaintext** — the mail store is trusted — unless the recipient *also* holds a client key and ``pepsi-stage-reencrypt`` is in the pipeline, in which case the filed copy is re-sealed to that key (see below). ``[stage-reencrypt] ON_NO_CLIENT_KEY = bounce`` (site-wide or per recipient) refuses mail rather than file it readable. This is the model of an S/MIME gateway appliance, and it is chosen on purpose: it protects users who will never install anything. .. _tm-client: Client custody (``custody = client``, the MUA key) -------------------------------------------------- The user's own mail client holds the private key; Pepsi holds only the public half (registered through ``pepsi-keys``, the console, the ``register`` control command, or the user's own ``Autocrypt:`` header). **The end is the user's client.** Pepsi publishes the key (it becomes the address's public face in WKD), encrypts mail from other local users to it, and passes mail encrypted to it through **unopened** — even when an MTA key of the same user is among the recipients too. What that buys: * **Against a database thief, a compromised stage, or a compromised crypto stage:** mail encrypted to the client key cannot be read — no server process ever held the private half. * **Not against a malicious operator**, who can still (a) substitute a different key in what Pepsi publishes (WKD, DANE, VKS) and in the ``own`` pre-emption, so that *future* mail is encrypted to a key of their choosing; (b) read mail that arrived in cleartext before Pepsi encrypted it to the user; and (c) suppress or delay mail. A user who needs protection from the operator must have correspondents encrypt to a fingerprint verified out of band. * **Signatures the user makes in their client** are theirs alone; Pepsi adds no signature of its own over mail the client already signed. * **Only for mail actually encrypted to that key** — in transit. A user may hold both kinds: the MTA key is not retired when an MUA key appears, and it keeps opening mail from correspondents who still use it. That mail is in gateway custody while it is queued, with gateway custody's guarantees. * **At rest, mail the gateway opened is re-sealed to the client key** when ``pepsi-stage-reencrypt`` runs before local delivery: the spam, language and whitelist stages read the plaintext, and what is filed is ciphertext only the client opens. That protects the *filed copy* against anybody who later obtains the mail store, the database or the server; it does not undo the plaintext's passage through the queue, which a compromised stage or the operator could have read or diverted at the time. Mail that *arrived* in cleartext is filed as it arrived — sealing it would claim a confidentiality it never had. No signature is added: the stage holds no private key, and the sender-signature verdict is the one ``pepsi-stage-decrypt`` recorded. Choosing between them --------------------- .. list-table:: :header-rows: 1 :widths: 40 30 30 * - Adversary - Gateway custody - Client custody * - Passive network observer - Protected - Protected * - Database or backup thief - Protected (keys wrapped) - Protected (no private key) * - Compromised ordinary stage - Reads queued plaintext; can obtain signatures; cannot take the key - Reads mail only if it arrived in cleartext; cannot sign as the user * - Compromised ``pepsi-crypto`` stage - Keys taken; all mail readable - Protected for mail encrypted to the client key * - Malicious operator - Not protected - Not protected for future mail (key substitution); protected for mail already encrypted to a verified fingerprint * - Thief of the mail store after delivery - Not protected (filed in plaintext) - Protected: mail encrypted to the client key, and — with ``pepsi-stage-reencrypt`` — mail the gateway opened .. _tm-peer-keys: Keys of correspondents ---------------------- Encrypting to a correspondent is only as good as the key's provenance. The trust ladder (:doc:`key-management`) ranks every source, and ``[pepsi-keydiscovery] MIN_TRUST`` sets the floor. The default floor is the bottom rung, so **trust-on-first-use keys are encrypted to** — which defeats a passive eavesdropper, and does not defeat an attacker who controls the correspondent's key source or can inject a harvested key before the real one is seen. That is a deliberate trade: the alternative to a TOFU key is cleartext, not a better key. A deployment that prefers cleartext (or the secure-link portal) to an unauthenticated key raises the floor; see `The network adversary`_. The same trade governs a key **changing**. Within the trust-on-first-use rungs the newest key wins (Autocrypt Level 1's peer-state update), and "newest" is judged from a message whose ``From:`` the sender wrote — so a remote sender who can forge a correspondent's address can make future mail to that correspondent go to a key of their choosing. What Pepsi guarantees is that this is **never silent**: every such replacement is an audit-log row written by the statement that makes it, and the local recipients of the message that caused it are mailed both fingerprints (:ref:`km-rotation-trace`). An operator who prefers refusing genuine rotations to accepting forged ones sets ``ACCEPT_ROTATION = expired``, under which a live stored key is never replaced by a message; the correspondent then cannot read our mail until an operator accepts their new key by hand, or the stored key reaches the expiry it states itself. That date is read from the stored key's own material when it is learnt (an OpenPGP self-signature, an X.509 ``notAfter``), and on a re-sighting of the same OpenPGP key it only ever moves **later** — so replaying an older copy of the certificate, whose earlier self-signature had a shorter, lapsed expiry, cannot make a live key look dead. A gossiped key the owner later advertises themselves is **promoted** to the owner's rung rather than replaced (:ref:`km-gossip-promotion`). That grants a remote sender nothing a forged ``From:`` did not already: a forged ``Autocrypt:`` header carrying the attacker's *own* key displaces a gossiped row outright on rank, so repeating the gossiped key can only confirm what was stored. Promotion changes the rank the row defends itself with, never the verdict a signature checked against it can reach. .. _tm-indicators: What a security indicator means =============================== A reader, a filter rule, and later stages all act on what Pepsi says about a message. Each indicator's meaning is exact: ``[decrypted]``, ``state.encrypted`` The message arrived encrypted to a key this deployment held, and was opened here. It says nothing about who sent it. ``[verified]``, ``state.signature_verified``, verdict ``valid`` A signature that covers the **plaintext** verified, *and* the key is anchored: our own identity, a manually pinned key, a DNSSEC-validated DANE key, a WKD key, or an S/MIME chain to an operator-installed trust anchor — a chain that also stays inside every ``nameConstraints`` bound and certificate-policy requirement set by the CAs on it, the anchor's own included, so anchoring a partner CA vouches for the partner's namespace and nothing more. Nothing weaker — a harvested, gossiped, attached or VKS key, a DANE answer without the AD bit, a CA nobody chose — can produce it; those give ``valid-untrusted``. A signature that covers only ciphertext never produces it either, because anybody who holds a ciphertext can wrap their own signature around it. ``Authentication-Results`` What ingress concluded about SPF, DKIM, DMARC and ARC, as recorded; it is not a filter. Inbound authentication is fail-open. ``X-Pepsi-Crypto`` The decrypt stage's summary. Believable **only** because every inbound ``X-Pepsi-*`` header is stripped first; a client must not believe it on mail that did not pass through a Pepsi decrypt stage. Every one of these is written by pipeline code into the queue (boundary B3). They mean what they say against a remote sender; they mean nothing against a compromised ``pepsi`` stage, which can write any of them. .. _tm-settings: Per-address settings are a trust boundary ========================================= Configuration is layered (:doc:`configuration`): the INI file, then the operator's database overlay (global, ``domain:``, ``address:``), then ``pepsi.settings`` — the one layer an **account owner** can write, by e-mail through ``pepsi-stage-edit-settings``. The layers below it are the operator's; this one is the user's, and it is treated as untrusted input: * **Only stages the operator lists** in ``EDITABLE_STAGES`` can be changed. An edit to any other section is refused and nothing is stored. * **Only the user's own copy is affected.** An override is keyed on the envelope sender of locally submitted mail and on the envelope recipient of everything else, and a message whose recipients' overrides diverge is split first, so one user's settings never act on another user's copy. * **Identity options are barred** (``SIGNING_DOMAIN``): which domain a message is signed as is the operator's decision. * **Programs cannot be replaced.** Which program runs a stage, its default successor ``NEXT_STAGE`` and ``BOUNCE_STAGE`` are taken from the operator's file; an override of them has no effect (and ``PROGRAM`` is additionally restricted to a ``pepsi-stage-*`` command by the edit stage). * **Every edit is validated** by the owning stage's own parser, against the effective configuration it would produce, before it is stored. * **Secrets are never echoed.** The reply quotes the effective options with every credential-bearing value masked, using the same predicate as the API. What an editable stage **does** hand the user is every option that stage reads from its own section — including its **branch targets** (``TRUE_STAGE``, ``QUARANTINE_STAGE``, ``RESPONSE_STAGE``, ``SECURE_LINK_STAGE`` and the like). So a user can route *their own* mail through a different branch than the operator's default. That is the intended power; the consequence is a rule for the operator: .. warning:: **Never list a stage in** ``EDITABLE_STAGES`` **whose effect you mean to be mandatory.** In particular: * ``pepsi-stage-encrypt`` — ``ENCRYPT``, ``SIGN`` and ``SECURE_LINK_STAGE`` decide whether a message leaves in the clear. List it only if users may choose that for their own mail. * ``pepsi-stage-dkim-sign``, ``pepsi-stage-arc``, ``pepsi-stage-srs`` — these protect the *domain's* reputation, not the user's. * ``pepsi-stage-decrypt`` on a shared path — its ``QUARANTINE_STAGE`` and ``ON_BAD_SIGNATURE`` route inbound mail. * The anti-spam and whitelist stages, if the site's policy is that filtering is not optional. * Any delivery or relay stage — their credentials are masked, but their targets are not. ``UNRESTRICTED_UNSAFE_STAGES = yes`` lifts the ``PROGRAM`` and identity restrictions entirely; it turns every editable stage into one the user fully controls. .. _tm-network: The network adversary ===================== **The defaults defeat a passive eavesdropper, not an active one.** Every hop uses TLS when the peer offers it, and every key the ladder finds is used for encryption, but an attacker in the path can strip ``STARTTLS``, substitute a certificate, or (with the resolver) substitute a key, wherever the peer has not published a policy that forbids it. This is the posture of every opportunistic MTA, and it is what keeps mail flowing to the majority of domains that publish nothing. A **hardened profile** makes a deployment resist an active attacker wherever the peer has published something to check against: .. list-table:: :header-rows: 1 :widths: 40 60 * - Setting - What it closes * - A **validating resolver** on a trusted path (``unbound`` on loopback, or ``systemd-resolved`` with ``DNSSEC=yes``) - Every DNSSEC-based check below depends on the AD bit being honest; without one, DANE finds nothing and nothing downgrades noisily. * - ``DANE = strict`` on ``pepsi-stage-relay-to-internet`` (and on each smarthost entry) - A peer with DNSSEC-signed ``TLSA`` records can no longer be intercepted: a mismatch defers instead of delivering. * - ``MTA_STS`` left enabled (the default) - A peer with an ``enforce`` policy can no longer be downgraded. * - ``[pepsi-keydiscovery] MIN_TRUST = owner-confirmed`` (or ``wkd``) - Encryption only to keys whose publication somebody controlling the mailbox or domain proved; below the floor, the message goes cleartext or to the secure-link portal as ``ENCRYPT`` decides. * - ``DMARC_ENFORCE`` on ingress - Mail forging a DMARC-protected domain's ``From:`` — including your own — is refused rather than delivered with a failing result. * - Publish your own ``TLSA``, MTA-STS ``enforce`` and TLSRPT records (``pepsi-setup`` prints them) - The same protection for mail *sent to* you, by senders that check. Even hardened, an active attacker can always cause a **denial of service**; the profile converts interception into deferral. .. _tm-availability: Availability ============ Pepsi's availability goal is narrower than its confidentiality goal, and stated as a rule: **under hostile input, a message is deferred rather than lost, and a failure is closed rather than disclosing.** A stage that cannot decide parks or fails the one message; it does not stop its peers, and it never sends in the clear because a check could not be made. Volumetric flooding of the network or the host is out of scope — that belongs in front of the MTA. Within that scope, each resource an attacker can make Pepsi spend is bounded as follows. **Open** marks a gap: it is recorded in ``docs/ROADMAP.md`` with the fix chosen, and until it is closed the component is only as robust as the limits around it. SMTP ingress ------------ .. list-table:: :header-rows: 1 :widths: 30 44 26 * - Resource - Limit - Status * - Connections - ``MAX_CONNECTIONS`` (1024), capped below the file-descriptor limit; at the cap the most idle connection of the busiest address range is evicted. New connections are rate-limited per /32 (``CONN_RATE_PER_SECOND``, ``CONN_RATE_BURST``), and one address range may hold at most ``MAX_CONNECTIONS_PER_IP`` (16) at once. The local submission socket, exempt from the rate limit and from eviction, has a ceiling of its own (``MAX_LOCAL_CONNECTIONS``, 32), so local users cannot starve remote SMTP. - Bounded * - Messages - ``MAX_MESSAGES_PER_SESSION`` (100) transactions per session, then ``421`` and a reconnect into the rate limit. Local submission is metered per ``SO_PEERCRED`` uid across sessions (``LOCAL_MESSAGES_PER_MINUTE``, 60, burst ``LOCAL_MESSAGE_BURST``, 120), deferring with ``451``. - Bounded * - Recipient checks - ``MAX_INVALID_RECIPIENTS`` (10) unknown recipients per session, then ``421``. Probes of the MDA or backend are cached (an hour for "yes", five minutes for "no") and capped at 16 in flight; past the cap the address is accepted rather than queued behind the backend. - Bounded * - Message size and shape - ``MAX_MESSAGE_SIZE`` (25 MiB) at ``MAIL SIZE=``, during ``DATA`` and ``BDAT``; 1000 recipients; 4 KiB command lines, 1 MiB data lines. - Bounded * - Time - ``COMMAND_TIMEOUT`` (300 s, also bounds the TLS handshake), ``DATA_TIMEOUT`` (180 s), both per read. - Bounded * - Password guessing - Three failed ``AUTH`` attempts close the session with ``421``, so every further batch of guesses costs a new, rate-limited connection; across sessions an address range may fail ``AUTH_FAILURES_PER_HOUR`` (20) times an hour, after which ``AUTH`` is refused without the password being checked. Invalid SRS recipients are capped at three per session. - Bounded * - Authentication work - SPF's ten-lookup limit, at most 50 ARC sets, ``DNS_TIMEOUT`` per query; at most ``MAX_DKIM_SIGNATURES`` (10) signatures verified, those that can align with ``From:`` first; SPF and DKIM concurrently under ``VERIFY_TIMEOUT`` (20 s), then DMARC under another. A check that runs out of time is ``temperror`` for that check alone, which DMARC ignores unless it aligns — so the deadline cannot be used to dodge ``DMARC_ENFORCE``. - Bounded The queue and the pipeline -------------------------- .. list-table:: :header-rows: 1 :widths: 30 44 26 * - Resource - Limit - Status * - Queue depth and disk - ``[pepsi] MAX_QUEUE_ROWS`` (1 000 000 rows), ``MAX_QUEUE_BYTES`` (off by default) and ``MIN_FREE_SPACE`` (1 GiB on ``FREE_SPACE_PATH``): ``pepsi-ingress`` answers ``452 4.3.1`` at ``RCPT``, ``DATA`` and after the end of data while any is reached, and ``pepsi-stage-list-post`` holds a fan-out back instead of emitting its next batch. Measured at most every ``QUEUE_CHECK_INTERVAL`` (10 s), so the ceilings are soft by one interval's admissions; a failed measurement admits. Mail the pipeline itself originates (bounces, DSNs, notices) is never refused, and a queue already over a limit drains normally. - Bounded * - Storage amplification - A message's body is stored once in ``pepsi.workqueue_body`` and shared by every row split off it, so a post to *N* recipients costs one body and *N* small rows, not *N* bodies. A stage that rewrites a body copies it for that one row (copy-on-write). - Bounded * - Fairness - ``pepsi-dispatch`` shares each stage's worker time among *senders* (an authenticated account, else the connecting address, IPv6 by /64), least recently served first, so one sender's burst queues behind itself rather than in front of everybody. ``[pepsi-dispatch] FAIR_FIFO_PERCENT`` (10) of every stage's slots still go to its oldest rows, which bounds how long any message can wait. A sender able to connect from many addresses counts as many senders. - Bounded * - A hung or crashing worker - ``[pepsi-dispatch] MAX_RUNTIME`` (300 s) kills the worker, and a crash ends it; either way the message takes a strike and is retried a minute (then two) later, and the third strike moves it to ``timeout`` or ``failed``, both terminal. A poison message is therefore tried three times, not in a loop; ``pepsi-failure-bouncer`` never re-bounces a row already at the bounce stage. - Bounded * - Retries - ``MAX_LIFETIME`` (120 h) on every stage, for its own retries and for any error it does not mark permanent; ``PAYMENT_DEADLINE`` on the anti-spam gate. - Bounded * - Parsing - MIME depth 16–32 and part counts; decompressed OpenPGP plaintext capped at 64 MiB while streaming; three cleartext signature layers above the ciphertext; X.509 chains of eight, with at most 64 policies or policy mappings per certificate in the policy tree and 2\ :sup:`20` name-against-subtree comparisons per certificate in name-constraint checking; harvested and gossiped key material capped in count and size; an OpenPGP certificate from anybody else with an RSA key above 8192 bits refused (S/MIME stops at 4096); language detection reads at most 20 000 characters. - Bounded Loops and amplification ----------------------- * **Mail loops** stop at ``MAX_HOP_COUNT`` (30) on the relay stages, at the ``.forward`` chain guard, at alias depth 32 and at five list hops. A bounce is never bounced. * **Automatic replies** — vacation notices, secretary challenges and the anti-spam payment request — all obey the RFC 3834 suppression rules in ``pepsi-common::autoreply``, so none answers a list, a bulk mailing, another responder or a delivery report. Vacation notices are further limited to one per correspondent per ``SUPPRESS_DAYS``, secretary challenges to five per sender a day, and payment requests to one per sender and protected mailbox per ``BLOCK_RESPONSE_SUPPRESS`` (one hour by default), decided and recorded in one statement so parallel workers cannot both send. * **Mailing lists** multiply a post by their membership, so a sender's posts to one list beyond ``SENDER_POSTS_PER_HOUR`` (30) in an hour are held for the moderator instead of distributed, and a list holds at most ``MAX_HELD_MESSAGES`` (1000) posts for moderation — past that a post that would be held is discarded, the new one rather than the oldest, so a flood cannot flush out what a moderator has yet to see. List hops are capped as above. * **Mail the web forms send** (list sign-up, password reset) is limited per *target* address to two an hour, and per client, so the forms cannot be used to flood a third party's mailbox. * **Key discovery** answers each address once however many messages ask (requests are de-duplicated), remembers failures (``NEGATIVE_TTL``, ``ERROR_TTL``), caps a response at 256 KiB and one same-host redirect, and puts a deadline on the whole response — a server that dribbles bytes cannot hold a discovery method, which serves every other lookup in turn. A message waits at most ``TIMEOUT`` for a key before it proceeds without one. The web tier ------------ .. list-table:: :header-rows: 1 :widths: 30 44 26 * - Resource - Limit - Status * - Connections and slow clients - ``MAX_CONNECTIONS`` (256), at most ``MAX_CONNECTIONS_PER_IP`` (32) from one client address (an IPv4 address or IPv6 /64; connections through a reverse proxy's UNIX socket are the proxy's to limit); 30 s for the headers, the TLS handshake and the request body each. - Bounded * - Argon2id (PINs and passwords) - One derivation per CPU at a time, off the executor; a request that cannot get a slot within seconds is turned away with ``429`` rather than queued. The secure-link PIN is additionally limited to ``MAX_ATTEMPTS`` per message with a doubling ``LOCKOUT``; sign-ins by account and by source. - Bounded * - Request bodies - 8 KiB for forms, 1 MiB for the administrative API, 8 MiB for the list API. - Bounded * - Stored data - Secure-link messages expire (``EXPIRY_DAYS``). Timers named by ``pepsi.target``, independent of the web server, prune the logs (90 days of ``event_log``, 30 of ``mail_log``; ``pepsi-log-prune.timer``), the TLS-report counters (``RETAIN_DAYS``; ``pepsi-tlsrpt-prune.timer``) and expired MX address cache rows (hourly, ``pepsi-origin-gc.timer``). - Bounded Money and mailboxes ------------------- * **Auto-pay** spends at most ``MAX_TOTAL`` per message we sent and, with ``MAX_TOTAL_PER_DAY`` set, at most that much per **wallet** in any 24 hours, both booked atomically in one statement (serialised per wallet) so concurrent demands — for one message or for many — cannot overspend either, and a payment that fails to settle is credited back to both. Without the daily limit, somebody who received many of our messages can demand ``MAX_TOTAL`` for each, so it should be set wherever auto-pay is. It is deliberately not per correspondent: mailing lists and aliases make the demanding party unknowable, while the wallet is always known. Under ``WALLET_SCOPE = unified`` the one wallet is everybody's, so one correspondent can use up the day for all senders — a denial of automatic payment (the demands then bounce as usual), not a loss beyond the limit. * **Mailbox quotas** refuse only on a *measurement*, never on an estimate, and the three rules that stop a full mailbox from staying shut after its owner empties it (a maximum age for the measurement ingress acts on, a re-measure before refusing, and the ``pepsi-quota reconcile`` timer) are in :doc:`programs/pepsi-quota`. .. _tm-out-of-scope: What Pepsi does not protect =========================== * **Anything against root or a malicious operator.** See `Principals and how far each is trusted`_. * **Traffic analysis.** Who corresponds with whom, when and how much is visible to anyone who can see the network, the queue, or the logs. End-to-end encryption does not encrypt envelopes, and header protection covers only what the container allows. * **Plaintext in the queue.** Every stage must read the message it processes. * **Signatures against a compromised pipeline**, as above. * **Correspondents' own compromise.** A key published by a compromised domain is a compromised key, at whatever rank the ladder gives that domain. * **Recipients' mailboxes after delivery**, beyond one thing: for a user with a client key, mail the gateway decrypted is filed re-sealed to it (:ref:`tm-client`). * **Lookup privacy toward the correspondent's domain.** Discovering a key tells that domain's DNS and WKD host that somebody here is about to write to the address; a VKS server learns the same. ``ALLOW_DOMAINS`` / ``DENY_DOMAINS`` and ``DISCOVERY = no`` limit it (:doc:`key-management`). TLS reports and opt-in telemetry disclose aggregate counts only; telemetry is off until the operator turns it on — at a terminal, or in the console, which can offer it only once the operator has started ``pepsi-telemetry-client`` and given ``[pepsi]`` a ``SYSTEM_ID`` by hand. .. _tm-matrix: The guarantees at a glance ========================== "Yes" means the adversary can compromise that asset; "No" means a mechanism prevents it; "Partial" is explained in the section named in the first column. .. list-table:: :header-rows: 1 :widths: 16 12 12 12 12 12 12 12 * - Adversary - Queued mail - E2E private keys - Signing as a user / the domain - Other users' settings and mail - Money - Credentials - Filed mail (MUA-key users) * - Passive network - No - No - No - No - No - No - No * - Active network (defaults) - Partial (downgrade) - No - No - No - No - No - No * - Remote sender - No - No - No - No - Partial (budget) - No - No * - Local shell user - No - No - No - No - No - No - No * - Mail-only user - No - No - No - No - Own wallet only - No - No * - Stolen mail password - No - No - Partial (no second factor) - No - Own wallet only - No - Partial (no second factor) * - Delegated admin - Partial (scope) - No - No - Partial (scope) - No - No - No * - Database copy - Yes - No - No - Read only - No - No - No * - One secret fragment - No - No (KEK alone) - Partial (per secret) - No - Partial (Origin key) - No - No * - Compromised ``pepsi`` stage - Yes - No - **Yes** - Yes - Partial (budget) - No - Partial (future mail) * - Compromised crypto stage - Yes - Yes (gateway custody) - Yes - Yes - Partial (budget) - No - Partial (future mail) * - Compromised ``pepsi-httpd`` - Partial (envelopes, routing; no content) - No - No - Partial (routing; no settings) - No - Its own tables - No * - Operator / root - Yes - Yes (gateway custody) - Yes - Yes - Yes - Yes - Partial (future mail) "Partial (no second factor)" for a stolen mail password: without a second factor the thief can register their own key as the user's, which makes signatures verify as the user and has future mail — filed mail included — sealed to a key they hold; with one enrolled, every key change needs a code (`Someone holding a user's mail password`_). *Filed mail* is a message the gateway decrypted and then re-sealed to the recipient's own client key before delivery (``pepsi-stage-reencrypt``, see :ref:`tm-client`). "Partial (future mail)": an adversary who controls the pipeline, a crypto stage or the host can read — or stop re-sealing — mail that passes through while they are in control, and can never open a copy already filed, since no server process holds the client key. For a user with no MUA key the column does not apply: their mail is filed in plaintext, or refused under ``ON_NO_CLIENT_KEY = bounce``. The matrix covers what is held **here**. The key that *future* mail to a correspondent is encrypted with is covered by `Keys of correspondents`_: a remote sender able to forge that correspondent's ``From:`` can replace a trust-on-first-use key (Partial — always audited and announced to the recipients; refused while the stored key is alive — not revoked, and not past the expiry it states itself — under ``ACCEPT_ROTATION = expired``), and can never replace a pinned, published or operator-entered one. See also ======== :doc:`security` — the mechanisms behind every "No" above. :doc:`key-management` — custody, the trust ladder, discovery. :doc:`secure-link` — the no-key fallback and its PIN model. :doc:`admin-api` — scopes and the applier. :doc:`configuration` — the layering the settings boundary sits on top of.