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:
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.
14.2. Assets¶
Asset |
Where it lives |
The property that matters |
|---|---|---|
Mail in flight |
|
Confidentiality and integrity while queued; it must reach the recipient it was addressed to and nobody else. |
Delivered mail |
The recipient’s |
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 ( |
Stored secure-link messages |
|
Confidentiality against anybody who does not hold the PIN. |
End-to-end private keys |
|
Never disclosed; used only by the two crypto stages and |
Domain signing keys and service secrets |
DKIM keys on the filesystem (group |
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 ( |
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 |
A signature as alice is made only on mail alice submitted. |
Security indicators |
|
Integrity: a reader or a later stage may believe them. |
Configuration and policy |
|
Integrity: only the operator decides policy, and each user only their own part of it. |
Correspondence metadata |
Envelopes in the queue, |
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 |
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 |
|
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 |
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 |
The pipeline (the |
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 |
Mail-only users (SMTP submission, IMAP, no shell) |
Hostile to each other. Their levers are submission (as the identities
|
Trusted relay hosts ( |
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 |
Hostile beyond their scope: a principal can never mint a credential
stronger than itself ( |
List members, owners and moderators |
Hostile beyond their lists. A separate account system
( |
Remote senders |
Hostile. Everything they send — envelope, headers, MIME structure,
ciphertext, keys, |
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 forgedFrom: alice@our-domaincarrying anAutocrypt: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 avalidsignature verdict. A gossiped key leaves the bottom rung only when its owner advertises the same key themselves, which promotes it to theinboundrung (audited askey.peer.promote, never as a rotation) and still cannot producevalid.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 = expiredit 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),.forwardchains carry a loop guard, hop counts are capped, and a message that has passed through this host’s ingressMAX_SELF_HOPStimes 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-paypays only for mail it can prove we sent (thePepsi-OriginHMAC), and only within the per-message budget and — whenMAX_TOTAL_PER_DAYis 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_ENFORCErejects. A spoofed message is therefore delivered with an honestAuthentication-Resultsrather 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
0666because ingress takes the caller’s identity fromSO_PEERCREDand feeds it toUSERNAME_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 mode2550or4750, executable only bypepsior a dedicated group — and2550rather than2755is the whole defence: world-execute would give every account the group that gates a setuid-root helper.Setuid programs sanitise before trusting.
pepsi-whitelistdrops 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 (setresuidon all three ids).Helpers become the user fully. The Maildir,
.forwardand 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-httpddirectly, which cannot check a code, so anown:<address>principal changes them without one. None of them changes which key is trusted for the address; the same operations by e-mail orpepsi-keysdo 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 |
The whole queue: read, change, redirect, delete, or inject messages.
Every |
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. |
|
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
( |
|
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; |
|
The queue’s envelopes and |
The headers or body of a queued message; users’ settings and
whitelists; root, |
|
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, |
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.dyields 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-reencryptis 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
ownpre-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-reencryptruns 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 onepepsi-stage-decryptrecorded.
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 |
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
|
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.encryptedThe message arrived encrypted to a key this deployment held, and was opened here. It says nothing about who sent it.
[verified],state.signature_verified, verdictvalidA 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
nameConstraintsbound 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 givevalid-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-ResultsWhat ingress concluded about SPF, DKIM, DMARC and ARC, as recorded; it is not a filter. Inbound authentication is fail-open.
X-Pepsi-CryptoThe 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_STAGEScan 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_STAGEandBOUNCE_STAGEare taken from the operator’s file; an override of them has no effect (andPROGRAMis additionally restricted to apepsi-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,SIGNandSECURE_LINK_STAGEdecide 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-decrypton a shared path — itsQUARANTINE_STAGEandON_BAD_SIGNATUREroute 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 ( |
Every DNSSEC-based check below depends on the AD bit being honest; without one, DANE finds nothing and nothing downgrades noisily. |
|
A peer with DNSSEC-signed |
|
A peer with an |
|
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 |
|
Mail forging a DMARC-protected domain’s |
Publish your own |
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 |
|
Bounded |
Messages |
|
Bounded |
Recipient checks |
|
Bounded |
Message size and shape |
|
Bounded |
Time |
|
Bounded |
Password guessing |
Three failed |
Bounded |
Authentication work |
SPF’s ten-lookup limit, at most 50 ARC sets, |
Bounded |
14.10.2. The queue and the pipeline¶
Resource |
Limit |
Status |
|---|---|---|
Queue depth and disk |
|
Bounded |
Storage amplification |
A message’s body is stored once in |
Bounded |
Fairness |
|
Bounded |
A hung or crashing worker |
|
Bounded |
Retries |
|
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.forwardchain 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 perSUPPRESS_DAYS, secretary challenges to five per sender a day, and payment requests to one per sender and protected mailbox perBLOCK_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 mostMAX_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 mostTIMEOUTfor a key before it proceeds without one.
14.10.4. The web tier¶
Resource |
Limit |
Status |
|---|---|---|
Connections and slow clients |
|
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 |
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 ( |
Bounded |
14.10.5. Money and mailboxes¶
Auto-pay spends at most
MAX_TOTALper message we sent and, withMAX_TOTAL_PER_DAYset, 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 demandMAX_TOTALfor 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. UnderWALLET_SCOPE = unifiedthe 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 reconciletimer) 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_DOMAINSandDISCOVERY = nolimit 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 startedpepsi-telemetry-clientand given[pepsi]aSYSTEM_IDby 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 |
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 |
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.