14. Threat model

This chapter states what Pepsi protects, from whom, and how far — the claims. The next chapter, Security model, 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.

14.1. 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:

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.

14.2. Assets

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.

14.3. Principals and how far each is trusted

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 An attacker who has the database — 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:<address>)

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 The administrative 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 (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.

14.4. Trust boundaries

Authority changes hands at a small number of places, and every mechanism in Security model 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.

14.5. Adversaries

14.5.1. 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.

14.5.2. 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.

14.5.3. 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 (Test Suite).

  • 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 (Finding the users who bring their own key).

14.5.4. 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.

14.5.5. 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.

14.5.6. 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 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:<address>, 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:<address> 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.

14.5.7. A delegated administrator

Can: whatever the scopes on their token or account allow — for example queue:read without queue:write, or own:<address> 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.

14.5.8. Somebody holding a copy of the database

A backup, a replica, an SQL-injection foothold on a read path. An attacker who has the database 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.

14.5.9. 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 An attacker who has the configuration file.

14.5.10. 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 (The privilege split).

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 Security model.

14.6. 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.

14.6.1. 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.

14.6.2. 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.

14.6.3. Choosing between them

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

14.6.4. Keys of correspondents

Encrypting to a correspondent is only as good as the key’s provenance. The trust ladder (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 (A rotation is never silent). 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 (A gossiped key its owner confirms is promoted). 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.

14.7. 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.

14.8. Per-address settings are a trust boundary

Configuration is layered (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.

14.9. 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:

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.

14.10. 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.

14.10.1. SMTP ingress

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

14.10.2. The queue and the pipeline

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 220 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

14.10.3. 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.

14.10.4. The web tier

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

14.10.5. 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 pepsi-quota.

14.11. 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 (Client custody (custody = client, the MUA key)).

  • 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 (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.

14.12. 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.

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 Client custody (custody = client, the MUA key)). “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.

14.13. See also

Security model — the mechanisms behind every “No” above. Key management — custody, the trust ladder, discovery. The secure-link fallback portal — the no-key fallback and its PIN model. The administrative API — scopes and the applier. Configuration — the layering the settings boundary sits on top of.