70.2.1. pepsi.conf¶
configuration file for the Pepsi pipeline
- Manual section:
5
70.2.1.1.1. Name¶
pepsi.conf - configuration file shared by all Pepsi components.
70.2.1.1.2. Description¶
Every Pepsi component — pepsi-ingress(1), pepsi-dispatch(1), every
pepsi-stage-* program, pepsi-httpd(1), pepsi-queue(1),
pepsi-tlsrpt(1), pepsi-setup(1) and the rest — reads the same
INI-style configuration file, conventionally pepsi.conf. The format is
shared with the GNU Taler components Pepsi builds upon.
Because there is one file, each setting is documented once, under the section
that owns it. A component reads only the sections relevant to it: the shared
[pepsi] and [pepsi-postgres] sections are read by everything; the
[stage-<name>] pipeline sections are read by the dispatcher and the stage
programs; component-specific sections ([pepsi-ingress], [pepsi-httpd],
[pepsi-dispatch], [pepsi-payments], [pepsi-tlsrpt] …) by the
component named.
70.2.1.1.3. File Format¶
A configuration file is a sequence of [SECTION] headers, each followed by
OPTION = VALUE assignments:
[pepsi-ingress]
HOSTNAME = mail.example.org
ACCEPTED_DOMAINS = example.org example.com
Section and option names are case-insensitive and are conventionally written in
upper case. Blank lines are ignored. A # or % at the start of a line
introduces a comment. A value may be wrapped in double quotes to preserve
leading or trailing whitespace.
70.2.1.1.3.1. Directives¶
- @inline@ FILE
Include another configuration file, resolved relative to the current file.
- @inline-matching@ GLOB
Include every file matching the shell glob GLOB.
- @inline-secret@ SECTION FILE
Merge section SECTION from FILE (typically a mode-restricted file holding credentials) into the configuration. A file that cannot be read is ignored with a warning. This keeps secrets out of the world-readable main file.
A directive is a top-level line: like
@inline@, it ends the section it appears in. Write it after the last option of the section it feeds — an option placed below it belongs to no section, and the whole file then fails to load withExpected section header or directive. pepsi-setup(1) always emits its directives last within a section for this reason:[pepsi-srs] SRS_DOMAIN = srs.example.org MAX_AGE_DAYS = 21 @inline-secret@ pepsi-srs secrets.d/pepsi-ingress.secret
Because an unreadable fragment is only a warning, an option it should have supplied simply looks unset — a permission problem is reported by whatever needs the value, typically as “… is not configured”. Pepsi detects that case and says so explicitly, naming the fragment, its owner and mode; run
pepsi-setup runas root to give each fragment to its reader account.
70.2.1.1.3.2. Value Substitution¶
Options read as paths undergo $-expansion: $VAR and ${VAR} are
replaced with the value of VAR from the [PATHS] section or, failing that,
the environment. The form ${VAR:-default} supplies a fallback. The
[PATHS] section is pre-populated with the usual installation directories
(PREFIX, BINDIR, DATADIR and so on).
70.2.1.1.3.3. Value Types¶
- boolean
YESorNO(case-insensitive).- number
A decimal integer.
- duration
A span such as
5 sor2 h. See the caveat below.- path
A filesystem path subject to
$-expansion as described above.- unix mode
A file permission written in octal, e.g.
660.
Warning
duration values are parsed by jiff::SignedDuration, which accepts
only hours/minutes/seconds and below. A calendar unit such as 5 d or
2 weeks does not parse. For day-or-longer windows write the
equivalent in hours (120 h) or use an integer option where one is
provided (e.g. [pepsi-srs] MAX_AGE_DAYS).
70.2.1.1.4. The Database Overlay¶
This file is the base layer. On top of it, every component reads a set of
administrator-managed overrides from the pepsi.config_override table of the
shared schema, so a deployment can be reconfigured without editing files and, for
the stage sections, without a restart. With no rows in that table a deployment
behaves exactly as this file says; deleting every override returns it to that
state.
The scope chain, lowest precedence first:
this file;
database scope
global;database scope
domain:DOMAIN;database scope
address:ADDRESS;the per-address
pepsi.settingstable (pepsi-settings(1)).
Higher layers override lower ones option by option, never section by
section: an option that no layer mentions keeps the value written here. The
address a domain:/address: scope is matched against is the envelope
sender for a locally-originated message, otherwise each envelope recipient.
The last layer is a different table with a different write authority, and that
is deliberate. pepsi.settings is written by account owners themselves, by
e-mail, within the EDITABLE_STAGES allowlist; config_override defines the
pipeline and is writable only through the pepsi-config PostgreSQL role.
pepsi-setup(1) grants that role INSERT/UPDATE/DELETE on the
table, revokes them from every service account, and verifies both against the
live database after each install: a stage worker parses hostile mail for a
living and must not be able to rewrite the configuration that defines the stage
it runs in. The separation is a security boundary, not duplication.
70.2.1.1.4.1. Sections That Are Never Read From the Database¶
An override naming one of these is ignored at run time and refused at write time:
[pepsi],[pepsi-postgres],[PATHS]Read before a database connection can exist – and
[pepsi]additionally holds the administrator-only cryptographic policy.[pepsi-httpd],[pepsi-httpd-listener-*],[pepsi-httpd-cert-*]The server that serves the configuration interface must always be able to start.
[pepsi-ingress-listener-*]Listening sockets are bound at start-up, often under socket activation and before privileges are dropped.
Consequently adding a submission port, changing a listener’s TLS material, the
database connection or the crypto policy is a text-editor-and-restart
operation, and no administrative interface can do it. [pepsi-ingress]
DB_POOL_SIZE and [pepsi-dispatch] DB_POOL_SIZE are file-only for the same
bootstrap reason: the pool must exist before the overlay can be read.
70.2.1.1.4.2. Hot Reload¶
Every write to the table fires a config_changed notification.
[stage-*] option changes are applied without a restart:
pepsi-dispatch(1) retires its stage workers on the notification – no
message is interrupted – and the replacements read the new values, so the next
message runs with the new configuration.
Every other section is read once at start-up by the component that owns it and
therefore needs that component restarted. So do the two pipeline properties
the dispatcher itself reads once: which stages exist, and each stage’s
PROGRAM/PARALLELISM. pepsi-config set prints which case applies.
A component that misses a notification re-reads the overlay when its database listener reconnects, so a lost notification costs latency, not correctness.
Secrets are never stored in the database – only the @inline-secret@
reference is. Each fragment stays owned by the single reader that needs it.
A broken value in the database cannot stop a program from starting: an overlay that cannot be read or does not produce a usable configuration is logged loudly and this file is used unchanged.
See pepsi-config(1) for set/unset/list, for
dump --origin (which annotates every effective value with the layer it came
from), and for the encrypted export/import of this file and its
secrets.d fragments.
70.2.1.1.5. The [pepsi] Section¶
Cross-cutting identity and policy settings shared across the Pepsi tools. This section is never read from the database overlay.
- KEY_DIR = PATH
Directory below which per-domain signing keys are stored. Each domain gets a sub-directory (mode
0700) containingdkim.rsa.keyanddkim.ed25519.key(mode0600). Optional; defaults to/var/pepsi/keys.- DKIM_SELECTOR = NAME
Base DKIM selector. The RSA key is published as a TXT record at
<NAME>._domainkey.<domain>and the Ed25519 key at<NAME>-ed25519._domainkey.<domain>. Optional; defaults topepsi.- ARC_DOMAIN = DOMAIN
Domain whose identity pepsi-stage-arc(1) uses to ARC-seal inbound mail (RFC 8617). The seal reuses this domain’s DKIM key, so pepsi-setup(1) adds it to the set of domains it generates keys and DKIM DNS records for; ARC needs no separate record. Optional; if unset, authentication results are recorded but not sealed.
- ARC_ALGORITHM =
rsa|ed25519 Algorithm that signs the single ARC seal (ARC permits only one per hop, unlike DKIM which is signed with both). Optional; defaults to
rsafor the widest interoperability.- ORIGINATE_SUCCESS_DSN =
yes|no Whether Pepsi may originate a positive (
Action: delivered) delivery-status notification when a delivery succeeds and the sender requestedNOTIFY=SUCCESS. Optional; defaults tono— Pepsi normally emits only failure bounces. Whenyes, the delivery stages (pepsi-stage-relay-to-internet(1), pepsi-stage-relay-to-smarthost(1)) and pepsi-stage-discard(1) route a successful delivery to theirBOUNCE_STAGEto emit the report. Pepsi never originatesDELAYreports.- MAILBOX_QUOTA = SIZE
Default limit on how much mail a local account may hold, applied by pepsi-stage-relay-to-maildir(1). A plain byte count or a
K/M/G/Tsuffix in powers of 1024 (2G,500M,1048576), ornone. Optional; defaults tonone— no quota. A single account’s limit is set with pepsi-quota(1)set, which overrides this; the kernel’s own limit tightens whichever applies.The quota covers the whole Maildir++ tree — the INBOX plus every folder (
.Sent,.Trash, …) — because that is what the user calls their mailbox, and counting only the INBOX would let anyone park unlimited mail one drag-and-drop away from it.- MAILBOX_QUOTA_COUNT = N
Default limit on the number of messages an account may hold;
0or absent for none. A very large number of very small messages is its own denial of service against a mailbox, which a byte limit alone does not prevent.- MAILBOX_OVER_QUOTA =
defer|bounce What happens to a recipient whose mailbox is full. Optional; defaults to
defer.deferkeeps the message queued and retries it until the delivery stage’sMAX_LIFETIME, then bounces — what Pepsi has always done with a kernelEDQUOT, and it gives the account time to make room.bouncerefuses at once with an RFC 34635.2.2, routing the recipient to the delivery stage’sQUOTA_LIMIT_STAGE, so the sender is not left waiting for days.The same choice picks the reply pepsi-ingress(1) gives when it refuses an over-quota recipient at
RCPT:452 4.2.2fordefer,552 5.2.2forbounce.- MAILBOX_QUOTA_MAX_AGE = DURATION
How long a mailbox measurement may be trusted for a
RCPT-time refusal. Optional; defaults to15 m. Units areh/m/s.pepsi-ingress(1) refuses an over-quota recipient only on a measurement younger than this, and never on the running usage estimate. That is not a performance trade-off but a correctness one: the estimate only ever grows (nothing tells Pepsi when a user deletes mail over IMAP), so refusing on it would shut a mailbox whose owner has since emptied it — permanently, because with every message refused no delivery would run to re-measure. This bound, and pepsi-quota(1)
reconcile, are what keep that from happening.- MAILBOX_QUOTA_RCPT_CHECK =
yes|no Whether pepsi-ingress(1) consults the quota table at
RCPTtime. Optional; defaults toyes. Refusing there costs one indexed lookup and saves a bounce: the sending MTA is still on the line, so it tells its user, and Pepsi generates no backscatter to an envelope sender that may well be forged. Turning it off moves all enforcement to delivery time.- MAILBOX_FS_QUOTA =
yes|no Whether the filesystem holding home directories enforces kernel disk quotas, in which case Pepsi consults
quotactl(2) as well and lets the kernel’s limit tighten the effective one. Optional; defaults tono.Normally set for you. This is a fact about the host’s mounts, not a preference, so pepsi-setup(1) probes for it and records the answer without asking. An explicit value is never overwritten. It is worth having on where it applies: the kernel’s accounting is the only layer of the three that the account owner cannot tamper with (the Maildir++
maildirsizefile lives in their own directory), and it counts everything they own on that filesystem rather than only their mail.- RECIPIENT_DELIMITER = CHARACTER |
none The site-wide sub-address separator (
alice+listsis accountalice). Optional; defaults to+. A[stage-<name>]section may override it for its own delivery decisions, but prefer setting it here: pepsi-ingress(1) uses this value to work out whose mailbox quota a recipient belongs to and has no stage section to read, so a stage that disagrees makes sub-addressed mail escape theRCPT-time refusal. pepsi-setup(1) warns when one does.- MAIL_LOG =
off|summary|full Whether to keep a per-message record. Optional; defaults to
off, and the default is the point.Pepsi deletes a message’s row when the pipeline is finished with it, so there is normally no per-message success log — a property of the design rather than an omission: an ordinary deployment does not accumulate a record of its users’ correspondence, and cannot be compelled to produce one it does not have.
Setting this option changes that.
summarywrites onepepsi.mail_logrow as a message leaves the pipeline, carrying the envelope sender and recipients, the direction (outboundfor locally-submitted mail,inboundotherwise), the stage it ended at, the outcome (completed/failed) and what the pipeline decided about it (the authentication verdict, the spam/paid decision, any next-hop failure detail).fulladditionally records theSubject:line. Message content is never recorded at any setting.Warning
Turning this on means the deployment keeps a record of who corresponds with whom. Some deployments genuinely need that evidence; make sure yours is one of them, and that keeping it is lawful where you operate.
Rows are readable through
GET /api/v1/mail-logby a principal holdinglogs:read, are append-only for every component that processes mail, and are pruned after[pepsi-admin] MAIL_LOG_RETENTION_DAYSdays. The write rides on the same statement as the terminal that removes or fails the message, so enabling the log costs no extra database round-trip. See the manual’s “The administrative API” chapter.- ALLOW_FUSION =
yes|no Whether stage fusion is permitted. When a stage advances to a successor that is marked
FUSION = yes(see the per-stage option below), whosePROGRAMis folded into the same unifiedpepsibinary, and that needs no message column the predecessor did not already load, the predecessor runs the successor’s body in its own worker process — skipping both the advancingUPDATEand the successor’s loadingSELECT, and reporting a single completion to the dispatcher for the whole fused chain. Optional; defaults toyes. Set it tonoto force every stage transition back through the database and the dispatcher (the per-stage cost benchmark does this so each stage is measured as its own dispatched worker). Fusion is in any case inert in a per-program (multibin) build, where a successor is always a separate process. See pepsi-dispatch(1) for the full model.- MTA_STS_MODE =
enforce|testing|none Mode of the MTA-STS policy published for our domains.
enforceandtestingcause pepsi-setup(1) to emit a_mta-sts.<domain>TXT record (and pepsi-httpd(1) to serve the corresponding policy file);nonedisables MTA-STS publishing. Optional; defaults toenforce. The policy’smxhost is the[pepsi-ingress] HOSTNAME.- MTA_STS_MAX_AGE = SECONDS
The
max_agepublished in the MTA-STS policy (how long senders may cache it). Optional; defaults to604800(one week).- TEMPLATE_DIR = PATH
Directory holding message templates (
<name>.<lang>.body) expanded at runtime — for example the pepsi-stage-anti-spam(1) payment-request reply and the pepsi-stage-bounce(1)BOUNCE_MESSAGEbody. Optional; defaults to${DATADIR}/templates, wheremake installplaces the bundled templates.${DATADIR}is resolved from the install prefix of the running binary, so the default is/usr/share/pepsi/templatesfor a--prefix=/usrinstallation and follows the prefix for any other; set this option only if the templates live somewhere the prefix does not imply.- LOG_JSON =
yes|no Emit logs as structured JSON lines (one JSON object per record) on standard error instead of the default human-readable text format, so a log aggregator such as Loki can ingest them without extra parsing. Applies to every Pepsi binary. Optional; defaults to
no(text). The-L/--loglog-level option and-v/--verboseapply in both formats.- LOG =
error|warn|info|debug|trace Default maximum log level for every Pepsi binary. Because all components — including the stage workers the dispatcher spawns (which receive no command-line log flag of their own) — load this same configuration, this is the single knob that lowers or raises logging across the whole pipeline at once; it is, for instance, what the benchmark scripts set to
warnso per-message logging does not skew their measurements. Optional; defaults toinfo. The-L/--logflag, when given, overrides it for that one process.- SHARE_TELEMETRY =
yes|no Whether this deployment shares anonymous feature-usage telemetry with the Pepsi project. Opt-in: optional, and defaults to
no, so an installation that never sets it shares nothing. When set toyes, pepsi-setup(1) generates a randomSYSTEM_ID(below) and the pepsi-telemetry-client(1) daemon aggregates feature-usage events from the stage workers and pepsi-ingress(1) and submits per-feature counts toTELEMETRY_SERVERunder that anonymous id (no addresses, message data, host names or IP addresses). Left atno, the daemon exits immediately, noSYSTEM_IDis generated, and the in-process telemetry calls are no-ops that open no socket. An unparseable value is read asno.- SYSTEM_ID = 64-HEX
Anonymous 256-bit identifier (64 hexadecimal characters) submitted with every telemetry report. Generated and written here by pepsi-setup(1) only once
SHARE_TELEMETRYhas been turned on — a deployment that has not opted in never has one minted for it; required by pepsi-telemetry-client(1). Do not reuse it across deployments.- TELEMETRY_SERVER = HOST | URL
The central telemetry collector. Optional; defaults to
telemetry.pepsi.taler.net. pepsi-telemetry-client(1) submits tohttps://<server>/telemetry/{usage,features}; a value containing an explicitscheme://is honoured verbatim (e.g.http://localhost:18080for local testing).
The following CRYPTO_* options govern end-to-end message cryptography
(OpenPGP and S/MIME) — not the DKIM/ARC keys above. They are deliberately
system-administrator settings and live in [pepsi] rather than in a
[stage-*] section, which puts them structurally out of reach of the
per-address override layer (pepsi.settings overrides only stage sections)
and therefore of pepsi-stage-edit-settings(1): algorithm selection,
downgrade permission, weak-digest acceptance and minimum key sizes are security
decisions with a secure default, not per-user preferences. Every one is
optional; a deployment that does no end-to-end cryptography can ignore them all.
The key store those keys live in is configured in [pepsi-crypto] (below) and
managed with pepsi-keys(1).
- CRYPTO_ALLOW_DOWNGRADE =
no|negotiate|yes Whether Pepsi may emit a downgraded container (S/MIME AES-256-CBC
EnvelopedData, OpenPGP SEIPDv1-with-MDC) and accept 3DES on decryption. Optional; the value compiled in isnegotiate, which downgrades only when the recipient’s own key proves it can do no better — which matters, because much of the installed base cannot read an RFC 9580 AEAD message at all. Reading SEIPDv1 and AES-CBC is always permitted and is not gated by this option.An installed Pepsi ships
yes, from${DATADIR}/config.d/thunderbird.conf, so the effective default on a packaged system is S/MIME AES-256-CBCEnvelopedData. Thunderbird 140.12.0esr cannot read our AES-256-GCMAuthEnvelopedDataand fails silently — no error, an empty plaintext — so a deployment left on the compiled-in default would send blank mail to the largest S/MIME client population there is. Deleting that file restores AEAD in one step, andCRYPTO_ALLOW_DOWNGRADE = negotiateinpepsi.confdoes the same without deleting it (pepsi.confis parsed afterconfig.d). The measurement ispepsi-crypto/tests/thunderbird.rs; the client matrix is in the manual’s interoperability chapter.yesdoes not coarsen OpenPGP: the encrypt stage requests the containerauto, which bothnegotiateandyesresolve per recipient — AEAD for a certificate that advertises SEIPDv2, SEIPDv1+MDC only for one that does not. The difference between the two values is the S/MIME container and the acceptance of an inbound 3DES S/MIME message, which is never emitted under any setting.pepsi-setupdoes not write this option. The wizard deliberately leaves it out of the generatedpepsi.conf— anything it wrote there would override the packaged file — so a deliberate override is made by editingpepsi.confby hand or withpepsi-setup --wizard --expert=CRYPTO_ALLOW_DOWNGRADE.- CRYPTO_ALLOW_WEAK_DIGESTS =
yes|no Accept SHA-1 and MD5 signatures on verification. Optional; defaults to
no. A signature under a weak digest is reported as such, never silently counted as valid.- CRYPTO_MIN_RSA_BITS = BITS
Smallest RSA modulus accepted on inbound verification and outbound encryption. Optional; defaults to
2048. Values below1024are refused outright.- CRYPTO_GENERATE_RSA_BITS = BITS
Modulus size used when this deployment generates an RSA key. Optional; defaults to
2048. It may not be belowCRYPTO_MIN_RSA_BITS— a deployment must not generate keys it then refuses to accept — and OpenPGP key generation accepts only2048,3072or4096.- CRYPTO_INLINE_PGP =
accept|reject How to treat inline (non-MIME) PGP. Optional; defaults to
accept, because real senders still emit it. Pepsi never generates it.- CRYPTO_OPENPGP_ALGORITHM =
ed25519|rsa Algorithm used when generating a new OpenPGP identity. Optional; defaults to
ed25519— generation is instantaneous, which matters because an identity may have to be created while a message waits.rsausesCRYPTO_GENERATE_RSA_BITS.- CRYPTO_SMIME_ALGORITHM =
rsa|p256|p384 Algorithm used when generating a new S/MIME identity. Optional; defaults to
rsa(atCRYPTO_GENERATE_RSA_BITS);p256andp384are ECDSA/ECDH over the corresponding NIST curve.- CRYPTO_SMIME_SHARED_KEY =
yes|no Issue one S/MIME certificate carrying both
digitalSignatureandkeyEnciphermentinstead of a separate signing and encryption certificate. Optional; defaults tono.The default splits them because a separate signing certificate can be destroyed at expiry or revocation — nothing legitimate ever re-signs old mail, so a retired signing key is pure forgery liability — while the decryption half is retained so archived and in-flight ciphertext stays readable. Turning this on halves the certificates an operator buys per identity and gives that up: a shared key follows the decryption rule, so revoking a compromised signing key also ends the ability to receive new encrypted mail at that certificate.
The option decides only what newly created identities look like. Both shapes are supported permanently and may coexist for one address, so every lookup resolves by capability rather than by shape. Combined with an elliptic-curve
CRYPTO_SMIME_ALGORITHMit is refused by pepsi-setup(1): one EC key doing both ECDSA and ECDH is cross-algorithm key reuse that several S/MIME clients reject.
70.2.1.1.6. The [pepsi-postgres] Section¶
Shared database connection settings. Every Pepsi component connects to the
same database and the shared pepsi schema, so these settings live in one
section rather than being repeated per component.
Note
Connection budget. Each Pepsi process opens its own connection pool. The
stage workers and the operator CLIs do their database work strictly serially,
so they use a single connection each — and because the dispatcher runs up
to one worker process per stage slot (the sum of the stages’ PARALLELISM),
this single-connection-per-worker rule is what keeps the bulk of the
connection count predictable and minimal. The concurrent components keep a
small bounded pool sized by their own DB_POOL_SIZE option: the
pepsi-dispatch(1) coordinator (default 8, for the many workers’
concurrent result-processing) and the pepsi-ingress(1) and
pepsi-httpd(1) servers (default 1 each). When tuning, ensure
Σ PARALLELISM (one connection per live worker) + the dispatcher pool
+ the two servers’ pools stays below the server’s max_connections.
A stage’s QUEUE_LIMIT does not enter this budget: pipelining more
messages onto a worker raises its in-flight count, not the number of worker
processes (or connections), so it is the safe knob for throughput when
max_connections is the binding constraint.
- CONFIG
(string, required) PostgreSQL connection string or URI, e.g.
postgres:///pepsi. The server sets the schema search path topepsiand usesREAD COMMITTEDtransactions (at most one writer ever touches a given queue row, so serializable isolation is unnecessary).- SQL_DIR
(path, optional) Directory containing the SQL migration files. Only pepsi-setup(1) reads this, when installing the schema; the serving components ignore it. Defaults to
${DATADIR}/sql(i.e.<install-prefix>/share/pepsi/sql, wheremake installplaces the files). Override it when running from a source checkout.- WORKER_SYNCHRONOUS_COMMIT
(boolean, optional) Whether a stage worker’s database connection keeps PostgreSQL’s
synchronous_commiton. Defaults tono.With it off, a worker’s
COMMITreturns before the write-ahead log record has reached stable storage, which lets the pipeline’s writes group-commit instead of each paying its own flush. Nothing else changes: isolation, atomicity and visibility are unaffected, and a crash can never expose a partial transaction. What can be lost is the last few hundred milliseconds of durability — and only for stage transitions, never for message acceptance, because pepsi-ingress(1) always commits synchronously. A250still means the message is on disk.Losing a stage advance to a machine crash means the message is found still at the previous stage and runs through it a second time — exactly what already happens when a worker is killed after doing its work but before committing. Set this to
yesif a stage in your pipeline has side effects that must not be repeated even across a power loss, and accept the throughput cost.
70.2.1.1.7. The [pepsi-srs] Section¶
The Sender Rewriting Scheme parameters, shared by pepsi-stage-srs(1) (the
forward rewrite) and pepsi-ingress(1) (the reverse decode at RCPT
time), so the secret and domain match in both directions. Omitting the section
disables SRS: the stage refuses to run and ingress treats SRS-looking recipients
as ordinary addresses.
- SRS_DOMAIN
(string, required to enable SRS) Domain that rewritten envelope senders live under. It must be a domain you control: its MX must point at the Pepsi ingress (so bounces return) and its SPF must authorise Pepsi’s sending IPs. pepsi-setup(1) provisions keys and DNS for it.
- SECRET_FILE / SECRET
(required when SRS_DOMAIN is set) The secret keying the HMAC that signs SRS addresses, given as a file path (preferred; store it mode 0600) or inline. It must stay stable over time — a bounce returned days later must still verify — and be identical across all instances.
- MAX_AGE_DAYS
(integer, optional) How many days a rewritten address stays valid for returning bounces (SRS timestamps are day-granular). Default 21.
70.2.1.1.8. The [pepsi-origin] Section¶
Proof-of-origin keying, shared by both relay stages
(pepsi-stage-relay-to-internet(1) and pepsi-stage-relay-to-smarthost(1),
which stamp the Pepsi-Origin header on outbound mail and record its nonce) and
pepsi-stage-anti-spam(1) (which verifies the header on a returning bounce).
The feature is on by default: when no secret is configured, pepsi-setup(1)
generates a random one on first run (provided [pepsi-ingress] HOSTNAME is set).
The secret is not stored inline in this world-readable file; it is written to a
secrets.d/pepsi-origin.secret fragment beside the configuration (owned by the
pepsi dispatcher account, mode 0640) and pulled into this section with an
@inline-secret@ directive. Re-running setup reuses the existing fragment rather
than rotating the key. Removing the section disables the feature — outbound mail is
unstamped and every bounce is then unverifiable (routed per the stage’s
BOUNCE_TARGET_STAGE, else forwarded).
The header carries the version, our HOSTNAME (the [pepsi-ingress]
option), the originating sender account, a 128-bit nonce and a timestamp, plus an
HMAC-SHA256 over all of them. Each stamped nonce is recorded in
pepsi.origin_nonce for two weeks; a bounce is trusted only if its embedded
header’s MAC verifies and its nonce is still tracked. See
pepsi-stage-anti-spam(1).
- SECRET_FILE / SECRET
The secret keying the HMAC, given as a file path (preferred; store it mode 0600) or inline. Like the SRS secret it must stay stable over time — a bounce returned days later must still verify — and be identical across all instances. When neither is set, pepsi-setup(1) provisions a random
SECRETin asecrets.d/pepsi-origin.secretfragment referenced by@inline-secret@(see above), keeping it out of the world-readable main configuration. The feature requires[pepsi-ingress] HOSTNAME(bound into the MAC).
70.2.1.1.9. The [pepsi-crypto] Section¶
Custody and lifecycle of the end-to-end key store — the
pepsi.crypto_identity, pepsi.peer_key and pepsi.ca_trust tables
managed by pepsi-keys(1). Like the [pepsi] CRYPTO_* options above
this is administrator-only, but it is about storage rather than about
cryptographic strength. Omit the whole section if the deployment does no
end-to-end cryptography; as soon as it says anything, pepsi-setup(1)
insists on a usable KEY_WRAP_SECRET, because a key store that cannot store
keys is a configuration mistake rather than a choice.
Private key material is kept in the database, sealed with AES-256-GCM under a
key-encryption key derived from KEY_WRAP_SECRET. The database alone therefore
never yields a private key: a dump, a replica or a backup tape produces
ciphertext, and the key that opens it is a separate artefact guarded by file
permissions. The other half of the boundary is a database grant —
crypto_identity.private_wrapped is granted to the pepsi-crypto role
alone; see pepsi-setup(1).
- KEY_WRAP_SECRET
(string, required once the section exists) The key-encryption key that wraps private key material at rest. Keep it out of this world-readable file: put it in a
secrets.d/pepsi-crypto.secretfragment and reference it with an@inline-secret@directive, exactly as the SRS and proof-of-origin secrets are handled. Every pepsi-setup(1)runre-asserts mode0640on that fragment and hands it to thepepsi-cryptoaccount. Fewer than 16 characters is refused.Back it up separately from the database. If it is lost, every stored private key is gone: mailboxes hold plaintext, so nobody’s mail becomes unreadable, but in-flight and archived ciphertext is lost, every published identity is invalidated and the organisation must re-key.
- KEY_WRAP_KEY_ID
(string, optional) Identifier of the key-encryption key new material is wrapped under. Default
k1. Must be a non-empty run of ASCII letters, digits and underscores, because it is spliced into theKEY_WRAP_SECRET_<ID>option name a retired key is read from. Each row records the id that sealed it, which is what makes rotation incremental rather than a flag day.- KEY_WRAP_SECRET_<ID>
(string, optional) A retired key-encryption key, kept until every row wrapped under it has been rotated.
<ID>is thewrap_key_idit names, upper-cased (soKEY_WRAP_SECRET_K0for keyk0). Between introducing a new key and runningpepsi-keys wrap rotatethe deployment keeps working: new material is sealed under the new key while old material still opens under this one. Remove it once the rotation reports everything re-wrapped.- AUTO_CREATE_IDENTITY
(boolean, optional) Whether an identity may be generated automatically for a served address. Default
YES, but the trigger is lazy: creation happens the first time a local sender explicitly asks for protection, never merely because an address appeared in an envelope, so a user who never uses the feature never has key material. (The option is read and validated today; the stage that acts on it ships with the rest of the end-to-end cryptography work, so until then every identity is created by hand with pepsi-keys(1).)The consequence is worth knowing, because it is the one surprising part of the design: the trigger is on the outbound path, so an address that only ever receives stays keyless and is never published — outsiders therefore have nothing to encrypt to it with. Pre-provision such addresses with
pepsi-keys identity generate.- IDENTITY_VALIDITY_DAYS
(number, optional) Lifetime of a generated identity, in days. Default
730(two years); must be positive. An integer count of days rather than adurationbecause the parser rejects calendar units (see the warning at the top of this page).pepsi-keys identity generate --daysoverrides it for one identity.- LOCAL_DOMAINS, TARGETS, RECIPIENT_DELIMITER
(string, optional) Which domains and accounts an address is attributed to when deciding who owns an identity — the same options, with the same meanings and defaults, that the local-delivery stages use (pepsi-stage-relay-to-maildir(1)).
LOCAL_DOMAINSdefaults to[pepsi-ingress] ACCEPTED_DOMAINS,TARGETSto the/etc/login.defsUID_MIN..``UID_MAX`` range andRECIPIENT_DELIMITERto+. An address that resolves to a local account has that login recorded with its identity and may be managed by that user; one that does not (a role address, a hosted domain) is operator-only.
70.2.1.1.10. The [pepsi-keydiscovery] Section¶
How Pepsi finds a correspondent’s public key or certificate — the input to
pepsi.peer_key, as distinct from [pepsi-crypto] above, which is about
custody of our own key material. Read by the pepsi-keydisc(1) discovery
services, by pepsi-keys(1) (whose peer refresh --address runs the same
fan-out in one process), and by any stage that must park a message on a key it
does not yet have.
The whole section is optional: with none of it, discovery runs with the default source set, which is what an operator who never thought about it should get.
Discovery is asynchronous. A stage that needs a key it does not have in
cache performs no network I/O of its own — it commits its work, pauses the
message and enqueues a request in pepsi.key_request, and a discovery service
answers the request and releases every message parked on that address. See
pepsi-keydisc(1) for the service side and pepsi.state(7) for the
keydisc key a parked message carries.
- SOURCES
(string, optional) Which discovery methods this deployment runs, as a comma- or whitespace-separated list of
dane,wkd-advanced,wkd-direct,ldapandvks(wkdis accepted forwkd-advanced,hkpforvks; duplicates are ignored). Defaultdane wkd-advanced wkd-direct vks—ldapis deliberately absent, being feature-gated and useless until configured.This list is also the roster the stop rule waits for, which has two consequences. A
pepsi-keydisc@<method>instance whose method is not listed refuses to start; and a listed method with no instance running merely costs every request the fullTIMEOUTbefore it settles. Keep the list and the enabled units in step.inboundis rejected: harvesting keys out of a message is not a service, so nothing waits for it.- MIN_TRUST
(string, optional) The trust floor — a key from a worse-ranked source is not used. Named by its source:
manual/api,dane,wkd-advanced,wkd-direct,ldap,vks,harvested(also spelledinboundorautocrypt), orgossip(alsoautocrypt-gossip,any,none). Defaultany— accept anything.That default is a decision, not an oversight: the alternative to encrypting to a trust-on-first-use key is not encrypting to a better key, it is sending cleartext. Raising it is how a deployment that would rather send nothing than send to an unauthenticated key says so. The ranking still does work at the default: it decides conflicts, and it is recorded per key.
any/nonename the bottom rung, whatever it currently is, while every other spelling names one rank exactly. The distinction matters for the two that look alike:harvestedis rank 7 — what a correspondent said about their own address, in anAutocrypt:header or an attached key — andgossipis rank 8, one correspondent’s introduction of another (Autocrypt Level 1 §5.3). SoMIN_TRUST = harvestedis the useful middle setting: keep opportunistic encryption, refuse third-party introductions. Gossiped keys are still stored either way; the floor decides only whether they may be used.The floor does not apply to an address this deployment holds an identity for. Our own key is not a discovered key: it pre-empts the lookup, so no
peer_keyrow for that address is consulted whatever its rank, and no floor can exclude it.own(alsolocal) is accepted as a spelling for completeness — every rank has a name andstate.cryptorenders this one — but as a floor it means “encrypt only to addresses we hold our own key for”, i.e. no external correspondent ever receives ciphertext. See pepsi-stage-encrypt(1) and pepsi-stage-decrypt(1).- TIMEOUT
(duration, optional) Whole-request deadline: how long a request may stay outstanding before it settles with whatever has been found. Default
60 s. Usesh/m/sunits only (see the duration note above).- INBOUND_TIMEOUT
(duration, optional) The same deadline for a message parked on the inbound path — waiting for the key that verifies a signature — defaulting to
TIMEOUT. It is separate precisely so it can be lowered on its own: an inbound park delays exactly the mail somebody is waiting for, so an operator who feels it can drop this to seconds without shortening the outbound deadline. pepsi-setup(1) warns above five minutes.- RANK_GRACE
(duration, optional) The speed-versus-thoroughness dial. When a lookup succeeds, every method still running that ranks better is granted
RANK_GRACE× (how many ranks better it is) of extra time; when that expires, the best answer in hand wins. Default5 s, so one rank up gets 5 s and three ranks up gets 15 s.One knob spanning the whole range:
0means the first usable answer wins outright; the default sits where a slow WKD server cannot hold a message behind a key-server hit that has already arrived, but a nearly-as-fast better source still gets to win; a value at or aboveTIMEOUTmeans every method is waited for and the ranking applies in full (pepsi-setup(1) notes that case rather than treating it as an error — it is a legitimate choice).- METHOD_TIMEOUT
(duration, optional) One method’s own network deadline, covering connect, handshake and every read. Default
15 s. Setting it aboveTIMEOUTmakes a slow method unable to finish inside the whole-request deadline, which pepsi-setup(1) warns about.- CACHE_TTL
(duration, optional) How long a stored key is used before
pepsi-keys peer refreshre-checks it (it becomes the row’srefresh_after). Default24 h.- NEGATIVE_TTL
(duration, optional) How long an authoritative “this address has no key” is remembered. Default
24 h. While the entry is fresh a message for that address takes the no-key path immediately and never parks: pausing 60 s to re-learn a known answer only delays every message to that correspondent. The accepted cost is that a key published five minutes ago stays invisible until the entry ages out — lower this if that matters more than the delay.- ERROR_TTL
(duration, optional) The same, after a method failed rather than answering “nothing here”. Default
1 h: “the server was down” is worth re-trying sooner than “there is no key”.- MAX_RESPONSE_BYTES
(number, optional) Cap on a fetched WKD or VKS response body, enforced while streaming rather than afterwards, so a 10 MB body costs the cap and not 10 MB. Default
262144(256 KiB). Must be greater than zero.- VKS_SERVERS
(string, optional) Verifying key servers to query, in order, as a comma- or whitespace-separated list. Default
https://keys.openpgp.org. Every entry must be anhttps://URL — a key fetched over an unauthenticated channel is not a key there is any reason to believe — and a trailing/is stripped.- ALLOW_DOMAINS, DENY_DOMAINS
(string, optional) Recipient-domain lookup policy, comma- or whitespace-separated and matched case-insensitively on the domain part. A non-empty
ALLOW_DOMAINSis exclusive (only those domains are ever looked up);DENY_DOMAINSalways wins over it. Both empty by default, so every domain may be looked up. An address with no@is never looked up.This is the recipient-side privacy control, and it is the operator’s. The per-user switch below is the sender side; the two are not interchangeable.
- LDAP_URL, LDAP_BASE, LDAP_FILTER, LDAP_ATTRIBUTE, LDAP_BIND_DN, LDAP_BIND_PASSWORD
(string, optional) The directory
pepsi-keydisc@ldapqueries.LDAP_URLmust be anldap://orldaps://URL and, withLDAP_BASE, is required onceldapis inSOURCES.LDAP_FILTERdefaults to(mail={address})and must contain the{address}placeholder (the address is RFC 4515-escaped before substitution, so a local part containing*or(cannot become filter syntax);LDAP_ATTRIBUTEdefaults touserCertificate;binary. An absentLDAP_BIND_DNmeans an anonymous bind.LDAP support is a compile-time cargo feature. On a build without it,
ldapinSOURCESis rejected by pepsi-setup(1) and thepepsi-keydisc@ldapinstance refuses to start.
70.2.1.1.10.1. The per-sender switch: [stage-encrypt] DISCOVERY¶
A stage that consumes the key store reads one option of its own, in its
[stage-<name>] section, so it can be overridden per address through the
pepsi.settings layer (pepsi-settings(1)):
- DISCOVERY
(boolean, optional) Whether a cache miss may trigger a network lookup. Default
YES. WithNOthe cached keys are still used, but a miss goes straight to the stage’s no-key path instead of parking the message — there would be nothing to wait for.
Important
What the per-address override actually means. The settings layer resolves
an outbound message on its envelope sender, so an override of
DISCOVERY for an address means “this user’s mail never triggers a
lookup” — not “never look this correspondent up”. Recipient-side
suppression is the operator’s ALLOW_DOMAINS/DENY_DOMAINS above; there
is deliberately no per-address recipient denylist, which would need its own
column and a second list to stay consistent with this one.
DISCOVERY is read by pepsi-stage-encrypt(1) and
pepsi-stage-decrypt(1) from that stage’s effective section, so a
pepsi.settings override applies. Their other options are documented under
pepsi-stage-encrypt Options and pepsi-stage-decrypt Options.
70.2.1.1.11. The [pepsi-keys] Section¶
The mirror image of the section above: how other people find our users’ keys. Read by pepsi-keys(1) and by pepsi-stage-vks-confirm(1). The whole section is optional, and a configuration that omits it publishes nothing to any key server.
Web Key Directory publication needs no options at all. It follows each
identity’s published flag (on by default, cleared with pepsi-keys identity
unpublish) and is served by pepsi-httpd(1) from our own domain, so it
takes effect on the next request and is undone by clearing the flag. What this
section configures is the other channel, which behaves nothing like it: a key
server accepts a key permanently — it can later be told the key is revoked,
never told to forget it.
- VKS_PUBLISH
(boolean, optional) Whether every published OpenPGP identity is uploaded to the key server automatically and retried until verified. Default NO.
Deliberately an operator-level switch and not a per-identity one. An upload cannot be withdrawn, so the decision belongs to whoever understands that rather than to whoever happened to generate a key; and a per-user opt-in is a step essentially nobody takes, so making it per-user would mean key-server discoverability never materialised at all. What it must not be is a consequence of a default: uploading a user’s address to a third party should be something an operator decided. With the switch off,
pepsi-keys identity publish <ID> --vksremains available as a confirmed one-off.- VKS_SERVER
(string, optional) The key server uploads go to;
https://only, and a plaintext URL is refused rather than downgraded. Defaults to the first[pepsi-keydiscovery] VKS_SERVERSentry, so a deployment that already chose a key server to read from does not name it twice.One server, not a list: publishing the same key to several multiplies an irreversible act, and every one of them then has to be kept up to date with revocations.
Its host is also the default
VKS_HOSTof pepsi-stage-vks-confirm(1) — the one host that stage will accept a verification mail from and follow a link to.- VKS_MAX_ATTEMPTS
(integer, optional) How many attempts one identity gets before
pepsi-keys identity publish --retryleaves it alone. Default5. The row keeps its last error, so an abandoned identity says why.- VKS_RETRY_INTERVAL
(duration, optional) Minimum spacing between attempts for one identity. Default
6 h. Calendar units are rejected:6 hparses,1 ddoes not.- VKS_BATCH
(integer, optional) How many identities one
--retryrun works on, so a first run over a large deployment is not one enormous burst at a third party. Default50.
70.2.1.1.12. The [pepsi-ingress] Section¶
Global options governing message acceptance, read by pepsi-ingress(1).
- HOSTNAME
(string, required) Host name announced in the SMTP greeting and the
EHLOresponse, e.g.mail.example.org.- ACCEPTED_DOMAINS
(string, required) Whitespace- or comma-separated list of domains for which mail is accepted. Recipients in any other domain are rejected as relaying with a
550reply. Matching is case-insensitive. At least one domain must be given.- USERNAME_MAP
(path, optional) File mapping each SASL authentication id to the e-mail addresses it may use as the envelope sender and
From:identity (RFC 6409 §6/§8.1). Each non-blank, non-comment line isusername: addr…— a#begins a comment, the key is the SASL username, and the value is the whitespace/comma separated list of allowed addresses (which may be empty). A username listed with addresses is allowed exactly those (the first concrete one being the canonical identity used for theSender:fixup); an address may be a*wildcard such as*@example.org(any address at the domain) or a bare*(any address). A username listed with no addresses is denied any address (itsAUTHis refused even though the credentials are valid); a username not listed falls back to the defaultusername@HOSTNAME. The file is re-read whenever its modification time changes (checked on each authentication); a missing/unreadable file is treated as empty. When unset, every authenticated user is mapped to its defaultusername@HOSTNAME. See pepsi-ingress(1).- POSTMASTER
(string, optional) Routable address the domainless reserved
<Postmaster>mailbox is rewritten to. RFC 5321 §4.5.1 requires<Postmaster>(with no domain) to be accepted regardless ofACCEPTED_DOMAINS; on acceptance Pepsi rewrites it to this address so the normal pipeline (aliases, local delivery, relay) can route it. When unset it is rewritten topostmaster@HOSTNAME— add an alias for that address in apepsi-stage-aliasesALIASESmap to deliver postmaster mail to a real person, or setPOSTMASTERto that person directly. See pepsi-ingress(1).- MAX_MESSAGE_SIZE
(number, optional) Maximum accepted message body size in bytes — the size of the
DATApayload. The value is advertised through the ESMTPSIZEextension; messages exceeding it are rejected with552. Defaults to26214400(25 MiB).- MAX_CONNECTIONS
(number, optional) Maximum number of SMTP connections served concurrently across all listeners. The effective admission ceiling is the smaller of this and
MAX_OPEN_SOCKETS. Defaults to1024.- DB_POOL_SIZE
(number, optional) Maximum number of PostgreSQL connections in the ingress database pool. Ingress serves many SMTP sessions concurrently, each briefly calling
ingress_add, so unlike the single-connection stage workers it can use more than one. Defaults to1; raise it only if inbound acceptance is bottlenecked on the single connection, and keep the sum of every component’s pool below PostgreSQL’smax_connections.- MAX_OPEN_SOCKETS
(number, optional) Hard ceiling on concurrently open client connections, guarding against file-descriptor exhaustion. When unset, it is derived from the process’s soft
RLIMIT_NOFILEminusRESERVED_SOCKETS. When the ceiling is reached and a new connection arrives, the server makes room by closing the longest-idle connection in the source range (one IPv4 address, or an IPv6/32) whose total idle time is largest (a421, then close).- RESERVED_SOCKETS
(number, optional) File descriptors held back from the
RLIMIT_NOFILE-derived budget (for the database pool, listeners, stdio and DNS sockets). Defaults to64. Ignored whenMAX_OPEN_SOCKETSis set.- CONN_RATE_PER_SECOND
(number, optional) Token-bucket refill rate limiting accepted new connections per source range, in connections per second. A connection over the rate is refused with
421and closed without taking a slot. Defaults to1. Local (UNIX-socket) connections are never rate-limited.- CONN_RATE_BURST
(number, optional) Token-bucket burst allowance per source range. Defaults to
1.- COMMAND_TIMEOUT
(duration, optional) Longest a session waits for the next command (or
AUTHcontinuation, the start of aDATA/BDATbody, or completion of aSTARTTLS/implicit-TLS handshake) before the connection is closed with421(RFC 5321 §4.5.3.2). Defaults to300 s.- DATA_TIMEOUT
(duration, optional) Longest a session waits for the next block of payload octets while receiving a
DATA/BDATbody before closing with421. Defaults to180 s.- DMARC_ENFORCE
(boolean, optional) When
YES, reject at SMTP time (550) a message whose DMARC evaluation fails and whose published policy isquarantineorreject. Defaults toNO: as a forwarder, pepsi-ingress(1) normally accepts such mail (and pepsi-stage-arc(1) seals the failing result) so the downstream receiver can decide. Other authentication failures never reject a message.- DNS_SERVERS
(string, optional) Whitespace- or comma-separated list of resolver IP addresses (IPv4 and/or IPv6) used for the SPF/DKIM/DMARC lookups, queried on port 53. If unset, the system resolver configuration (
/etc/resolv.conf) is used.- DNS_TIMEOUT
(duration, optional) Per-query timeout for those lookups. Defaults to
5 s. Because verification runs synchronously while receivingDATA, a slow resolver delays the250acceptance.- DISPATCH_WAKE_INTERVAL
(duration, optional) Minimum spacing between two
NOTIFYs telling pepsi-dispatch(1) that new mail has arrived. Defaults to5 ms;0disables coalescing and notifies once per accepted message.Ingress does not notify from inside the transaction that stores the message. PostgreSQL holds a database-wide lock from the moment a transaction queues a notification until it commits, so notifying transactions cannot group-commit — under concurrent load they serialise, each paying its own disk flush. Admissions are therefore notified out of band, once per interval, after the rows have committed; because the dispatcher answers any wake-up with a full scan of the queue, one notification serves an arbitrary number of newly accepted messages.
The limit is trailing: the first wake-up after an idle period is sent immediately, so a lone message is not delayed at all, and only notifications behind it are merged. Raising this trades a little worst-case queue latency for fewer notifications under sustained load; there is rarely a reason to.
The ARC sealing identity and algorithm are configured in the shared [pepsi]
section (ARC_DOMAIN and ARC_ALGORITHM) above and used by
pepsi-stage-arc(1), not by ingress.
70.2.1.1.13. The [pepsi-ingress-listener-*] Sections¶
Each section whose name begins with pepsi-ingress-listener- defines one
listening socket. The suffix is a free-form name used in log messages, e.g.
[pepsi-ingress-listener-mx]. Declare as many listeners as required.
- SERVE
(required) The kind of socket to bind. One of:
tcpBind a TCP socket. Requires BIND_TO and PORT.
unixBind a UNIX-domain socket. Requires UNIXPATH; honours UNIXPATH_MODE and UNIXPATH_GROUP.
systemdAdopt a socket passed by a systemd parent through socket activation; see FD_INDEX.
- BIND_TO
(string) IP address to listen on when
SERVE = tcp, e.g.0.0.0.0or::. Required fortcp.- PORT
(number) TCP port to listen on when
SERVE = tcp. Required fortcp.- UNIXPATH
(path) Filesystem path of the socket when
SERVE = unix. Required forunix.- UNIXPATH_MODE
(unix mode, optional) Permission bits of the UNIX-domain socket, in octal. Defaults to
660.- UNIXPATH_GROUP
(string, optional) Group name the UNIX-domain socket is
chgrped to after binding (the owner is unchanged). Combined with the default0660mode this lets a specific peer reach the socket while it stays non-world-accessible — used when pepsi-httpd runs behind a front web server (e.g.www-data) that reverse-proxies to it. The group must exist; an unknown group is a startup error.- FD_INDEX
(number, optional) Zero-based index of the systemd-passed file descriptor to adopt when
SERVE = systemd. Index0is the first descriptor (SD_LISTEN_FDS_START). Defaults to0.It is a position, not a name: it counts
ListenStream=entries in the owning.socketunit, so inserting one renumbers every listener after it while appending one renumbers nothing. The unit and these sections are two halves of one statement and nothing checks that they agree at configuration time, so a mismatch is reported where the descriptors actually are: the server logs a warning at start-up naming every index no listener claimed. Take it seriously — a socket the server never accepts on is worse than an absent one, because systemd still queues the connection and the client blocks until its own read timeout expires rather than being refused at once.A descriptor may be a UNIX-domain socket as readily as a TCP one; which it is lives in the unit, not here. Options that depend on the socket’s family are therefore resolved once the descriptor is in hand rather than while parsing: AUTH_PEERCRED must be stated explicitly (it cannot be inferred as it is for
SERVE = unix), and a plaintext SUBMISSION listener that turns out to have inherited a network socket makes pepsi-ingress(1) refuse to start rather than carry credentials in the clear.- MODE
(optional) Transport security of the listener. One of:
plainCleartext only;
STARTTLSis not advertised. This is the default.tlsImplicit TLS: the connection is wrapped in TLS from the first byte (e.g. the
submissionsport 465). Requires TLS_CERT and TLS_KEY.starttlsCleartext that may be upgraded with the
STARTTLScommand. Requires TLS_CERT and TLS_KEY.
- TLS_CERT
(path) PEM file containing the server certificate chain. Required when MODE is
tlsorstarttls— but may be omitted, in which case pepsi-setup(1) auto-fills the certbot path/etc/letsencrypt/live/<HOSTNAME>/fullchain.pemand can obtain the certificate (see pepsi-setup(1) and its –no-certbot option).- TLS_KEY
(path) PEM file containing the private key for TLS_CERT. Required when MODE is
tlsorstarttls; like TLS_CERT it may be omitted for pepsi-setup(1) to auto-fill (…/privkey.pem).- MYNETWORKS
(optional) Whitespace/comma separated list of trusted IPv4/IPv6 CIDR networks (a bare address is a
/32or/128host). A connection from any of these is authenticated without further proof. Empty by default.- AUTH_PEERCRED
(optional, default ``yes`` on a ``SERVE = unix`` listener, ``no`` elsewhere) Authenticate the client by its kernel-reported UNIX-socket peer credentials (
SO_PEERCRED): the connecting process’s user id is resolved to a login through the passwd database, and that login is looked up in USERNAME_MAP exactly as a SASL username would be. The session is then authenticated and carries a submission identity, so the RFC 6409 §6/§8.1 rules apply — a local program can only use an envelope sender andFrom:its own account is mapped to, and an account mapped to no address is refused.This is the mechanism behind pepsi-sendmail(1) and the reason the socket may safely be world-writable: being able to open it decides nothing about who you can send as. Unlike MYNETWORKS, which trusts a network position and therefore lets any local process send as anyone, the identity here is supplied by the kernel and cannot be forged by the client.
Rejected on a
SERVE = tcplistener, where there are no peer credentials to read. Set it tonofor an unauthenticated local socket (for instance behind a proxy) — but such a listener cannot then also be a SUBMISSION listener, since nothing would authenticate.With
SERVE = systemdit is accepted but not inferred, and so must be written out: whether the inherited descriptor is a UNIX socket or a TCP one is a property of the.socketunit rather than of this file, and the shippedpepsi-ingress.socketpasses both kinds. If the descriptor turns out to be a TCP socket the option authenticates nobody —SO_PEERCREDhas no meaning there, so every session stays anonymous — which is the safe direction but a silent one, so pepsi-ingress(1) warns at start-up naming the listener.- SASL_TYPE
(optional) SASL backend for the
AUTHcommand:none(default) ordovecot.AUTH(mechanismsPLAINandLOGIN) is advertised and accepted only on a TLS-protected session; a cleartext attempt is refused with538. Any value other thannonerequires MODEtlsorstarttls.- SASL_PATH
(path) Path of the Dovecot auth-client socket. Required when SASL_TYPE is
dovecot. The default (/run/dovecot/auth-client-pepsi) is a Pepsi-private listener rather than Dovecot’s sharedauth-clientsocket: the latter is0600 dovecotand unreachable by the unprivilegedpepsi-ingressuser.pepsi-setup runprobes this socket as the service user and, if it is missing or unreadable, offers to install a/etc/dovecot/conf.d/10-pepsi.confthat adds a dedicated listener owned bypepsi-ingress(or prints it for you to deploy).- TLS_AUTH_CLIENT
(optional) Whitespace/comma separated list of SHA-256 hashes (64 hex characters) of the DER
SubjectPublicKeyInfoof authorised client or CA public keys. When set, the listener requests a client certificate: a session is authenticated if the presented leaf’s key hash is listed, or if its chain signature-validates up to a listed CA key. An unrecognised certificate does not abort the handshake. Requires MODEtlsorstarttls. Compute a hash withopenssl x509 -in cert.pem -noout -pubkey | openssl pkey -pubin -outform DER | openssl dgst -sha256.- SUBMISSION
(optional, default ``no``) When
yes, the listener is an RFC 6409 message submission agent: authentication becomes mandatory (an unauthenticatedMAIL FROMis refused with530 5.7.0), and every accepted message gets the submission fixups — a missingDate:andMessage-ID:are added. Requires at least one authentication mechanism (SASL_TYPE, TLS_AUTH_CLIENT, MYNETWORKS or AUTH_PEERCRED); a submission listener with none is rejected. Leave it off for an MX listener (port 25).A TLS MODE is required too, except on a
SERVE = unixlistener: a filesystem socket has no transport an attacker could observe or interpose on, and its authentication comes from the kernel rather than from the wire, so requiring a certificate there would protect nothing.
A session accepted by any of MYNETWORKS, AUTH_PEERCRED, SASL AUTH or
TLS_AUTH_CLIENT is recorded with state.local_origin = true (otherwise
false) and may relay to any domain, not only the served domains.
70.2.1.1.14. The Stage Pipeline and [stage-*] Sections¶
After ingress, a message is advanced by a chain of stage programs.
pepsi-dispatch(1) claims a pending row, sets it running and runs
the program named by its stage’s section with the message id as the only
argument; the program does its work and advances the row (or finishes it). New
messages enter at the initial stage, [stage-init], which must exist.
Each section whose name begins with stage- describes one stage. The suffix
is the stage name, e.g. [stage-init]. The stage name is an
operator-chosen label, independent of the binary it runs.
- PROGRAM
(string, required) The stage binary to run, e.g.
pepsi-stage-relay-to-smarthost. It is located on thePATHunless given as an absolute path. The same binary may serve several stages (it reads its options from whichever section the message is currently at).- NEXT_STAGE
(string, optional) The stage a message advances to on success. With none, a successful terminal stage removes the message.
- BOUNCE_STAGE
(string, optional) The stage a message is routed to so a delivery-status notification is generated. For delivery stages (pepsi-stage-relay-to-smarthost(1), pepsi-stage-relay-to-internet(1)) and pepsi-stage-discard(1) it receives a permanently-failed (non-bounce) message for a bounce, and — when
[pepsi]ORIGINATE_SUCCESS_DSNis enabled — a successfully-delivered message for a positive report. With none, a permanently-failed message is markedfailed(delivery stages) or simply dropped (pepsi-stage-discard(1)).- PARALLELISM
(number, optional) Upper bound on the number of concurrent worker processes pepsi-dispatch(1) runs for this stage. Workers are started on demand and stopped when idle, so this is a ceiling, not a fixed pool size. Each worker holds one database connection, so this also bounds the stage’s share of the connection budget (see the
[pepsi-postgres]note above). Default 4. The dispatcher temporarily lowers a stage below this value when the database reports its connection limit is exhausted (see pepsi-dispatch(1)).- MAX_MESSAGES
(number, optional) Number of messages a worker of this stage handles before the dispatcher retires it and starts a fresh process (bounding memory growth). Default 1000.
- QUEUE_LIMIT
(number, optional) Upper bound on the number of messages pepsi-dispatch(1) pipelines to a single worker process of this stage at once. The worker still processes them strictly one at a time, but having the next ids already queued on its standard input removes a coordinator round-trip per message, so the stage’s total in-flight capacity is
QUEUE_LIMIT × PARALLELISM. Because the worker count (and thus the connection budget) is unchanged, raising this trades a little head-of-line latency (a message can wait behind a slower pipelined sibling on the same worker) for throughput, without spending more database connections; lower it (e.g. to1) for a stage whose per-message work is long and uneven, such as a network relay. Default 4.- FUSION =
yes|no (optional) Whether a predecessor stage may run this stage in its own worker process instead of writing the row
pendingand waiting for the dispatcher to claim it (stage fusion; see[pepsi] ALLOW_FUSIONabove and pepsi-dispatch(1)). Fusion happens only when it is also globally enabled, this stage’s PROGRAM is folded into the unifiedpepsibinary, and this stage needs no message column the predecessor did not already load — so it is refused (and the advance falls back to a normal dispatched hop) whenever any of those does not hold, which is always safe. Defaults toyesfor the fast, body-free routing/classification stages — pepsi-stage-if(1), pepsi-stage-discard(1), pepsi-stage-srs(1), pepsi-stage-check-whitelist(1), pepsi-stage-auto-whitelist(1) and pepsi-stage-block-language(1) — andnofor every other program. Of those, pepsi-stage-block-language(1) is the one that needs the header block (itsENFORCEMENT = softpath rewrites theSubject:), so it fuses only into a predecessor that had already loaded it.
The remaining options in a [stage-<name>] section depend on its PROGRAM.
The subsections below document the options each stage program reads from its own
section; a section reads only the keys listed for its PROGRAM plus the
general keys above. The shared sections these programs also draw on are
documented elsewhere on this page: [pepsi] (signing identity), [pepsi-srs]
(the SRS engine), [pepsi-payments] (the merchant backend) and the smarthost
[pepsi-stage-relay-to-smarthost-mta-*] sections.
70.2.1.1.14.1. pepsi-stage-arc Options¶
A stage with PROGRAM = pepsi-stage-arc reads, besides a required
NEXT_STAGE, its DNS settings and the signature parameters of the ARC set it
adds (these apply to the ARC-Message-Signature; the ARC-Seal is always
relaxed-canonicalized over a fixed header set, per RFC 8617):
- DNS_SERVERS
(string, optional) Whitespace/comma-separated explicit resolvers (port 53) for the SPF/DKIM/DMARC/ARC lookups. When unset, the system
resolv.confis used.- DNS_TIMEOUT
(duration, optional) Per-query DNS timeout. Default
5 s.- HEADER_CANONICALIZATION
(``relaxed`` | ``simple``, optional) Header canonicalization of the AMS (the left half of
c=). Defaultrelaxed.- BODY_CANONICALIZATION
(``relaxed`` | ``simple``, optional) Body canonicalization of the AMS (the right half of
c=). Defaultrelaxed. ARC supports any combination of the two.- COVER_BODY
(boolean, optional) When
yes(default) the AMS strictly covers the whole body. Whennoit carries a body-length (l=) tag fixed to the body as signed, so a later hop may append a footer without invalidating the AMS.- SIGNATURE_EXPIRATION_DAYS
(integer, optional) When set to a positive number of days, the AMS and ARC-Seal carry an expiration (
x=) that many days after signing. Unset (the default) emits no expiration.- SIGNED_HEADERS
(string, optional) Whitespace/comma-separated list of headers the AMS covers (
h=). Must includeFrom. Defaults to the built-in originator/MIME header set.
The ARC signing identity, key directory, selector and algorithm come from the
shared [pepsi] section (ARC_DOMAIN, ARC_ALGORITHM, KEY_DIR,
DKIM_SELECTOR). The hash is always SHA-256; the signature algorithm (a=)
is the single [pepsi] ARC_ALGORITHM choice, since ARC permits one signature
per hop.
70.2.1.1.14.2. pepsi-stage-srs Options¶
A stage with PROGRAM = pepsi-stage-srs needs only a NEXT_STAGE in its own
[stage-<name>] section; all its parameters (SRS_DOMAIN,
SECRET/SECRET_FILE, MAX_AGE_DAYS) live in the shared [pepsi-srs]
section documented above, so they are identical to ingress’s reverse-decode.
70.2.1.1.14.3. pepsi-stage-encrypt Options¶
A stage with PROGRAM = pepsi-stage-encrypt signs locally submitted mail as
its From: author and encrypts it to each recipient. It reads, besides a
required NEXT_STAGE and an optional BOUNCE_STAGE:
Warning
Place this stage before the DKIM-signing stage, not after:
… → encrypt → srs → dkim-sign → relay. DKIM must sign the bytes that are
actually transmitted, and this stage rewrites the body. Signing first yields
mail that looks valid and whose signature does not verify — worse than no
signature, because a broken one is a stronger negative signal to a receiver
than an absent one. pepsi-setup(1) warns when it finds a DKIM-signing
stage that leads to this one.
The pay-to-send gate and language detection both read the message body and both want cleartext; on the outbound path they run before this stage, which is why encryption is last.
Every option below is behavioural and is per-address overridable through the
pepsi.settings table (pepsi-settings(1)), keyed for outbound mail on
the envelope sender. The cryptographic policy — algorithms, downgrade
permission, minimum key sizes — is in [pepsi]’s CRYPTO_* options and
[pepsi-crypto], is system-administrator-only, and is deliberately not
reachable from here.
- SIGN
(``no`` | ``opportunistic`` | ``always``, optional) Whether to sign with the
From:author’s key. Defaultopportunistic: sign when the author has usable material, otherwise send unsigned.alwaysadditionally asks for an identity to be created when[pepsi-crypto] AUTO_CREATE_IDENTITYpermits it. Neither is a guarantee — an author this deployment does not serve is sent unsigned rather than bounced.- ENCRYPT
(``no`` | ``opportunistic`` | ``required``, optional) Default
opportunistic: encrypt to a recipient we have a usable key for, send ordinary mail to one we do not.requiredmeans a recipient with no usable key gets the secure link or a bounce, never cleartext.optionalis accepted as a synonym foropportunistic.- PREFER
(``openpgp`` | ``smime``, optional) Which protocol to reach for when a correspondent has usable material for both. Default
openpgp;pgpis accepted as a synonym for it.- ON_NO_KEY
(``cleartext`` | ``secure-link`` | ``bounce``, optional) What happens to a recipient with no usable key. Defaults from ENCRYPT:
cleartextforno/opportunistic, and forrequiredeithersecure-link(whenSECURE_LINK_STAGEis set) orbounce. Setting it explicitly overrides that — except that a message whose own request raised the policy torequirednever falls back tocleartext.Caution
ON_NO_KEY = cleartexttogether with[pepsi] CRYPTO_ALLOW_DOWNGRADE = nosends a correspondent whose key cannot read AES-GCM plaintext — strictly worse than the SEIPDv1+MDC the option was set to avoid, and most of the OpenPGP installed base (GnuPG 2.4 and older) is in that position. pepsi-setup(1) warns about the pair. Leave the option alone: bothnegotiate(compiled in) andyes(whatconfig.dships) downgrade rather than refuse, and undernegotiateonly when the recipient’s own key proves they must.- ON_OVERSIZE
(``cleartext`` | ``secure-link`` | ``bounce``, optional) What happens to a message larger than
MAX_SIZE. Defaults exactly likeON_NO_KEY, sorequireddoes not become cleartext because an attachment was large.- MAX_SIZE
(number, optional) Largest message, in bytes, this stage will encrypt. Default
25000000(25 MB). Neither the CMS builder nor the OpenPGP paths stream, so the whole message is resident while it is encrypted, once per recipient; a visible cap is what keeps the per-worker memory ceiling predictable instead of lettingPARALLELISMworkers each hold a multi-hundred-megabyte message.- MIN_TRUST
(``manual`` | ``dane`` | ``wkd-advanced`` | ``wkd-direct`` | ``ldap`` | ``vks`` | ``harvested`` | ``gossip``, optional) The lowest-ranked key source this stage will encrypt to. Defaults to
[pepsi-keydiscovery] MIN_TRUST, so a deployment has one floor unless it deliberately wants two. See The [pepsi-keydiscovery] Section for the ranking, for why the default floor accepts anything, and for the difference betweenharvestedandgossip.- SUBJECT_KEYWORDS_SIGN, SUBJECT_KEYWORDS_ENCRYPT, SUBJECT_KEYWORDS_BOTH
(comma-separated list, optional) Keywords a sender may put anywhere in the
Subjectto ask for protection. Defaults[sign],[encrypt]and[secure]. Matched case-insensitively; the matched keyword is removed from the outgoingSubject. An encryption keyword raises the policy torequiredfor that message — reading a deliberate human request as “try, and quietly give up” would make the feature a lie. Commas, not spaces, separate the list, because a keyword may legitimately contain a space.- STRIP_REQUEST_HEADERS
(boolean, optional) Remove every
X-Pepsi-*field from the outgoing message. DefaultYES. The two headers this stage acts on (X-Pepsi-Sign,X-Pepsi-Encrypt) are removed unconditionally, whatever this says: a control channel that survives onto the wire is one the next hop can be told to obey. This option decides only the fate of the otherX-Pepsi-*fields an add-in may have added.Pepsi-Originis not in that namespace and is never touched.- PROTECT_HEADERS
(boolean, optional) Copy the RFC 5322 header fields into the protected part and replace the outer
Subjectwith...— what Thunderbird implements for PGP/MIME. DefaultNO: client support is uneven, and the failure mode is a message whose subject reads...in a client that cannot look inside.- AUTOCRYPT
(boolean, optional) Advertise the
From:author’s own OpenPGP key in anAutocrypt:header. DefaultYES, and on every locally submitted message — signed, encrypted or plain cleartext — because a correspondent has to learn the key from ordinary mail before there is anything to encrypt with.The key is reduced to the minimal five-packet form of Autocrypt Level 1 §2.1.1. Only OpenPGP is advertised (Autocrypt has no S/MIME form), only for an identity that still holds its private material, and never on a message that already carries an
Autocrypt:field — a real client’s own header names the key it can decrypt with, and a second field makes Level 1 parsers discard both.prefer-encrypt=mutualis claimed only when the effectiveENCRYPTisrequired; see pepsi-stage-encrypt(1).Turn it off to keep key publication to the Web Key Directory and DNS, which is a defensible choice for a deployment that does not want its users’ keys travelling on every message they send.
- AUTOCRYPT_GOSSIP
(boolean, optional) Advertise the other recipients’ OpenPGP keys in
Autocrypt-Gossip:fields (Autocrypt Level 1 §5.3), so that two correspondents who have never written to each other can nevertheless answer each other encrypted after one message. DefaultNO.Off by default because it is not the same kind of act as
AUTOCRYPT: that one publishes a key its owner asked this deployment to hold, while this one redistributes third parties’ keys — material merely cached here — to correspondents who did not ask for them.It applies only to a message this stage actually encrypts, and the fields go inside the ciphertext, never on the outer header block: their whole purpose is to introduce the recipients to each other without disclosing the recipient set to the network. There is therefore no gossip on the cleartext pass-through, on a signed-but-unencrypted message, or on an S/MIME container (Autocrypt is defined over OpenPGP and has no S/MIME form). One field is emitted per introduced recipient — unlike
Autocrypt:, where a second field makes a Level 1 parser discard the message — andprefer-encryptnever appears, since it states the sender’s own policy and this field speaks for somebody else.Warning
Blind carbon copies are never gossiped. The addresses advertised are read from the message’s parsed
To:andCc:fields and from nowhere else; the envelope recipient list is consulted only to remove an address from the set. ABccrecipient is on the envelope and in neither field, so no copy of the message can carry a header naming them — which is the property to rely on, because a gossip header for a blind recipient would tell every other recipient that a hidden one exists and who it is. This holds even if aBcc:field was left on the message by a broken submitting client, because the field is not read.The converse is deliberate: a blind recipient’s own copy does carry gossip about the
To:/Cc:recipients. They received those header fields like everyone else, so it discloses nothing they cannot already read, and it is what lets them reply encrypted.A recipient this deployment holds no cached key for is simply left out — the stage performs no key discovery on their behalf and never delays a message to fetch a third party’s key. A message with more than 50 visible recipients gets no gossip at all: at that size it is a broadcast rather than a conversation, each introduced key is repeated in every recipient’s own ciphertext, and introducing an arbitrary subset would be worse than introducing none.
- SECURE_LINK_STAGE
(stage name, optional) Where a recipient taking the
secure-linkroute is sent. Required ifON_NO_KEY/ON_OVERSIZEresolves tosecure-link.- DISCOVERY
(boolean, optional) Whether a cache miss may trigger a network lookup; documented under The per-sender switch: [stage-encrypt] DISCOVERY.
- DISCOVERY_TIMEOUT
(temporal, optional) Override
[pepsi-keydiscovery] TIMEOUTfor the parks this stage creates. Note thatSection::duration()rejects calendar units:90 sand2 hparse,1 ddoes not.- LOCAL_DOMAINS, TARGETS, RECIPIENT_DELIMITER
(optional) The same locality options, with the same defaults, as
[pepsi-crypto]and pepsi-stage-decrypt Options — read here only to attribute a lazily created author identity to a passwd login, so that the ownership rule of The [pepsi-crypto] Section has something to key on. An address that resolves to no local account is a legitimate state (a role address or a hosted domain), not a failure. Whether an identity may be created at all is[pepsi-crypto] AUTO_CREATE_IDENTITY, and it is offered only for an author in a[pepsi-ingress] ACCEPTED_DOMAINSdomain.
The binary is installed setuid pepsi-crypto, mode 4750
pepsi-crypto:pepsi, because it must be the one database role granted
crypto_identity.private_wrapped — peer authentication keys off the effective
uid — and must read the 0640 key-encryption-key fragment owned by the same
account. See pepsi-stage-encrypt(1).
70.2.1.1.14.4. pepsi-stage-decrypt Options¶
A stage with PROGRAM = pepsi-stage-decrypt decrypts mail addressed to
recipients this host serves and verifies whatever signature it carries. It reads,
besides a required NEXT_STAGE and an optional BOUNCE_STAGE:
- ENABLED
(boolean, optional) Whether to do any cryptography at all. Default
YES.NOstill removes theX-Pepsi-*header namespace from inbound mail: a disabled stage must not become a channel for a forged indicator.- LOCAL_DOMAINS, TARGETS, RECIPIENT_DELIMITER
(optional) Which recipients this host serves — the same options, with the same defaults, as pepsi-stage-relay-to-maildir(1) and pepsi-stage-dot-forward(1).
LOCAL_DOMAINSdefaults to[pepsi-ingress] ACCEPTED_DOMAINS.- REQUIRE_ACCOUNT
(boolean, optional) Whether a local recipient must additionally resolve to a passwd account permitted by
TARGETS. DefaultNO, which is the difference from the delivery stages: they must know whose mailbox to write, while this stage only has to know whether the message is ours. A hosted domain whose users hold crypto identities but no shell accounts is an ordinary deployment.- ON_DECRYPT_FAILURE
(``passthrough`` | ``quarantine`` | ``bounce``, optional) What to do with a message this host could not open. Default
passthrough: the user may hold the key in their own client, and bouncing mail we merely could not open helps nobody.quarantineneedsQUARANTINE_STAGE;bounceneedsBOUNCE_STAGE.- ON_BAD_SIGNATURE
(``record`` | ``quarantine`` | ``bounce``, optional) What to do with a message whose signature did not verify. Default
record: mailing lists that rewrite bodies, forwarders and clients with broken canonicalisation all produce bad signatures on entirely legitimate mail. Only theinvalidverdict counts as bad here —valid-untrustedandunverifiableare the normal state of mail from an unknown correspondent and are never routed.- QUARANTINE_STAGE
(stage name, optional) Where a
quarantineroute leads. Required if either policy above is set toquarantine.- SUBJECT_TAGS
(boolean, optional) Prepend the earned tags to the
Subjectin nesting order —encrypted(signed(body))gives[decrypted][verified] …andsigned(encrypted(body))would give[verified][decrypted] …. DefaultYES: for most users this is the only signal they will ever see. The cost is a mutated user-visible field, which affects search, threading and quoted subjects in replies. (Both shapes open. The second earns[verified]only from a signature inside the ciphertext: an outer signature covers ciphertext and never reaches thevalidverdict the tag is written for — see pepsi-stage-decrypt(1).)- TAG_DECRYPTED, TAG_VERIFIED
(string, optional) The tag wording. Defaults
[decrypted]and[verified].TAG_VERIFIEDis written for thevalidverdict only.- TAG_BAD_SIGNATURE
(string, optional) The tag for the
invalidverdict. Empty by default: a bad signature is a routine property of legitimate mail, so a default-on tag would decorate a large part of a user’s inbox with an accusation.- STRIP_SUBJECT_TAGS
(boolean, optional) Remove this stage’s own tag vocabulary from the front of an inbound
Subjectbefore prepending ours. DefaultYES, and effectively mandatory: without it an external sender simply writes[verified]themselves. The vocabulary is derived from the threeTAG_*options plus the built-in[decrypted]and[verified], so renaming a tag cannot reopen the hole.- STRIP_SUBJECT_KEYWORDS
(comma-separated list, optional) Extra inbound subject tags to remove. Commas rather than whitespace, because a keyword may contain a space.
- ADD_RESULT_HEADER
(boolean, optional) Add the
X-Pepsi-Cryptoresult header to a message that carried protection. DefaultYES. Its grammar is documented in pepsi-stage-decrypt(1). EveryX-Pepsi-*field is removed from inbound mail first, unconditionally and whatever this option says — that removal is what makes the header believable.Note the asymmetry with the sending half (
[stage-encrypt] AUTOCRYPT_GOSSIP, defaultNO): emitting gossip redistributes third parties’ keys to correspondents who did not ask for them, while reading it only fills this host’s own cache. An operator who wants the introductions stored but never used sets[pepsi-keydiscovery] MIN_TRUST = harvestedinstead of turning this off.- MAX_LAYERS
(integer, optional) How many nested encrypted layers are unwrapped. Default
4. A message with more is delivered with the remainder unopened.- TRUST_ANCHOR_TTL
(temporal, optional) How long a worker process reuses the
ca_trustanchors it last read. Default5 m. An anchor added or disabled becomes effective within this window; restart the workers to apply it at once. Calendar units are rejected (5 mand2 hparse,1 ddoes not).- DISCOVERY
(boolean, optional) Whether a cache miss may trigger a network lookup; documented under The per-sender switch: [stage-encrypt] DISCOVERY. Read from this section by the same reader, so the two crypto stages cannot disagree about its name or its default.
- DISCOVERY_TIMEOUT
(temporal, optional) Override
[pepsi-keydiscovery] INBOUND_TIMEOUTfor the parks this stage creates.
[pepsi] CRYPTO_ALLOW_DOWNGRADE does not gate this direction. OpenPGP
SEIPDv1+MDC and CMS AES-CBC EnvelopedData are always readable, whatever the
emission policy says: refusing them inbound would make this host unable to
receive mail from most of the world while protecting nobody. Only 3DES and the
weak signature digests are gated inbound, and each refusal produces its own
verdict.
The binary is installed setuid pepsi-crypto, mode 4750
pepsi-crypto:pepsi, for exactly the reasons given for
pepsi-stage-encrypt(1) above. See pepsi-stage-decrypt(1).
The [pepsi-secure-link] Section
——————————-
The secure-link fallback portal: where a message that could not be encrypted is
held, and how the recipient is let in. Read by three programs — the stage that
stores a message (pepsi-stage-secure-link), the server that serves it
(pepsi-httpd(1)) and the operator CLI (pepsi-secure-link(1)) — which
is why it is one global section rather than options copied into a [stage-*]
one.
That placement is also a decision about who may change these values. This is
not a [stage-<name>] section, so it is out of reach of pepsi.settings and
pepsi-stage-edit-settings(1): a correspondent cannot mail themselves a
longer expiry, a shorter PIN or a disabled lockout. The genuinely behavioural
knob — whether an unencryptable message goes to the portal at all — is
[stage-encrypt] ON_NO_KEY, which is per-address overridable.
- BASE_URL
(string) Public origin the portal is reachable at, e.g.
https://secure.example.org. Setting it (together withPEPPER) is what enables the feature; the link mailed to a recipient is<BASE_URL>/secure/<token>. Nothing here says where the portal is mounted — its own listener and hostname, or one shared with the API and the Web Key Directory, are both supported.- PEPPER
(string) The server-side secret mixed into every content-key derivation. Written by pepsi-setup(1) to
secrets.d/pepsi-secure-link.secretand referenced with@inline-secret@; it is the one managed fragment with two readers, so it is installed mode0640ownedpepsi-httpd:pepsi. Without it the portal is off — there is no mode in which a message is stored in the clear. Losing or changing it makes every stored message unreadable, which is the same property that makes a stolen database worthless.- EXPIRY_DAYS
(integer, optional) How long a stored message lives. Default
7. An integer count of days rather than a duration, becausedurationvalues reject calendar units.- PIN_LENGTH
(integer, optional) Characters in a generated PIN, from a 31-symbol unambiguous alphabet (no
0/O, no1/I/L). Default10, minimum8. Six digits is refused: the PIN is the only secret an attacker holding both the database and the pepper still has to find.- MAX_ATTEMPTS, LOCKOUT
(integer / temporal, optional) Wrong PINs before the token locks (default
10) and the first lockout window (default15 m). The window doubles with each further lockout, capped at 64 times the base.- SESSION_LIFETIME
(temporal, optional) How long the portal session lasts after a correct PIN. Default
30 m.- RATE_LIMIT
(integer, optional) Requests per minute per peer address across every portal route. Default
30;0disables the limiter.- NOTIFY_STAGE
(string) Required. Where the link mail, the PIN mail and read receipts are injected — a stage on the outbound path, since these are messages the deployment originates.
- REPLY_STAGE
(string, optional) Where a reply composed in the portal is injected. Unset disables replying. Point it at the inbound path: a portal reply is written by an external party, so it is injected without
state.local_originand must not be DKIM-signed as one of your domains (pepsi-setup(1) warns if it resolves to a signing stage).- SEND_RECEIPT
(boolean, optional) Tell the sender, once, that the message was read. Default
yes. The receipt reports who and when, never what — there is nothing for it to quote.- PIN_DELIVERY
(``sender`` | ``command`` | ``none``, optional) How the second factor reaches the recipient. Default
sender: the PIN is mailed to the sender, who relays it by telephone or text message.commandrunsPIN_COMMAND.noneputs the PIN in the link itself and is strictly weaker — the mailbox becomes the only factor.- PIN_COMMAND
(path, optional) Required by
PIN_DELIVERY = command. Run with the recipient address as its single argument and the PIN on standard input (a command line is world-readable through/proc). A non-zero exit fails the message rather than leaving a link nobody can open.- MAX_REPLY_SIZE, MAX_REPLY_FILES, MAX_REPLIES
(integer, optional) Caps on a portal reply: total octets (default
25000000, matching[stage-encrypt] MAX_SIZE), files per reply (default10) and replies per stored message (default20). The size cap is enforced while the body streams, so a client that misstatesContent-Lengthgains nothing.- ACCESS_LOG_ROWS
(integer, optional) Access-log rows kept per message. Default
100. The log is deleted with the message.- ORGANIZATION, BRAND_COLOR, LOGO_FILE
(string / string / path, optional) Appearance.
BRAND_COLORmust be#rgbor#rrggbb— it is interpolated into the page’s stylesheet, so arbitrary text is refused rather than escaped.LOGO_FILEis a local PNG, JPEG, GIF or WebP inlined as adata:URI (SVG is refused: it can carry script). A logo URL is deliberately not an option — the portal’sContent-Security-Policyforbids remote content, which is also what stops a CDN learning who read what.
70.2.1.1.14.5. pepsi-stage-secure-link Options¶
A stage with PROGRAM = pepsi-stage-secure-link takes no options of its
own: everything is in [pepsi-secure-link] above. It has no NEXT_STAGE
(it is terminal — the message leaves the queue and its notification is a new
message injected at NOTIFY_STAGE).
70.2.1.1.14.6. pepsi-stage-dkim-sign Options¶
A stage with PROGRAM = pepsi-stage-dkim-sign reads, besides a required
NEXT_STAGE:
- COVER_BODY
(boolean, optional) Whether the signature covers the whole body strictly.
yes(default) emits nol=tag;noadds the body-length tag so trailing additions are tolerated.- HEADER_CANONICALIZATION, BODY_CANONICALIZATION
(``relaxed`` | ``simple``, optional) The two halves of the
c=canonicalization. Both default torelaxedand may be set independently; all four combinations are valid for both DKIM signing and ARC sealing.- SIGNATURE_EXPIRATION_DAYS
(integer, optional) When set to a positive number of days, the signatures carry an expiration (
x=) that many days after signing. Unset (the default) emits no expiration.- SIGNED_HEADERS
(string, optional) Whitespace/comma-separated list of headers the signatures cover (
h=). Must includeFrom. Defaults to the built-in originator/MIME header set.- SIGNING_DOMAIN
(string, optional) Force the signing domain (
d=). When unset, the domain is taken from each message’sFrom:header. A fixedSIGNING_DOMAINis a sending identity, so pepsi-setup(1) provisions its keys and DNS.
The DKIM key material and selectors come from the shared [pepsi] section
(KEY_DIR, DKIM_SELECTOR). The hash is always SHA-256; both an RSA
(rsa-sha256) and an Ed25519 (ed25519-sha256) signature are emitted.
70.2.1.1.14.7. pepsi-stage-relay-to-internet Options¶
A stage with PROGRAM = pepsi-stage-relay-to-internet reads (a NEXT_STAGE
and BOUNCE_STAGE are the general keys above):
- SERVER_NAME
(string, required) Our own host name, announced in
EHLOand written into theReceived:trace header, e.g.mail.example.org.- POSTMASTER
(string, optional) Address that receives a double bounce — a bounce (null sender) that is itself undeliverable. With none, such double bounces are discarded.
- CONNECT_TIMEOUT / COMMAND_TIMEOUT / DATA_TIMEOUT
(duration, optional) Per-connection timeouts for, respectively, establishing the TCP connection, an SMTP command/response, and the
DATAtransfer. Defaults300 s/300 s/600 s.- RETRY_INITIAL / RETRY_MAX_INTERVAL / RETRY_FACTOR
(duration / duration / number, optional) Exponential-backoff schedule between delivery attempts of a transiently-failing message: the first wait, the cap on the wait, and the multiplier applied each attempt. Defaults
300 s/2 h/2.- MAX_LIFETIME
(duration, optional) How long a message may stay in the pipeline before a transient failure becomes permanent (it is then bounced or failed). Default
5 dof wall-clock, but the value must be written withh/m/sunits — the duration parser rejects thedcalendar unit (so write120 h, not5 d).- DELAY_DSN_AFTER
(duration, optional) When set, a one-shot
Action: delayedDSN is sent to the sender once a still-undelivered message has been queued at least this long and the sender requestedNOTIFY=DELAY. Must useh/m/sunits. Unset (the default) disables delay warnings.- MAX_HOP_COUNT
(number, optional) Maximum number of
Received:headers tolerated before the message is treated as a mail loop and permanently failed. Default30.- DNS_SERVERS
(string, optional) Whitespace/comma-separated explicit resolvers (port 53) for the
MXand address lookups. When unset, the systemresolv.confis used.- DNS_TIMEOUT
(duration, optional) Per-query DNS timeout. Default
5 s.- MTA_STS
(boolean, optional) Whether to discover and enforce recipient-domain MTA-STS policies (RFC 8461). Default
yes.- MTA_STS_TIMEOUT
(duration, optional) Timeout for fetching an MTA-STS policy file over HTTPS. Default
10 s.- ADDRESS_FAMILY
(optional) Which IP address families to use when connecting to mail exchangers:
any(default),ipv4(also spelledv4/v4only) oripv6(v6/v6only). The configured value is intersected with what the local host can actually route, so pinning a family the host has no route for leaves nothing to connect to. pepsi-stage-relay-to-smarthost(1) has the same option, additionally settable per MTA entry.- DANE
(optional) DANE/TLSA (RFC 7672) enforcement for the destination MX:
off(noTLSAlookups),warn(the default; validate but, on a mismatch, log and deliver anyway) orstrict(a usable-but-mismatching record, or a failed TLSA lookup, defers delivery). Requires a DNSSEC-validating resolver (Pepsi trusts the response AD bit; see DNS_SERVERS); usableTLSArecords take precedence over MTA-STS.
70.2.1.1.14.8. pepsi-stage-relay-to-smarthost Options¶
A stage with PROGRAM = pepsi-stage-relay-to-smarthost reads the operational
options below; the upstream smarthosts themselves are defined once in the shared
[pepsi-stage-relay-to-smarthost-mta-*] sections (see The Smarthost (MTA)
Sections below). A NEXT_STAGE and BOUNCE_STAGE are the general keys
above.
- SERVER_NAME
(string, required) Our own host name, announced in
EHLO(unless an MTA overrides it withHELO_NAME) and written into theReceived:header.- CONNECT_TIMEOUT / COMMAND_TIMEOUT / DATA_TIMEOUT
(duration, optional) Per-connection timeouts for the TCP connect, an SMTP command/response and the
DATAtransfer. Defaults300 s/300 s/600 s.- RETRY_INITIAL / RETRY_MAX_INTERVAL / RETRY_FACTOR
(duration / duration / number, optional) Exponential-backoff schedule between relay attempts. Defaults
300 s/2 h/2.- MAX_LIFETIME
(duration, optional) How long a message may stay in the pipeline before a transient failure becomes permanent. Default
5 dof wall-clock, but write it withh/m/sunits — the parser rejects thedcalendar unit.- DELAY_DSN_AFTER
(duration, optional) When set, a one-shot
Action: delayedDSN is sent once a still-undelivered message has been queued at least this long and the sender requestedNOTIFY=DELAY. Must useh/m/sunits. Unset disables it.- MAX_HOP_COUNT
(number, optional) Maximum number of
Received:headers before the message is treated as a loop and permanently failed. Default30.- DNS_SERVERS
(string, optional) Whitespace/comma-separated explicit resolvers (port 53) for the DANE
TLSAlookups. When unset, the systemresolv.confis used. Only consulted when a smarthost enablesDANE; the resolver must validate DNSSEC.- DNS_TIMEOUT
(duration, optional) Per-query timeout for the DANE
TLSAlookups. Default5 s.
70.2.1.1.14.9. Bounce Stage Options¶
A [stage-<name>] section with PROGRAM = pepsi-stage-bounce additionally
reads, from its own section:
- SERVER_NAME
(string, required) Host name used in the generated bounce’s
From:andReporting-MTAfields and in itsMessage-IDdomain, e.g.mail.example.org.- POSTMASTER
(string, optional)
From:address of generated bounces (Mail Delivery Subsystem <POSTMASTER>) and the DKIM signing identity — the bounce is signed as this address’s domain, whose keys pepsi-setup(1) provisions. Defaults topostmaster@<SERVER_NAME>.- NEXT_STAGE
(string, required for this stage) The stage the rewritten bounce is advanced to. Because the bounce is left unsigned, this is normally a pepsi-stage-dkim-sign(1) stage (which signs it as the postmaster’s domain), which in turn points at a delivery stage such as pepsi-stage-relay-to-internet(1).
- BOUNCE_MESSAGE
(string, optional) Name of the template that renders the human-readable (
text/plain) part of the bounce. The template file isbounce-<NAME>.<lang>.bodyunder the[pepsi]TEMPLATE_DIR; it is a Mustache template selected by language (only English ships, andenis the fallback). When unset — or when rendering fails — a built-in English notice is used instead, so a bounce is always produced. The bundledbounce-defaulttemplate is installed bymake install.The whole message
stateis passed to the template as the rendering context, augmented with the convenience variablesserver_name,postmaster,bounce_to,failed_recipient,diagnostic,actionand the booleansfailed/delayed/delivered. In particular the delivery stage records structured next-hop detail understate.bounce—remote_mta,smtp_code,enhanced_status,phaseandreply_text— so the template can state precisely why the next MTA refused the message. When astate.bounce.enhanced_status(RFC 3463) is present it is also used for the DSN’s machine-readableStatus:field.
The failed recipient and diagnostic quoted in the bounce are taken from the
state left on the row by the delivery stage that routed the message here
(see BOUNCE_STAGE above); when absent (e.g. a hand-staged message) a generic
notice is produced.
70.2.1.1.14.10. pepsi-stage-discard Options¶
A stage with PROGRAM = pepsi-stage-discard deletes the message (a staging /
test sink). It ignores NEXT_STAGE — a discard is always terminal — and reads:
- DISPOSITION
(optional) The simulated outcome:
success(default) treats the message as delivered;failuretreats it as a permanent delivery failure.- BOUNCE
(boolean, optional) Whether the discard may emit a DSN at all. Default
no.- BOUNCE_STAGE
(string, optional) The DSN-generation stage a reportable discard is routed to (normally a pepsi-stage-bounce(1) stage). Without one, nothing is emitted and the row is just deleted.
The success path also consults the shared [pepsi] ORIGINATE_SUCCESS_DSN flag;
a null-sender message is never bounced.
70.2.1.1.14.11. pepsi-stage-anti-spam Options¶
A stage with PROGRAM = pepsi-stage-anti-spam reads (besides a NEXT_STAGE):
- BOUNCE_STAGE
(string, optional) The stage an unpaid-by-deadline message is routed to — normally a pepsi-stage-bounce(1) stage (a rejection DSN, honouring the sender’s
NOTIFY) or a pepsi-stage-discard(1) stage (silent drop). Without one, an unpaid message is just deleted.- ORDER_CHOICES
(JSON, required) The Taler v1 order
choicesarray, used verbatim: one or moreOrderChoiceobjects describing the accepted payments (for example several amounts in different currencies, or token-family inputs/outputs). See the GNU Taler merchant API.- PAYMENT_DEADLINE
(duration, optional) How long the message is held
pausedawaiting payment before it is rejected; also the order’spay_deadline. Usesh/m/sunits only (calendar units such asdare rejected). Default48 h.- DELAY_DSN_AFTER
(duration, optional) Send the sender a one-shot delayed delivery-status notification (RFC 3461) once a still-unpaid message has been held this long, without releasing the message — it stays paused until PAYMENT_DEADLINE. The DSN is sent only when the sender requested
NOTIFY=DELAYand a BOUNCE_STAGE is wired (it is routed there withkind = delay). Only fires when it lands strictly before PAYMENT_DEADLINE. Usesh/m/sunits only. Unset (the default) disables delay warnings.- SUMMARY
(string, optional) Human-readable order summary shown to the payer. Default
E-mail delivery.- FULFILLMENT_MESSAGE
(string, optional) Message shown to the payer after a successful payment. Default
Your e-mail has been queued for delivery.- BLOCK_RESPONSE_STAGE
(string, optional) The stage at which the payment-request auto-reply is injected — usually the head of a signing/relay chain (for example a pepsi-stage-dkim-sign(1) stage). Must reference an existing stage. When unset, no reply is sent (the message is still paused awaiting payment).
- BLOCK_RESPONSE_FROM
(string, optional) The
From:header of the auto-reply. When unset it defaults to the original recipient (the protected mailbox).- BLOCK_RESPONSE_SUBJECT
(string, optional) The
Subject:of the auto-reply. DefaultPayment required to deliver your e-mail.- PAYMENT_MESSAGE_DEFAULT_LANGUAGE
(string, optional) The fallback language for the auto-reply body, used when the message carries no detected
state.languageor none of the detected languages has apayment-request.<lang>.bodytemplate (under[pepsi]TEMPLATE_DIR). That template must exist (pepsi-setupchecks). Case-insensitive. DefaultEN.
The stage also requires the shared [pepsi-payments] section (the merchant
backend and access token, documented below) and, for the payment webhook to
release paused messages, the [pepsi-httpd] RESUME_AUTHORIZATION_TOKEN
(see pepsi-httpd(1)).
70.2.1.1.14.12. pepsi-stage-auto-pay Options¶
A stage with PROGRAM = pepsi-stage-auto-pay reads:
- MAX_TOTAL
(amount, required) The most this stage will spend, in total, to deliver one original message — a GNU Taler amount
CURRENCY:VALUE(for exampleEUR:5). The cap is cumulative across every payment demand returning for that message (all sharing one Pepsi-Origin nonce, e.g. a mailing-list fan-out) and single-currency: a demand in any other currency is not paid. An account can override this for its own mail via pepsi-stage-edit-settings(1).- NEXT_STAGE
(string, required) Where a message is forwarded when it is not a payment demand, is not provably ours, or is not paid (over budget / settlement failure). A stage with no NEXT_STAGE errors at run time.
- WALLET_MODE
(``local-user`` | ``shared``, default ``shared``) Which account the wallet helper pays from.
local-userdrops to the bounce recipient’s own local login (the original sender) and uses that user’s default wallet — the recipient must resolve to a permitted local account (the LOCAL_DOMAINS/TARGETS/ RECIPIENT_DELIMITER test, as for pepsi-stage-relay-to-maildir(1)).shareddrops to a single dedicated account (WALLET_USER).- WALLET_USER
(string, default ``pepsi-wallets``) The shared account the helper drops to in
WALLET_MODE = shared; its home holds the wallet database(s).- WALLET_SCOPE
(``per-sender`` | ``unified``, default ``per-sender``) In
sharedmode, how the shared account’s wallet databases are segregated: one per original-sender address (per-sender), or a single wallet shared by everyone (unified— e.g. a business where individual wallets are not sensible).- WALLET_CLI
(string, default ``taler-wallet-cli``) The GNU Taler wallet command-line tool the helper runs (a name resolved on
$PATHor an absolute path).- WALLET_PAY_OPTIONS
(string, default none) Extra arguments appended to the wallet’s pay invocation, whitespace-separated. The literal
nonepasses no extra options. (--yesand--choice-index 0are always added by the helper to confirm the payment and to name the contract choice it priced.) Nothing is needed here by default — the wallet’shandle-urialready waits for the payment to reach a terminal state.- HELPER
(string, default ``pepsi-helper-auto-pay``) The privileged wallet helper binary (a name on
$PATHor an absolute path).
The following options enable and configure the wallet self-service role (a locally-submitted message to the control address operates the sender’s own wallet by e-mail; see pepsi-stage-auto-pay(1)):
- RESPONSE_STAGE
(string, optional) The stage at which the self-service reply is injected (so it is DKIM-signed and relayed). Setting it enables the self-service role; leaving it unset means the stage only pays inbound demands.
- CONTROL_LOCAL_PART
(string, default ``pepsi-wallet``) The local part of the wallet control address: a locally-originated message to
<part>@<served-domain>is a wallet-control message.- RESPONSE_FROM
(string, optional) The
From:of the self-service reply. Defaults to<{CONTROL_LOCAL_PART}@{domain}>for the served domain addressed.- RESPONSE_SUBJECT
(string, default ``Pepsi wallet``) The
Subject:of the self-service reply.
A manual withdraw (top-up) names the GNU Taler exchange to withdraw from in
the request (Subject: withdraw CURRENCY:VALUE EXCHANGE-URL); because a wallet
is multi-currency and can withdraw from many exchanges, there is no server-side
exchange option.
The stage also uses the shared [pepsi-origin] section (the proof-of-origin
secret) to verify that a demand is for mail this deployment actually sent; without
it no demand can be verified and none is paid (self-service is unaffected). All
wallet work is done by the setuid-root pepsi-helper-auto-pay(1); the stage
binary is installed SGID pepsi-wallets so its worker may exec it.
70.2.1.1.14.13. pepsi-stage-check-whitelist Options¶
A stage with PROGRAM = pepsi-stage-check-whitelist reads:
- WHITELIST_NAME
(string, required) The
whitelist_namegroup to consult in thepepsi.whitelisttable.- NEXT_STAGE
(string, required) Where the message advances after the check (whether or not it matched). A stage with no NEXT_STAGE errors at run time.
70.2.1.1.14.14. pepsi-stage-auto-whitelist Options¶
A stage with PROGRAM = pepsi-stage-auto-whitelist reads:
- WHITELIST_NAME
(string, required) The
whitelist_namegroup to populate in thepepsi.whitelisttable. It should match the group the inbound pepsi-stage-check-whitelist(1) consults.- DKIM_REQUIRED
(boolean, optional) Value stored in the
dkim_requiredcolumn of every row this stage inserts. Defaultyes.- NEXT_STAGE
(string, required) Where the message advances after the recipients are recorded. A stage with no NEXT_STAGE errors at run time.
70.2.1.1.14.15. pepsi-stage-autocrypt-learn Options¶
A stage with PROGRAM = pepsi-stage-autocrypt-learn reads:
- LEARN_KEYS
(boolean, optional) Store the sender’s own key material found in the message — S/MIME signer certificates,
application/pgp-keysparts and theAutocrypt:header — at theinboundtrust rank, where it can never silently replace a better-sourced key. DefaultYES. Only material claiming the sender’s own address is kept. Turning it off turns the whole stage into a pass-through,LEARN_GOSSIPincluded.- LEARN_GOSSIP
(boolean, optional) Also learn the other recipients’ keys from the
Autocrypt-Gossip:fields inside a message that arrived encrypted (Autocrypt Level 1 §5.3), at thegossiprank — the bottom of the ladder, belowinbound. DefaultYES; it does nothing unlessLEARN_KEYSis also on.This is what makes an encrypted reply-all possible to a correspondent one has never had direct mail from. A field is used only when it was genuinely inside the ciphertext, only when its
addrappears in the message’s ownTo:,Cc:orReply-To:, never for the sender’s own address, and never for an address inLOCAL_DOMAINS— nobody may introduce this host to its own users. A gossiped key can never displace a better-sourced one and never makes a signature count as verified.- LEARN_FROM_SPAM
(boolean, optional) Whether a message the pipeline has already scored as spam (
state.spam = true) may still teach a key. DefaultNO, which is Autocrypt Level 1 §5.3’s “messages SHOULD be ignored … when the MUA believes the message to be spam”.It is an option rather than a fixed rule because the belief is Pepsi’s own: an operator whose spam scoring is deliberately loose may prefer key continuity to the rule. Note that honouring the default requires the stage to be placed after whatever sets
state.spam— see pepsi-stage-autocrypt-learn(1).- LOCAL_DOMAINS, TARGETS, RECIPIENT_DELIMITER
(optional) The shared locality options. Only
LOCAL_DOMAINSis consulted, and only to refuse: a gossip field naming an address this host serves is a key substitution rather than an introduction. Defaults to[pepsi-ingress]ACCEPTED_DOMAINS.- NEXT_STAGE
(string, required) Where the message advances once the stage has learnt whatever it could. The stage never consumes a message, so a missing NEXT_STAGE is refused by pepsi-setup(1) and errors at run time.
70.2.1.1.14.16. pepsi-stage-detect-language Options¶
A stage with PROGRAM = pepsi-stage-detect-language reads:
- NEXT_STAGE
(string, required) Where the message advances after classification (whether or not a language was recorded). A stage with no NEXT_STAGE errors at run time.
- LANGUAGES
(list, optional) The candidate languages, as whitespace/comma-separated ISO 639-1 codes (case-insensitive); at least two distinct codes are required. The token
*stands for every supported language and may be combined with explicit codes (which then come first). Detection only ever returns languages from this set, so it should cover the languages you expect to receive. Defaults to a broad common set (en de fr es it pt nl pl ru tr ar zh ja).Narrow it. Every enabled language adds to the memory each worker process uses, and — more importantly — every language that your traffic never contains still competes for probability. The stage removes non-prose (URLs, addresses, base64 blobs, DNS records) from the body before classifying, which is what stops a single such token from deciding the answer outright; what remains after that is a genuine contest between the candidates, and a language nobody writes to you can still win it on a short message. pepsi-detect-language(1) reclassifies a saved message against any candidate set, so a narrower list can be tried before it is configured.
70.2.1.1.14.17. pepsi-stage-block-language Options¶
A stage with PROGRAM = pepsi-stage-block-language reads:
- NEXT_STAGE
(string, required) Where a message scoring above THRESHOLD advances — and, under
ENFORCEMENT = soft, where a flagged message continues too. A stage with no NEXT_STAGE errors at run time.- BOUNCE_STAGE
(string, required when a message can be bounced) Where a message scoring at or below THRESHOLD is routed so a failure DSN can be generated (typically pepsi-stage-bounce(1)). Consulted only under
ENFORCEMENT = hard; a bounce with no BOUNCE_STAGE errors at run time. Worth setting even undersoft, so that switching tohardis a one-word edit.- ENFORCEMENT
(``hard`` | ``soft``, optional) What happens to a message that fails the policy.
hard(the default, and the behaviour the stage had before this option existed) routes it to BOUNCE_STAGE, so it is not delivered.softdelivers it anyway, marked: SUBJECT_FLAG_LABEL is appended to itsSubject:, aPepsi-Detected-Languagesheader records what was detected, and the message advances to NEXT_STAGE.softmarks exactly the messageshardwould have bounced, which is what makes it a training mode: run it for a while and the flags in the mailbox are a faithful preview of what enforcement would refuse, at no cost to a correspondent whose mail the lists have not been tuned for yet. Being an ordinary stage option it is per-address overridable (pepsi-settings(1)), so one account can stay in training while the rest of the deployment enforces.Note the
Subject:rewrite breaks the sender’s DKIM signature and this server’s ARCAMS(both over-signSubject), exactly as pepsi-stage-vacation(1)’sVACATION_TAGdoes — free on a branch that ends in local delivery, not free on one that relays onward.- SUBJECT_FLAG_LABEL
(string, optional) The marker appended to a flagged message’s
Subject:underENFORCEMENT = soft. Defaults to[!LANG]. Appended only when the subject does not already contain it (case-insensitively), so a reply quoting it back through the stage does not collect a second one. An empty value re-applies the default; there is no way to switch the marker off, since flagging nothing is the same as leaving the stage out of the pipeline.- WHITELIST
(list, optional) Languages whose probability is added to the score, as whitespace/comma-separated ISO 639-1 codes (case-insensitive) plus the optional pseudo-code
none(matching undetected mail). Empty when unset.- BLACKLIST
(list, optional) Languages whose probability is subtracted from the score, same syntax as WHITELIST. Empty when unset. A language may appear on at most one of the two lists.
- THRESHOLD
(float, optional) The score a message must exceed to advance. Defaults to
0.0.
70.2.1.1.14.18. pepsi-stage-vacation Options¶
A stage with PROGRAM = pepsi-stage-vacation answers mail that arrives while the
envelope recipient is away. Every option below is per-address overridable, and
that is how the feature is meant to be used: the INI section is the operator’s
default (no vacation), and each user’s leave dates — and, if they want, their own
message text — come from their pepsi.settings row or from a config_override
scope. See pepsi-stage-vacation(1).
- NEXT_STAGE
(string, required) Where the message continues. It always does: this stage adds a reply, it never consumes a message. pepsi-setup(1) rejects a stage without it.
- RESPONSE_STAGE
(string, required) Where the vacation notice is injected as a new null-sender message, so it is signed and relayed (typically the DKIM-signing stage). pepsi-setup(1) checks it names an existing stage.
- VACATION_RANGES
(list, optional) Comma-separated
YYYY-MM-DD:YYYY-MM-DDspans of days the recipient is away. Both endpoints are inclusive whole days in the server’s time zone; an empty end (2026-08-01:) is an open-ended leave and a bare date is that single day. Empty when unset, which switches the stage off for that address — the default, and the state of every recipient who has never configured a vacation. A range whose end precedes its start is rejected.- DEFAULT_LANGUAGE
(string, optional) The language whose message is used when none of the sender’s detected languages (
state.language) has one. Defaults toen. Written with either separator:pt_brandpt-BRare the same tag.- DEFAULT_MESSAGE_SECTION
(string, optional) The configuration section holding one inline message template per language, as
<LANG> = <template>. Defaults topepsi-vacation-default-message, which ships in${DATADIR}/config.dwith ten languages. A per-addressMESSAGE_<LANG>option of this section takes precedence over it, per language.- EMERGENCY_CONTACT
(address, optional) A single bare address offered to the message template as
{{EMERGENCY_CONTACT}}. Unset by default, in which case a template’s{{#EMERGENCY_CONTACT}}block is skipped. A display name or a list of addresses is rejected.- VACATION_TAG
(string, optional) Appended to the forwarded message’s
Subject:when a notice was sent, so the recipient can see which mail was answered for them. Defaults to[VACATION]; the sentinel valuenonedisables tagging. An empty value does not — the parser reads an empty option as absent, which re-applies the default. Appending is idempotent.- SUPPRESS_DAYS
(integer, optional) How long after answering one correspondent they may be answered again. Defaults to
7(the Sievevacationdefault, RFC 5230 §4.1);0answers every message.- REQUIRE_ADDRESSED_TO
(boolean, optional) Answer only when the recipient appears in
To:orCc:(RFC 3834 §3), so a blind carbon copy or a harvested-list blast draws no reply. Defaults toyes.- SUBJECT_PREFIX
(string, optional) Prepended to the original subject to form the notice’s subject. Defaults to
Auto:(RFC 5230 §4.5). A message with no subject yields the prefix alone, and an already-prefixed subject is not prefixed twice.
70.2.1.1.14.19. pepsi-stage-if Options¶
A stage with PROGRAM = pepsi-stage-if branches on a member of the message
state JSON:
- STATE_PATH
(string, required) The dot-separated path into the message
statewhose value is tested (object keys and integer array indices, e.g.spam,auth.dkim,dsn.rcpt.0.notify).- VALUE
(string, required, non-empty) The value the member at STATE_PATH is compared against, by its natural scalar text form (string,
true/false, decimal number, ornull). An empty option reads as unset.- TRUE_STAGE
(string, required) Where the message advances when the value equals VALUE. pepsi-setup(1) checks it names an existing stage.
- FALSE_STAGE
(string, required) Where the message advances otherwise (the value differs, the path is absent, or it resolves to an array/object). pepsi-setup(1) checks it names an existing stage.
70.2.1.1.14.20. pepsi-stage-milter Options¶
A stage with PROGRAM = pepsi-stage-milter hands the message to an existing
milter daemon (the sendmail/Postfix mail-filter protocol) and routes it by the
filter’s verdict. See pepsi-stage-milter(1), and note in particular that
the filter runs post-queue, so a REJECT costs a bounce where an MTA would
have answered 5xx inside the SMTP session.
- SOCKET
(string, required) Where the milter daemon listens, in sendmail’s
S=grammar (which Postfix also accepts):unix:/path(or its synonymlocal:/path),inet:host:port,inet:port@host,inet6:[addr]:portorinet6:port@addr. A bare path with no scheme is rejected. Pepsi connects to this socket; it never starts, stops or confines the daemon behind it.- MILTER_NAME
(string, optional) Exported to the filter as the
{daemon_name}macro and used in log messages. Defaults to the stage’s own name.- ACCEPT_STAGE
(string, optional) Where
SMFIR_ACCEPTsends the message. Defaults to NEXT_STAGE, which makes accept and continue the same thing; set it to skip past the rest of a filter chain, which is what Postfix’s milter list does natively. pepsi-setup(1) checks it names an existing stage.- REJECT_STAGE
(string, optional) Where
SMFIR_REJECT— and a tempfail that has exhausted MAX_LIFETIME — sends the message, and where individually rejected recipients are fanned out to. Defaults to the section’sBOUNCE_STAGE. Point it at a pepsi-stage-discard(1) to drop rejected mail instead of bouncing it. pepsi-setup(1) checks it names an existing stage.- QUARANTINE_STAGE
(string, optional) Where
SMFIR_QUARANTINEsends the message. Unset means discard: Pepsi has no quarantine store, only a route. A quarantine request decides the routing regardless of the verdict that follows it, because a sendmail milter quarantines and then still returns an accept. pepsi-setup(1) checks it names an existing stage.- ON_FAILURE
(``tempfail``/``accept``/``reject``/``discard``, default ``tempfail``) What to do when the milter cannot be reached, times out, or speaks something unparsable — Postfix’s
milter_default_action, with the same default.tempfailpauses the message for retry, so a filter that is down delays mail rather than losing it or letting it through unfiltered.- ALLOW_ACTIONS
(string, default ``addhdrs chghdrs chgbody``) The
SMFIF_*modification actions offered to the filter, whitespace- or comma-separated:addhdrs,chghdrs,chgbody,addrcpt,delrcpt,addrcpt_par,chgfrom,quarantine; plus the shorthandsallandnone. There is noinshdr: libmilter gates header insertion onSMFIF_ADDHDRStoo.With no sandbox around the filter this mask is the one blast-radius lever available, and the default is where it earns its keep: every content filter works under it, while a filter that wants to rewrite the envelope has to be granted that explicitly. A filter requiring an action that was not offered is reported by name and takes the ON_FAILURE path, rather than having its modifications silently dropped the way Postfix drops them.
SMFIF_SETSYMLISTis deliberately not listed and needs no grant: it authorises no change to the message.- PROTOCOL_VERSION
(integer 2–6, default 6) The highest milter protocol version offered; the filter negotiates down from it. Postfix’s
milter_protocol.- CONNECT_TIMEOUT
(duration, default ``30 s``) Bound on establishing the connection and completing the option negotiation — both halves of “can this filter be reached at all”. Postfix’s
milter_connect_timeout.- COMMAND_TIMEOUT
(duration, default ``30 s``) Bound on the reply to any single command up to end-of-body. Postfix’s
milter_command_timeout.- CONTENT_TIMEOUT
(duration, default ``300 s``) Bound on the end-of-message verdict, where a content filter does its real work. An
SMFIR_PROGRESSfrom the filter restarts the clock. Postfix’smilter_content_timeout.- RETRY_INITIAL / RETRY_MAX_INTERVAL / RETRY_FACTOR / MAX_LIFETIME
(durations / number, optional) The shared retry schedule for the tempfail path, with the same meaning as in the relay stages. Defaults:
300 s,1 h,2,120 h.- MACROS_CONNECT / MACROS_HELO / MACROS_MAIL / MACROS_RCPT / MACROS_DATA / MACROS_EOH / MACROS_EOM
(string, optional) The sendmail macros exported at each protocol phase, whitespace- or comma-separated. The defaults are Postfix’s, so a filter’s own documentation applies unchanged:
j {daemon_name} v {if_name} _,{tls_version} {cipher} {cipher_bits},i {auth_type} {auth_authen} {mail_addr},{rcpt_addr}, andifor the remaining three. A macro whose value this deployment does not know is not sent at all. Set a list to the sentinelnoneto export nothing at that phase — an empty value reads as absent and re-applies the default. A filter that names its own list withSMFIF_SETSYMLISToverrides these.
70.2.1.1.14.21. pepsi-stage-route Options¶
A stage with PROGRAM = pepsi-stage-route chooses a next stage for each
envelope recipient from that recipient’s domain, splitting the message onto
sibling rows when the recipients disagree. It exists for a gateway sitting in
front of another mail system (see the manual’s “Microsoft Exchange as a gateway”
chapter): the domains behind such a gateway are exactly the domains whose public
MX is the gateway, so routing one of them to a direct-to-MX relay is a mail
loop rather than a delivery failure.
Lookup order per recipient is ROUTES, then the managed-domain set, then NEXT_STAGE.
- ROUTES
(string, optional) Whitespace- or comma-separated
<domain-pattern>=<stage>pairs, consulted first and in the order written. Patterns are matched case-insensitively and may contain*, which matches any run of characters; they are globs, not regular expressions, and are anchored at both ends, so*.example.orgmatchesmail.example.orgbut neitherexample.orgnormail.example.org.evil.test. A value that looks like a regular expression is rejected rather than silently matching nothing. Explicit rules are consulted before the managed set so one subdomain can be carved out without removing its parent from MANAGED_DOMAINS.- MANAGED_DOMAINS
(string, optional) The domains behind this gateway, whitespace- or comma-separated,
*globs permitted. Defaults to[pepsi-ingress] ACCEPTED_DOMAINS, so the set cannot drift from what ingress actually accepts. At least one domain must resolve, from either source.- MANAGED_STAGE
(string, optional) Where a recipient at a managed domain goes — in a gateway deployment, the stage that relays to the system behind it. Without it, managed recipients take NEXT_STAGE, which pepsi-setup(1) then has to prove is not a loop.
- NEXT_STAGE
(string, required) Where every other recipient goes. Required: the stage never delivers, bounces or drops a message, so every recipient must have somewhere to be.
pepsi-setup(1) checks every target names an existing stage, and refuses
a configuration in which a message for a managed domain could reach
pepsi-stage-relay-to-internet(1) — walking forward through NEXT_STAGE
and any further routing stages, but deliberately not through BOUNCE_STAGE (a
bounce is a new message about a delivery that already failed, and legitimately
reaches a relay).
70.2.1.1.14.22. pepsi-stage-aliases Options¶
A stage with PROGRAM = pepsi-stage-aliases expands the envelope recipients of
a message through an alias mapping file before forwarding it:
- ALIASES
(path, required) The alias map file, parsed like a Postfix virtual(5) table: each line maps a key (a full address, an
@domaincatch-all, or a*wildcard such assales-*@example.org) to one or more comma/whitespace-separated target addresses;#comments and blank lines are ignored. Recipients matching a key are replaced by its targets (transitively, with a loop guard) and the result is de-duplicated; on competing wildcards the most specific match wins. The file is re-read only when its modification time changes; a missing file is treated as an empty map. pepsi-setup(1) syntax-checks it at install time when present. See pepsi-stage-aliases(1).- NEXT_STAGE
(string, required) Where the (expanded) message advances. pepsi-setup(1) checks it names an existing stage.
70.2.1.1.14.23. pepsi-stage-edit-settings Options¶
A stage with PROGRAM = pepsi-stage-edit-settings reads:
- EDITABLE_STAGES
(list, required) Whitespace/comma-separated stage names whose options a control e-mail may change. A body addressing any stage not listed here is refused.
- RESPONSE_STAGE
(string, required) Stage at which the reply is injected (typically the start of the outbound signing/relay path), so it is DKIM-signed and delivered. Must resolve to an existing stage.
- NEXT_STAGE
(string, required) Where a non-control message is advanced. A stage with no NEXT_STAGE errors at run time on the first ordinary message.
- SUBJECT
(string, optional) The exact subject that marks a control message, and the subject of the reply. Default
Pepsi.- CONTROL_LOCAL_PART
(string, optional) Local part of the control address. Default
pepsi.- RESPONSE_FROM
(string, optional)
From:header of the reply. Default<CONTROL_LOCAL_PART@domain>for the controlled domain the message was addressed to.
70.2.1.1.14.24. pepsi-stage-vks-confirm Options¶
A stage with PROGRAM = pepsi-stage-vks-confirm follows the confirmation link
in a key server’s address-verification mail, so an uploaded key becomes findable
by address. It sits on the inbound path and advances everything it does not
act on — nearly all mail — untouched. See
pepsi-stage-vks-confirm(1) for the full list of conditions it checks before
fetching anything.
- NEXT_STAGE
(string, required) Where every message this stage does not consume goes. Required rather than optional: a stage on the inbound path that silently dropped what it did not recognise would lose ordinary mail. pepsi-setup(1) checks it names an existing stage.
- VKS_HOST
(string, optional) The one host a verification mail may come from and a link may point at. Defaults to the host of
[pepsi-keys] VKS_SERVER. Matched exactly: a subdomain of a key server is not the key server.- MAX_LINKS
(integer, optional) How many links one message may cost. Default
3. A genuine verification mail carries one; the bound is what stops a message that got past every other check from becoming an arbitrary number of requests.- DISCARD_CONFIRMED
(boolean, optional) Whether a confirmation mail the stage acted on is deleted rather than delivered. Default
YES: it is machine mail about an action Pepsi took, addressed to a mailbox whose owner did not ask for it, and it has already been acted on by the time it would arrive.- REQUIRE_AUTHENTICATION
(boolean, optional) Whether SPF or DMARC must have passed, not merely the envelope sender matching VKS_HOST. Default
YES. Note that a DKIM pass deliberately does not satisfy this: it says a signature verified, not whose.
There is no BOUNCE_STAGE: the stage never fails a message.
70.2.1.1.15. The [pepsi-dispatch] Section¶
Options for pepsi-dispatch(1). Per-stage worker-pool sizing
(PARALLELISM, MAX_MESSAGES, QUEUE_LIMIT) lives in each
[stage-<name>] section above, not here.
- CONFIG_FILE
(path, optional) The configuration file passed to spawned workers via
-c. Set it to this file’s path whenever the dispatcher is started with an explicit-c(the loaded path cannot be recovered otherwise); if unset, workers use the default configuration search.- POLL_INTERVAL
(duration, optional) Safety-net heartbeat: the longest the dispatcher sleeps when no sooner
pausedretry, worker reap or spawn-cooldown is due, at which point it re-scans for work and requeues any duepausedrows. Default 30 s.- MAX_RUNTIME
(duration, optional) Maximum real time a worker may spend on a single message. A worker that exceeds it is killed (and replaced) and the message set to
timeout. Default 300 s.- WORKER_IDLE_TIMEOUT
(duration, optional) How long an idle worker process is kept before it is stopped. Total concurrency is set per stage (PARALLELISM), not globally. Default 5 s.
- STATS_INTERVAL
(duration, optional) How often the in-memory pipeline statistics (exported by pepsi-httpd(1) at
/metrics) are flushed to the database, in a single transaction. Also flushed on shutdown; an idle dispatcher writes nothing. Default 60 s.- DB_POOL_SIZE
(number, optional) Maximum number of PostgreSQL connections in the dispatcher’s database pool. Unlike a single-connection stage worker, the dispatcher processes many workers’ results (advancing/failing their messages) concurrently with its own coordinator queries, so it keeps a small bounded pool. The per-statement work is brief, so a handful of connections absorbs heavy worker churn. Defaults to
8; raise it only if the coordinator is connection-starved, and keep the sum of every component’s pool below PostgreSQL’smax_connections.
70.2.1.1.16. The [pepsi-failure-bouncer] Section¶
Options for pepsi-failure-bouncer(1), which moves failed/timeout
messages to a bounce stage so the sender is notified. Read only by that tool.
- BOUNCE_STAGE
(string, required when the tool is used) The
[stage-<name>]label a stuck message is moved to (and reset topending). Normally the same bounce stage the pipeline already routes permanent failures to (e.g.bounce). A message already at this stage is left untouched, so a bounce that fails at the bounce stage does not loop. pepsi-setup(1) checks the named stage exists.
70.2.1.1.17. The [pepsi-sendmail] Section¶
Options for pepsi-sendmail(1), the Sendmail-compatible local submission
client the Debian package installs as /usr/sbin/sendmail. Every option has a
default, so a site needs no section at all.
The client is unprivileged and asserts no identity: it hands the message to a UNIX-domain ingress listener, which derives the sender’s account from the connecting process’s kernel-reported credentials (AUTH_PEERCRED, above).
- SOCKET
(path, optional) The
pepsi-ingressUNIX-domain submission socket. Default/run/pepsi/submission.sock.Despite living in the client’s section, this option is read by both ends:
pepsi-ingressserves this socket unconditionally and synthesises its listener from this path, so there is no[pepsi-ingress-listener-*]section for it and no second declaration that could name a different path. (There used to be, and pepsi-setup(1) used to cross-check the two; a structure that cannot disagree is better than a check that can pass while the deployment is broken.)On systemd the descriptor is inherited when
pepsi-ingress.socketalready binds this path — matched by the address the descriptor is bound to, not by an FD_INDEX — so a change here needs a matchingListenStream=in the unit. Otherwise the server binds the path itself, and refuses to start if it cannot.The special value
nonedisables local submission entirely: no socket is served and pepsi-sendmail(1) refuses to run, so local programs must authenticate on 587/465. pepsi-setup(1) reports this once.- EHLO_NAME
(string, optional) Name announced in
EHLO. Defaults to[pepsi-ingress] HOSTNAME. Cosmetic: the server authenticates the peer by its credentials, not by what it claims here.
70.2.1.1.18. The [pepsi-whitelist] Section¶
Options for pepsi-whitelist(1), the tool that manages the sender whitelist
and can seed one from a user’s mailbox (import). Read only by that tool; every
option is optional, so a site that only uses add/list/remove needs no
section at all.
- LOCAL_DOMAINS
(string, optional) Domains whose senders count as the user’s own mail when a mailbox is scanned: a message whose
From:/Sender:is at one of them is one the user sent, so its recipients become whitelist entries. Comma- or space-separated. Defaults to[pepsi-ingress] ACCEPTED_DOMAINS, and is the same option pepsi-stage-relay-to-maildir(1) and pepsi-stage-dot-forward(1) use.- TARGETS, RECIPIENT_DELIMITER
(string, optional) As for the delivery stages.
TARGETSbounds which local accountsimport --all-usersseeds (default: the/etc/login.defsUID_MIN..``UID_MAX`` range);RECIPIENT_DELIMITERstrips a sub-address before an address is compared.- HOSTERS_FILE
(path, optional) File listing public e-mail hosters, one domain per line (
#comments; a leading*.covers a subdomain tree). Domains in it are never proposed as an*@domainwildcard byimport --auto-wildcard, however many correspondents the user has there — “everyone atgmail.com” is not a set of people the user knows, and whitelisting it would hand every spammer with a free account a bypass of the anti-spam gate. Their individual addresses are still imported. Defaults to${DATADIR}/hosters.txt, whichmake installwrites; point this at your own copy to extend the list, since the installed one is overwritten on upgrade.- WILDCARD_THRESHOLD
(number, optional) How many distinct addresses at one domain make
import --auto-wildcardpropose a single*@domainentry for it. Default10.- MAX_ENTRIES
(number, optional) Most rows one
importmay add. Default5000. Every row of a whitelist is matched against theFrom:of every inbound message that consults it, so this is a bound on a permanent per-message cost, not just on table size.- SCAN_HELPER
(string, optional) The mailbox-scan helper binary, located on
$PATH. Defaultpepsi-helper-mailbox-scan(see pepsi-helper-mailbox-scan(1)).- MAILBOX
(path, optional) Mailbox to scan when the user names none. Defaults to the user’s
~/Maildirif it exists, else/var/mail/<login>.
70.2.1.1.19. The [pepsi-httpd] Section¶
Options for pepsi-httpd(1). The server also reads the served domains
(ACCEPTED_DOMAINS), the MTA-STS mx host (the ingress HOSTNAME) and the
[pepsi] MTA_STS_* options.
- MAX_CONNECTIONS
(number, optional) Maximum number of HTTP connections served concurrently. Default 256.
- DB_POOL_SIZE
(number, optional) Maximum number of PostgreSQL connections in the HTTP server’s database pool. The server handles requests concurrently, so unlike the single-connection stage workers it can use more than one. Defaults to
1; raise it only if the endpoints are bottlenecked on the single connection, and keep the sum of every component’s pool below PostgreSQL’smax_connections.- RESUME_AUTHORIZATION_TOKEN
(string, optional) Bearer token guarding
POST /resume(compared in constant time); the endpoint is disabled (404) when unset. Use the Talersecret-token:form so the same value can be reused as the merchantpepsi-resumewebhook’sAuthorizationheader (see pepsi-stage-anti-spam(1) and the[pepsi-payments]section).- ADDIN
(boolean, optional) Whether the Outlook add-in routes (
GET /addin/manifest.xmlandGET /addin/taskpane.html) answer. Defaultno: a deployment that is not fronting Exchange has no use for them, and a manifest naming a gateway nobody configured is a support question waiting to happen. See the manual’s “Microsoft Exchange as a gateway” chapter.- ADDIN_URL
(string, optional) The public
https://origin substituted into the add-in assets. Unset (the default) derives it from each request’sHostheader, which is what works both whenpepsi-httpdterminates TLS itself and when it sits behind a reverse proxy — in the latter case it sees plain HTTP over a UNIX socket and cannot tell the public scheme from the connection. Set it when theHostreaching Pepsi does not name the gateway. The scheme is alwayshttps: Outlook refuses to load add-in resources over plain HTTP.
Each [pepsi-httpd-listener-<name>] section binds one socket, with the same
SERVE (tcp/unix/systemd) and transport options as a
[pepsi-ingress-listener-<name>] section, except a tcp listener defaults to
PORT 443 and MODE is plain or tls (no STARTTLS). A tls
listener may set TLS_CERT/TLS_KEY as a fallback certificate. It
additionally accepts:
- ADMIN =
yes|no Whether the administrative surface — the
/api/v1API and the/uiadministration console — is served on this listener. Optional; defaults tono. On a listener without it those routes answer a plain404— byte-for-byte what any unknown path gets — so publishing the public listener does not publish administration.pepsi-httpd(1) refuses to serve them on a flagged listener that would carry them in cleartext off this host: plaintext
tcpis accepted only on a loopback address, whiletlslisteners andunixsockets always qualify. A flagged listener that fails the test logs a warning at start-up and serves the public endpoints only, and pepsi-setup(1) reports the same at validation time.The shipped configuration flags one
unixlistener, which is the shape that needs no credentials at all: a connecting process is identified bySO_PEERCRED. See[pepsi-admin]below.
Each [pepsi-httpd-cert-<name>] section registers one certificate for SNI
selection: SNI (one or more host names), TLS_CERT and TLS_KEY.
70.2.1.1.20. The [pepsi-admin] Section¶
Options for the administrative API served by pepsi-httpd(1) on a listener
flagged ADMIN = yes. Every option has a default, so the section may be
omitted entirely. The authentication model, the scope table and the endpoint
reference are in the manual’s “The administrative API” chapter.
The same options govern the /ui administration console, which is a client of
that API and introduces no configuration of its own: it shares the session
timeouts, the rate limits, the body cap, the page size and the
configuration-write connection. See the manual’s “The administration console”
chapter.
- ADMIN_GROUP
(string, optional) UNIX group whose members are local administrators over a UNIX-socket listener, identified by
SO_PEERCRED.rootalways is, group or no group. Defaultpepsi-admin.- SESSION_IDLE
(duration, optional) Idle timeout of a browser session, pushed forward on every request. Default
30 m.- SESSION_LIFETIME
(duration, optional) Hard lifetime of a browser session, never extended. Default
12 h. There is no “remember me”: this console can revoke a key and read who corresponds with whom, and local administrators — who log in most often — authenticate bySO_PEERCREDand have no session to expire.- EVENT_RETENTION_DAYS
(number, optional) How long audit records are kept, in days. Default
90. pepsi-httpd(1) prunes both logs a few times a day; it is the one component holdingDELETEon them that does not process mail.- MAIL_LOG_RETENTION_DAYS
(number, optional) How long mail-log records are kept, in days, when
[pepsi] MAIL_LOGis on. Default30.Both retentions are integers of days rather than durations because
Section::durationparses throughjiff::SignedDuration, which accepts onlyh/m/sand below.- RATE_LIMIT
(number, optional) Requests per minute per source address (a UNIX-socket listener shares one key for the whole host).
0disables the limit. Default120.- LOGIN_RATE_LIMIT
(number, optional) Login attempts per minute, counted per account and per source address — many passwords against one account and one password against many accounts are different attacks.
0disables the limit. Default10.- MAX_BODY
(number, optional) Largest accepted request body, in bytes. Default
1048576.- PAGE_LIMIT
(number, optional) Default page size of a listing endpoint. A caller may ask for up to 1000; every listing honours
limitandoffsetin its SQL and reports an exacttotalbeside the page, and nothing returns an unbounded result set. Default100.- CONFIG_DB
(PostgreSQL connection string, optional) Connection used for configuration writes. Unset by default, in which case
PUT/DELETEon/api/v1/configanswer503with the reason and every other endpoint is unaffected.Writing
pepsi.config_overridebelongs to thepepsi-configdatabase role, which pepsi-setup(1) grants and explicitly revokes from every service account: a component that processes mail must not be able to rewrite the pipeline it runs in. pepsi-httpd is such an account, and peer authentication keys off the effective uid, so its own connection cannot be that role. Point this at a connection that authenticates aspepsi-config— a password in asecrets.dfragment readable only by thepepsi-httpdaccount, or apg_identmap. The server checks withSELECT current_userat start-up and refuses anything else, because a boundary the code merely believes in is not a boundary.The online setup enqueues through the same connection and for the same reason: only
pepsi-configmayINSERTintopepsi.setup_task, and the privileged applier refuses any row that says otherwise. WithoutCONFIG_DBthe setup endpoints answer503too — the server can serve the questions and watch a task, and cannot ask for one.- APPLY_SOCKET
(path, optional) Where to ring for the privileged setup applier. Default
/run/pepsi/setup-apply.sock.After enqueuing a setup task, pepsi-httpd connects to this socket and closes it. That connection is the entire message: nothing is written to it and nothing is ever read from it, so it cannot become a command channel. What it does is let systemd’s socket activation start
pepsi-setup apply, which takes its work from the database — where every row is validated and audited — and exits again when there is none.A socket rather than a signal or a spawn because pepsi-httpd runs unprivileged and has no way to start a root process, and should not acquire one. This is the one mechanism that lets it cause a privileged process to run without being able to influence what that process does. A failure to connect is not an error: the task is durably queued either way, and
pepsi-setup apply --oncefrom cron or a console picks it up.This path is also what
APPLIER = autoprobes; see below.- APPLIER
(
auto|yes|no, optional) Whether a privileged setup change can happen from this server at all. Defaultauto.The applier’s systemd units are packaged separately (Debian pepsi-httpd-admin, or
make install INSTALL_ADMIN_UNITS=nofrom source), so that a deployment which is administered from a terminal can remove the one mechanism by which an HTTP request reaches/etc/pepsi. With them gone nothing drainspepsi.setup_taskand an insert into it is inert — so the console must not go on offering forms whose submissions would silently never be applied.autoProbe two things: that
pepsi-setup-apply.socketexists as a unit file where systemd looks for one (installed), and thatAPPLY_SOCKETexists, is a socket and is writable by this account (armed — connecting to a UNIX socket needs write permission). The socket is never connected to: connecting is the doorbell ring, which would start a root process on every page render. The probe fails closed: any uncertainty, including a path that cannot be examined and a socket node left behind by a removed package, reads as “no applier”.yesAssume the applier is reachable and skip the probe. For a deployment that drains the queue some other way —
pepsi-setup apply --oncefrom cron, a hand-written unit, a host without systemd — where the probe would report a capability that does exist. It is a promise the operator makes; nothing verifies it.noRefuse every privileged setup request whatever is installed. A way to make the console read-only without changing the package set.
When there is no applier, pepsi-httpd still serves the setup interview, the staged answers and the task history — read-only, with the controls disabled — and answers the mutating endpoints
503setup_applier_unavailable. See pepsi-httpd(1).
70.2.1.1.21. The Egress Identity Option: PUBLIC_IP¶
- PUBLIC_IP = ADDR [ADDR …]
Whitespace- or comma-separated list of public IPv4 and/or IPv6 addresses that send mail as your domains. Each becomes an
ip4:orip6:mechanism in thev=spf1record pepsi-setup(1) suggests. No stage reads this option at run time — it is purely an input to the generated SPF record.PUBLIC_IPis a per-stage option set in a relay stage’s[stage-<name>]section, and pepsi-setup(1) collects it from every stage whosePROGRAMis pepsi-stage-relay-to-internet(1) or pepsi-stage-relay-to-smarthost(1) — detected byPROGRAM, never by the section name — and unions the results. A deployment may therefore have several relay stages, each carrying its ownPUBLIC_IP:On a pepsi-stage-relay-to-internet stage it is this host’s own public sending IP(s): the host delivers directly to recipients’ MX from them.
On a pepsi-stage-relay-to-smarthost stage it is the smarthost’s egress IP(s): mail leaves the internet from the smarthost, so SPF must authorise it (many providers instead publish an
include:you add to the record by hand).
If no relay stage sets
PUBLIC_IPthe generated record is a barev=spf1 -all— which tells receivers no host may send for your domains and makes your own outbound mail fail SPF — and pepsi-setup(1) logs a warning. A receive-only deployment (no relay stage) has noPUBLIC_IPand is not warned.Each address must be a globally-routable public IP — the address receivers see mail arrive from. pepsi-setup(1) warns (at validation and in the DNS output) if a
PUBLIC_IPis a non-public address (loopback, RFC 1918 / unique-local, link-local, or CGNAT): behind NAT, use the host’s public egress IP, not its LAN address, or outbound mail over that address family will fail SPF.Reverse DNS (PTR). Each pepsi-stage-relay-to-internet
PUBLIC_IP— this host’s own egress IP — must also have a forward-confirmed reverse DNS record that names the stage’sSERVER_NAME: aPTRfor the address whose host name in turn resolves (viaA/AAAA) back to the same address, and which is the very name the stage announces inEHLO. Many receiving mail servers treat a missing, non-forward-confirmed, or generic/dynamic-lookingPTRas a spam signal and reject or junk the mail, and a large family of them additionally compares theEHLOname against thePTRand refuses the session when the two name different hosts (HELO host does not match rDNS). Being inside yourACCEPTED_DOMAINSdoes not satisfy that test — a host answering to several names can announce one and reverse-resolve to another, both of them its own, and still be refused. pepsi-setup(1) warns for everyPUBLIC_IPwhose reverse DNS is not correct in this sense, including that mismatch, and prints the remediation (see pepsi-setup(1)). Reverse DNS for aPUBLIC_IPlives in the IP owner’s zone (in-addr.arpa/ip6.arpa) — usually your ISP or hosting provider — so unlike the forward records it is often not something you can publish yourself; where it cannot be changed, setSERVER_NAME(and[pepsi-ingress]HOSTNAME, the MX record and the TLS certificate) to the name thePTRalready gives. A pepsi-stage-relay-to-smarthostPUBLIC_IPnames the smarthost’s egress IPs, whose reverse DNS the smarthost operator controls, so those are not checked.The unioned record is published identically for every served domain. That is correct but broad; in special setups where different domains send from different hosts, a hand-written per-domain SPF record (listing only that domain’s own sending IPs) can be tighter and still correct. pepsi-setup(1) does not compute that grouping automatically and notes this in its DNS output.
70.2.1.1.22. The [pepsi-stage-relay-to-smarthost] MTA Sections¶
70.2.1.1.22.1. The Smarthost (MTA) Sections¶
Each [pepsi-stage-relay-to-smarthost-mta-<name>] section defines one upstream
smarthost; the suffix <name> is a free-form label used in logs. These
sections are shared by every smarthost-relay stage. A recipient domain is routed
to the MTA whose DOMAINS lists it, or to the single CATCH_ALL MTA.
- HOST
(string, required) Host name or address of the smarthost to connect to.
- PORT
(number, required) TCP port to connect on (e.g.
587or465).- MODE
(optional) Transport security:
starttls(default),tls(implicit TLS) orplain(cleartext).- TLS_VERIFY
(boolean, optional) Whether the smarthost’s TLS certificate is verified against the system trust store. Default
yes;noaccepts any certificate (insecure, for testing only).- TLS_CA
(path, optional) An extra CA certificate (PEM) to trust in addition to the system store, e.g. for a private smarthost CA.
- TLS_CLIENT_CERT / TLS_CLIENT_KEY
(path, optional; both-or-neither) A client certificate (PEM chain) and its private key to present to the smarthost during the TLS handshake (mutual TLS). Loaded fresh on every connection. Requires an encrypting
MODE(tlsorstarttls) and applies to PKIX,TLS_VERIFY = noand DANE alike. The key is secret: keep it readable only by thepepsi-tokengroup the relay stage runs SGID under (on Debian, place cert + key in the SGID/var/pepsi/tlsdirectory). SetAUTH = externalto also authenticate the SMTP session with the certificate via SASLEXTERNAL.- DANE
(optional) DANE/TLSA (RFC 7672) enforcement towards this smarthost:
off,warn(the default) orstrict. When the smarthost publishes DNSSEC-validatedTLSArecords at_<PORT>._tcp.<HOST>, the certificate is authenticated against them, taking precedence overTLS_VERIFY(PKIX). Inwarna mismatch is logged and delivery proceeds; instricta usable-but-mismatching record, or a failedTLSAlookup, defers the message. Ignored whenMODE = plain. Requires a DNSSEC-validating resolver reached over a trusted path (Pepsi trusts the response AD bit); see the stage’sDNS_SERVERS.- ADDRESS_FAMILY
(optional) Which IP address families may be used to reach this smarthost:
any,ipv4(also spelledv4/v4only) oripv6(v6/v6only).Precedence is: this entry, then the owning
[stage-*]section, thenany. The per-entry level is what makes the option useful: one misbehaving destination can be pinned without forcing every other destination — including one that publishes onlyAAAA— onto the same family.The motivating case is Microsoft Exchange Online, which rejects mail from a sending IPv6 address with no
PTRrecord (550 5.7.1 ... S820). A dual-stacked host prefers IPv6, so a machine with correct IPv4 reverse DNS and undelegated IPv6 reverse DNS delivers fine everywhere except there.With
any(the default, and the behaviour of every release before this option existed) the host name is handed to the system resolver as before. Any other value makes Pepsi resolve the host itself and try each address of the chosen family in turn. A destination that publishes no address of the chosen family is a transient failure, not a bounce: that is an operator error worth retrying past, not one worth destroying queued mail over.- AUTH
(optional) SMTP authentication mechanism:
none(default),plain,login,cram-md5,digest-md5,scram-sha-1,scram-sha-256,scram-sha-1-plus,scram-sha-256-plus,auto,external,oauth,ntlmorgssapi.plain/login/cram-md5/digest-md5/thescram-*family/autoall requireUSERNAMEandPASSWORD;oauthrequiresUSERNAMEandTOKEN_FILE;external(SASLEXTERNAL, RFC 4422) requiresTLS_CLIENT_CERT/TLS_CLIENT_KEYand authenticates by that certificate (no password sent), with an optionalUSERNAMEas the SASL authorization identity.cram-md5(RFC 2195) andscram-sha-1/scram-sha-256(RFC 5802 / RFC 7677) are challenge/response: the password is never sent, and SCRAM additionally authenticates the server to Pepsi (a bad server signature is a permanent failure). The-plusSCRAM variants addtls-server-end-pointchannel binding (RFC 5929).autopicks the strongest mechanism the smarthost advertises (SCRAM-SHA-256-PLUS>SCRAM-SHA-256>SCRAM-SHA-1-PLUS>SCRAM-SHA-1>CRAM-MD5>LOGIN>PLAIN); it never selects the deprecateddigest-md5,ntlmorgssapi.digest-md5(RFC 2831) is also a challenge/response with server authentication (rspauth) but is deprecated (RFC 6331 moved it to Historic) and provided only for legacy smarthosts; selecting it logs a warning from pepsi-setup(1) and at runtime.ntlmis the de-facto Microsoft mechanism (no RFC; NTLMv2 only, withtls-server-end-pointEPA channel binding) requiringUSERNAME/PASSWORD/NT_DOMAIN; it is weak and does not authenticate the server, so pepsi-setup(1) warns.gssapiis SASLGSSAPI/Kerberos (RFC 4752); it takes no password, obtaining a service ticket forSERVICE_NAMEfrom a credential cache (KRB5CCNAME). All are offered only over an encrypted transport (MODE=tls/starttls);ntlmandgssapirejectMODE = plainat configuration time.- USERNAME / PASSWORD
(string, required when AUTH is plain/login/cram-md5/digest-md5/scram-*/auto/ntlm) The SMTP-AUTH credentials. Keep
PASSWORDin a mode-restricted file merged with@inline-secret@. For thescram-*mechanisms both are normalised with SASLprep (RFC 4013).USERNAMEis also the optional authorization identity forAUTH = externalandAUTH = gssapi.- NT_DOMAIN / NT_WORKSTATION
(string; NT_DOMAIN required when AUTH = ntlm) The Windows domain of the NTLM account, and an optional cosmetic client workstation name.
- SERVICE_NAME
(string, optional; AUTH = gssapi) The Kerberos host-based service principal of the smarthost, in
service@hostform. Defaults tosmtp@<HOST>.- KRB5CCNAME
(string, optional; AUTH = gssapi) The Kerberos credential cache to read for this smarthost (e.g.
FILE:/var/pepsi/krb5/smarthost.cc), overriding the ambientKRB5CCNAME. Pepsi only reads the cache; an external keytab +k5start/cronjob must keep the ticket fresh, and the cache must be readable by the relay worker (the SGIDpepsi-tokengroup; see/var/pepsi/krb5). A missing/expired ticket defers the message (transient failure) rather than bouncing it.- TOKEN_FILE
(path, required when AUTH = oauth) File holding the current OAuth 2.0 access token (whitespace-trimmed), re-read on every delivery. The SASL mechanism is chosen from the smarthost’s
EHLO(OAUTHBEARERpreferred, elseXOAUTH2). The relay stage only reads this file; keep it current with pepsi-helper-token-refresh(1) (see the[pepsi-helper-token-refresh-*]sections) or any equivalent external job. A missing/expired/rejected token defers the message (transient failure) rather than bouncing it.- HELO_NAME
(string, optional) Name announced in
EHLOto this smarthost. Defaults to the stage’sSERVER_NAME.- DOMAINS
(string) Whitespace/comma-separated recipient domains routed to this MTA (case-insensitive). Required unless
CATCH_ALL = yes; a domain may be routed by at most one MTA.- CATCH_ALL
(boolean, optional) Whether this MTA receives every domain not matched by a
DOMAINSentry. Defaultno; at most one MTA may set it. An MTA must listDOMAINSor setCATCH_ALL = yes.
70.2.1.1.23. The [pepsi-helper-token-refresh] Sections¶
These configure pepsi-helper-token-refresh(1), the optional service that
keeps the OAuth access tokens of AUTH = oauth smarthosts current. The set of
tokens to maintain is derived from the smarthost MTA sections: for each
[pepsi-stage-relay-to-smarthost-mta-<name>] with AUTH = oauth the service
looks for a matching [pepsi-helper-token-refresh-<name>] section. The service
is not part of pepsi.target; enable it explicitly.
Because the per-target sections hold OAuth client secrets, keep them in a file
readable only by the pepsi-helper-token-refresh user and include each with the
@inline-secret@ directive (which a reader that cannot open the file — the
relay stage, run as pepsi — silently skips, so the same pepsi.conf parses
for both). See pepsi-helper-token-refresh(1) and the Extending the
Pipeline manual chapter.
The optional base section [pepsi-helper-token-refresh] holds service-wide,
non-secret options:
- STATE_DIR
(path, optional) Private directory (mode
0700) for per-target rotated refresh tokens. Default/var/pepsi/token-refresh.
Each [pepsi-helper-token-refresh-<name>] section (<name> matching an OAuth
smarthost MTA) holds:
- TOKEN_ENDPOINT
(string, required) The provider’s OAuth 2.0 token endpoint URL (HTTPS).
- GRANT
(optional)
refresh_token(the default; e.g. Google) orclient_credentials(e.g. Microsoft 365 app-only).- CLIENT_ID / CLIENT_SECRET
(string, required) The OAuth client credentials, sent in the request body.
- REFRESH_TOKEN
(string, required for GRANT = refresh_token) The long-lived refresh token. A rotated value returned by the endpoint is persisted under
STATE_DIRand then takes precedence.- SCOPE
(string, required for GRANT = client_credentials; optional otherwise) The requested scope.
- REFRESH_MARGIN
(duration, optional) How long before a token’s stated expiry to refresh it. Default
5 m. Usesh/m/sunits only.
70.2.1.1.24. The [pepsi-payments] Section¶
This optional section configures the GNU Taler payment integration. When it is
absent (or MERCHANT_BACKEND_URL is unset) pepsi-setup(1) performs no
merchant checks and no webhook is provisioned. The same section is read at
run time by pepsi-stage-anti-spam(1) to create per-message payment orders.
- MERCHANT_BACKEND_URL = URL
Base URL of the Taler merchant backend, optionally including the instance (
…/instances/$ID). pepsi-setup(1) requestsGET /configat the backend root (any trailing/instances/$IDis stripped) to confirm it is ataler-merchantbackend, andGET /private/orders?limit=1at this URL to confirm the access token. Required to enable the integration.- MERCHANT_ACCESS_TOKEN = TOKEN
Access token for the merchant backend, in the Taler
secret-token:…form, sent as anAuthorization: Bearercredential. It needs theorders-readandwebhooks-read/webhooks-writepermissions. Required whenMERCHANT_BACKEND_URLis set.
The provisioned pepsi-resume webhook fires on the pay event and
POSTs {"message_id":"{{order_id}}"} to
https://<host>/resume (the shortest [pepsi-httpd-cert-*] SNI host),
authenticated with [pepsi-httpd] RESUME_AUTHORIZATION_TOKEN. The merchant
order_id must therefore equal the message’s external token.
70.2.1.1.25. The [pepsi-vacation-default-message] Section¶
The per-language vacation notices pepsi-stage-vacation(1) falls back to, one option per language:
[pepsi-vacation-default-message]
EN = Hello {{SENDER_NAME}}, I am away until {{VACATION_END}}.
DE = Hallo {{SENDER_NAME}}, ich bin bis {{VACATION_END}} abwesend.
The option name is the language tag (EN, DE, PT_BR — either separator),
and the value is a Mustache template. Which
language is used, which fields expand, and why two spaces become a newline are
all documented in pepsi-stage-vacation(1).
This section normally already exists. make install ships it as
${DATADIR}/config.d/vacation.conf, and taler’s configuration loader parses every
file in ${DATADIR}/config.d before pepsi.conf. So a stage configured with
no message of its own still has ten languages available, and an operator who wants
different wording redefines just the language they care about in pepsi.conf:
later definitions win option by option, never section by section, so
overriding EN leaves the other nine in place. Editing the shipped file works too,
but a package upgrade overwrites it.
The section name is only a default (DEFAULT_MESSAGE_SECTION), so an operator who
wants several message sets — a formal one for a support address, a terse one for
everyone else — can define more sections and point different stages, domains or
addresses at them.
An account owner never touches this section: their own text goes in
MESSAGE_<LANG> in the stage’s own section, which the settings layer can carry
and which wins per language.
70.2.1.1.26. The [pepsi-autoconfig] Section¶
This optional section publishes mail account autoconfiguration
(draft-ietf-mailmaint-autoconfig): the XML document a mail client fetches,
knowing only the user’s address, so it can configure itself instead of asking
for eight settings the user usually cannot supply. It is served by
pepsi-httpd(1) at /mail/config-v1.1.xml on autoconfig.<domain>, and
at /.well-known/autoconfig/mail/config-v1.1.xml on the mail domain itself.
The feature is off until an incoming server is named. Omit the section — or
set neither IMAP_HOST nor POP3_HOST — and both endpoints answer 404.
That is deliberate rather than incidental: a client that finds a configuration
document stops walking the fallback chain, so publishing one that cannot tell it
how to read mail leaves the user worse off than publishing nothing at all.
Pepsi does not serve mailboxes (see pepsi-stage-relay-to-lmtp(1)), so the incoming half must name whatever MDA the deployment pairs with, typically Dovecot on the same host.
Two things outside this file are needed as well. An autoconfig.<domain>
DNS record must resolve to this server, and the TLS certificate must cover that
name — clients try the https://autoconfig.… URL first, and a certificate
error there is a failed lookup. A deployment unwilling to add the name can rely
on the /.well-known/ form alone, at the cost of being found only by clients
that try it.
- IMAP_HOST, POP3_HOST
(hostname, optional) The mailbox server to advertise. Setting either enables the feature; setting neither disables it. No default — an invented hostname would be worse than silence.
- IMAP_PORT, POP3_PORT
(number, optional) Defaults
993and995, the implicit-TLS ports.- IMAP_SOCKET, POP3_SOCKET
(``SSL`` | ``STARTTLS`` | ``plain``, optional) Transport security, in the draft’s own vocabulary (
SSLmeans implicit TLS). DefaultSSL.- IMAP_AUTH, POP3_AUTH
(optional) One of the draft’s authentication values:
password-cleartext,password-encrypted,NTLM,GSSAPI,TLS-client-cert,OAuth,client-IP-address,none. Defaultpassword-cleartext— which, overSSL, is the ordinary password-over-TLS arrangement rather than anything sent in the clear. An unrecognised value is a start-up error, not a silently published typo.- SMTP_HOST, SMTP_PORT, SMTP_SOCKET, SMTP_AUTH
(optional) The submission server to advertise. Normally omit all four: they are derived from the
[pepsi-ingress-listener-*]section flaggedSUBMISSION = yes— its port, itsMODEand its authentication mechanism — so moving submission from STARTTLS on 587 to implicit TLS on 465 updates the published document by itself, and the two cannot drift apart. Set them only when the public submission endpoint is not the listener, for instance behind a load balancer. SettingSMTP_HOSTswitches off the derivation entirely, so give the others too if they differ from587/STARTTLS/password-cleartext.A UNIX-socket or systemd-activated submission listener is never advertised: a remote client cannot use the first, and the second has no port in the configuration to publish.
- DISPLAY_NAME, DISPLAY_SHORT_NAME
(text, optional) What the client shows the user. Both default to the first
[pepsi-ingress] ACCEPTED_DOMAINSentry.- DOCUMENTATION_URL
(URL, optional) A help page for the account settings, published as the draft’s
<documentation>element. Omitted when unset.
[pepsi-autoconfig]
IMAP_HOST = mail.example.org
POP3_HOST = mail.example.org
DISPLAY_NAME = Example Mail
DISPLAY_SHORT_NAME = Example
# SMTP_* deliberately omitted: derived from the submission listener.
70.2.1.1.27. The [pepsi-tlsrpt] Section¶
This optional section configures SMTP TLS Reporting (RFC 8460). It has two independent halves; omit the section to disable both. pepsi-setup(1) reads it for the advertising half; the sender half is read at run time by the relay stages (session recording) and pepsi-tlsrpt(1) (the daily report job).
- RUA = URI [, URI …]
Reporting address(es) advertised in the
_smtp._tls.<domain>TXT record pepsi-setup(1) emits for each domain, so other senders report their TLS results toward our domains to us. Amailto:orhttps:URI, or a comma-separated list, stored verbatim. When unset, no record is published.- SEND_REPORTS =
yes|no Whether the relay stages record outbound TLS sessions (in
pepsi.tls_session) and the pepsi-tlsrpt(1) report job is active. Optional; defaults tono.- REPORT_FROM = ADDRESS
Envelope sender and
From:of the report e-mails pepsi-tlsrpt(1) sends. Required to send reports (its domain is the report submitter). Reports are not null-sender (RFC 8460 §5.3: they must be DKIM/DMARC-alignable); the report-about-a-report loop is prevented by the injectedstate.tlsrptflag.- REPORT_STAGE = STAGE
Pipeline stage report e-mails are injected at (so they are signed and relayed), like the pepsi-stage-anti-spam(1)
BLOCK_RESPONSE_STAGE. Must reference an existing[stage-*]. Required to e-mail (mailto:) reports.- ORGANIZATION = NAME
organization-nameplaced in emitted reports. Optional; defaults to theREPORT_FROMdomain.- CONTACT = ADDRESS
contact-infoplaced in emitted reports. Optional.- RETAIN_DAYS = DAYS
How many days pepsi-tlsrpt prune keeps
pepsi.tls_sessioncounters. Optional; defaults to7.
70.2.1.1.28. The [pepsi-telemetry-client] Section¶
This optional section configures the local feature-telemetry aggregator,
pepsi-telemetry-client(1). All options have working defaults; the whole
feature is gated by [pepsi] SHARE_TELEMETRY above, which is off unless the
operator turned it on — so nothing here has any effect until it is. (The central
collector, pepsi-telemetry(1), reads the separate [pepsi-telemetry]
section instead.)
- UNIXPATH = PATH
Local UNIX socket the daemon listens on for feature events; the same path the producer programs connect to. Optional; defaults to
/run/pepsi-telemetry/socket.- UNIXPATH_GROUP = GROUP
Group the socket is
chgrp-ed to (modeUNIXPATH_MODE), so thepepsi(stage workers) andpepsi-ingressusers can connect. Optional; defaults topepsi-telemetry.- UNIXPATH_MODE = OCTAL
Permission bits of the socket. Optional; defaults to
660.- SUBMIT_INTERVAL = DURATION
Interval between submissions to the collector (
h/m/sunits only — see the duration note above). Optional; defaults to60 s.- MAX_FEATURES = N
Cap on the number of distinct feature names held in memory, bounding memory against a misbehaving local producer. Optional; defaults to
4096.
70.2.1.1.29. Stored Data¶
For every accepted message, pepsi-ingress(1) inserts one row into the
pepsi.ingress table containing a local identifier (ingress_id), a random
token, the reception timestamp (received_at), the envelope sender
(mail_from; the empty string for the null sender <>), the envelope
recipient list (rcpt_to), and the parsed from_header and subject
headers. The message itself is stored split into two BYTEA columns — a
headers column (the RFC 5322 header block up to but not including the
blank-line separator, with the Authentication-Results header prepended; the
ARC set is added later by pepsi-stage-arc(1)) and a body column
(everything after the blank line; empty for a header-only message). The
reconstruction invariant is raw = headers || CRLF || body. Splitting the
message lets stages that only touch the headers or envelope load (and rewrite)
just the headers column and never pull the (potentially large) body. The
computed SPF, DKIM and DMARC verdicts are stored in the row’s state under an
auth key, and the authserv_id under state.origin; the arc verdict
starts as none and is filled in by the ARC stage. It then issues
NOTIFY ingress with the new row identifier as payload.
Each row additionally carries its processing state for the stage pipeline that
runs after ingress: a stage (the [stage-<stage>] section of the program
currently responsible for advancing the message), a status (one of
pending, running, paused, failed or timeout), a state
(the JSON object carried with the message, documented in pepsi.state(7)),
and a timeout (when a paused
message should be re-queued). Every new message is inserted explicitly at the
initial stage — stage = init, status = pending — so it enters the
pipeline at [stage-init]. These columns are inspected and repaired with
pepsi-queue(1).
The schema holds one other table, pepsi.dns_address: a cache of resolved
A/AAAA addresses for recipient mail-exchanger hosts, with per-address connect
health, maintained by pepsi-stage-relay-to-internet(1). Each row records a
host, one resolved address, the DNS-TTL expires_at, a failed flag
and the last_success_at time, so a working address is preferred until its TTL
elapses and a host is re-resolved once all its cached addresses have failed or
expired. It carries no message data and is not managed by pepsi-queue(1).
The state column is the JSON object carried with the message from ingress
to its terminal stage; it is the channel through which stages communicate.
Ingress seeds it with the connection’s SMTP-origin provenance, the inbound
SPF/DKIM/DMARC verdicts and any RFC 3461 DSN parameters; later stages merge in
their own verdicts and scratch. Its full layout — every key, who writes it and
who consumes it — is documented in pepsi.state(7).
70.2.1.1.30. Example¶
A complete forwarder: receive on the three canonical SMTP ports, ARC-seal on arrival, SRS-rewrite, deliver direct-to-MX, and bounce failures (signing the bounce before delivery):
[pepsi]
KEY_DIR = /var/pepsi/keys
DKIM_SELECTOR = pepsi
ARC_DOMAIN = example.org
ARC_ALGORITHM = rsa
[pepsi-postgres]
CONFIG = postgres:///pepsi
# SQL_DIR defaults to ${DATADIR}/sql; set it when running from a checkout.
[pepsi-ingress]
HOSTNAME = mail.example.org
ACCEPTED_DOMAINS = example.org example.com
MAX_MESSAGE_SIZE = 26214400
# The three canonical SMTP listeners. TLS_CERT/TLS_KEY are omitted so
# pepsi-setup auto-fills the certbot path and obtains the certificate.
# Port 25: cleartext with opportunistic STARTTLS.
[pepsi-ingress-listener-mx]
SERVE = tcp
BIND_TO = 0.0.0.0
PORT = 25
MODE = starttls
MYNETWORKS = 127.0.0.0/8 ::1/128 10.0.0.0/8
# Port 465: implicit TLS (SMTPS), authenticated submission via Dovecot SASL.
[pepsi-ingress-listener-submissions]
SERVE = tcp
BIND_TO = 0.0.0.0
PORT = 465
MODE = tls
SUBMISSION = yes
SASL_TYPE = dovecot
SASL_PATH = /run/dovecot/auth-client-pepsi
# Port 587: cleartext submission upgraded with STARTTLS.
[pepsi-ingress-listener-submission]
SERVE = tcp
BIND_TO = 0.0.0.0
PORT = 587
MODE = starttls
SUBMISSION = yes
SASL_TYPE = dovecot
SASL_PATH = /run/dovecot/auth-client-pepsi
[pepsi-srs]
SRS_DOMAIN = srs.example.org
SECRET_FILE = /etc/pepsi/srs.secret
# init: ARC-seal the message as received, then hand to SRS.
[stage-init]
PROGRAM = pepsi-stage-arc
NEXT_STAGE = srs
[stage-srs]
PROGRAM = pepsi-stage-srs
NEXT_STAGE = deliver
# deliver: direct-to-MX; failures go to the bounce stage.
[stage-deliver]
PROGRAM = pepsi-stage-relay-to-internet
SERVER_NAME = mail.example.org
POSTMASTER = postmaster@example.org
BOUNCE_STAGE = bounce
# This host's public sending IP(s); pepsi-setup unions PUBLIC_IP from every
# relay stage into the SPF record.
PUBLIC_IP = 203.0.113.7 2001:db8::25
# bounce: build an unsigned DSN, sign it, then deliver it.
[stage-bounce]
PROGRAM = pepsi-stage-bounce
SERVER_NAME = mail.example.org
POSTMASTER = postmaster@example.org
NEXT_STAGE = bounce-sign
BOUNCE_MESSAGE = default
[stage-bounce-sign]
PROGRAM = pepsi-stage-dkim-sign
NEXT_STAGE = deliver
To relay via a smarthost instead, replace the deliver stage’s PROGRAM
with pepsi-stage-relay-to-smarthost and add
[pepsi-stage-relay-to-smarthost-mta-*] sections, e.g.:
[pepsi-stage-relay-to-smarthost-mta-smarthost]
HOST = smtp.relay.example.net
PORT = 587
CATCH_ALL = yes
70.2.1.1.31. Files¶
Every Pepsi component reads the same file. When a component is started without –config, the first existing path from the following list is used:
$XDG_CONFIG_HOME/pepsi.conf$HOME/.config/pepsi.conf/etc/pepsi/pepsi.conf/etc/pepsi/pepsi.conf
make install places the default at /etc/pepsi/pepsi.conf and a reference
pepsi.conf.sample beside it.
Before that file is read, every *.conf in ${DATADIR}/config.d is parsed, so
packaged defaults are in place and pepsi.conf overrides them option by
option (a section defined in both is merged, not replaced). Two ship today:
vacation.conf— the per-language vacation notices; see The [pepsi-vacation-default-message] Section above.thunderbird.conf—[pepsi] CRYPTO_ALLOW_DOWNGRADE = yes, so S/MIME goes out as AES-256-CBCEnvelopedData, which Thunderbird can read and our compiled-in AES-256-GCM default cannot be read as. The file explains itself at length and is meant to be deletable: removing it restores authenticated encryption with no other edit.
These files belong to the
package: redefine the options you want in pepsi.conf rather than editing them,
or an upgrade will discard the change.
70.2.1.1.32. See Also¶
pepsi-ingress(1), pepsi-dispatch(1), pepsi-httpd(1), pepsi-setup(1), pepsi-config(1), pepsi-queue(1), pepsi-whitelist(1), pepsi-settings(1), pepsi-tlsrpt(1), pepsi-failure-bouncer(1), pepsi-stage-arc(1), pepsi-stage-srs(1), pepsi-stage-encrypt(1), pepsi-stage-dkim-sign(1), pepsi-stage-relay-to-internet(1), pepsi-stage-relay-to-smarthost(1), pepsi-stage-bounce(1), pepsi-stage-discard(1), pepsi-stage-anti-spam(1), pepsi-stage-check-whitelist(1), pepsi-stage-auto-whitelist(1), pepsi-stage-detect-language(1), pepsi-stage-block-language(1), pepsi-stage-if(1), pepsi-stage-milter(1), pepsi-stage-aliases(1), pepsi-stage-vacation(1), pepsi-stage-edit-settings(1), pepsi.state(7), systemd.socket(5)
70.2.1.1.33. Bugs¶
Report bugs to the Pepsi issue tracker.