70.1.11. pepsi-stage-relay-to-smarthost¶
relay a message through an upstream smarthost
- Manual section:
1
70.1.11.1.1. Name¶
pepsi-stage-relay-to-smarthost - the smarthost delivery stage of the Pepsi pipeline.
70.1.11.1.2. Synopsis¶
pepsi-stage-relay-to-smarthost [GLOBAL-OPTIONS] worker
70.1.11.1.3. Description¶
pepsi-stage-relay-to-smarthost is a stage program run by pepsi-dispatch(1) as a persistent worker reading message ids on standard input. It loads that pepsi.ingress row (refusing to
act unless its status is running), reads its [stage-<stage>] section,
and relays the message to a configured upstream MTA (smarthost).
The recipient’s domain selects the smarthost: the [pepsi-stage-relay-to-smarthost-mta-<name>]
section whose DOMAINS lists it, or the catch-all MTA (CATCH_ALL = yes).
These MTA sections are shared across all egress stages (see
pepsi.conf(5)). The message is sent with its own envelope sender,
using the MTA’s configured transport (plain/tls/starttls),
certificate verification and authentication.
On success the row is removed (or, if the stage sets NEXT_STAGE, advanced).
A transient failure pauses the message with an exponential backoff (re-queued by
pepsi-dispatch(1) when its timeout elapses); past MAX_LIFETIME the
failure is treated as permanent. A permanent failure — including a recipient
with no matching MTA — is routed to the stage’s BOUNCE_STAGE (or, with none,
the row is marked failed). A permanent failure of a bounce (null sender) is
discarded rather than bounced again.
Unlike pepsi-stage-relay-to-internet(1), pepsi-stage-relay-to-smarthost does no MX discovery and never uses DNS-learned MTA-STS: it speaks only to the smarthosts named in the configuration.
DSN (RFC 3461): when the smarthost advertises the DSN extension, the
parameters the origin requested (RET/ENVID on MAIL FROM and the
recipient’s NOTIFY/ORCPT on RCPT TO, read from the row’s
state.dsn) are propagated to it; when it does not advertise DSN they are
omitted. On a permanent failure the recipient’s NOTIFY/ORCPT and the
ENVID are handed to the bounce stage so a failure DSN is generated only when
the sender asked for one. This stage preserves state.dsn.
When the global [pepsi] ORIGINATE_SUCCESS_DSN is enabled and a successful
delivery’s recipient requested NOTIFY=SUCCESS, the message is routed (after
delivery) to BOUNCE_STAGE, which emits a positive (Action: delivered)
report to the sender. With the flag off, or without a SUCCESS request, a
successful delivery is simply finished.
When DELAY_DSN_AFTER is set and a still-undelivered message has been queued
at least that long, a one-shot “delayed” (Action: delayed) DSN is sent to the
sender — but only if the sender requested NOTIFY=DELAY (delay has no implicit
default). The original message keeps being retried; the warning is a separate
message routed through BOUNCE_STAGE, and is sent at most once.
Content downgrading (RFC 6152 / RFC 6531): the message is matched to the
extensions the smarthost actually advertises. When the body is 8-bit and the
smarthost supports 8BITMIME the envelope carries BODY=8BITMIME; when it
does not, each 8-bit MIME leaf part is re-encoded with a 7-bit
content-transfer-encoding (quoted-printable for text/*, base64 otherwise).
When the message needs SMTPUTF8 and the smarthost supports it the envelope
carries SMTPUTF8; when it does not, UTF-8 header fields are rewritten as
RFC 2047 encoded-words and a message with a non-ASCII address is bounced.
Re-encoding a body breaks any body-covering signature already on the message
(the originator’s DKIM body hash and the ARC-Message-Signature); see
pepsi-stage-relay-to-internet(1).
Proof of origin: when the shared [pepsi-origin] section is configured (on by
default), this stage stamps every outbound message with a Pepsi-Origin header
and records its nonce, exactly as pepsi-stage-relay-to-internet(1) does (the
smarthost passes the header through to the eventual MX), so a bounce returning for
smarthost-relayed mail can be verified by pepsi-stage-anti-spam(1). See that
stage and pepsi.conf(5) for details. Without the section no header is added.
SMTP TLS Reporting (RFC 8460): when [pepsi-tlsrpt] SEND_REPORTS is set, each
TLS session to the smarthost is recorded (best-effort) as an aggregate counter in
pepsi.tls_session — a success, or a classified TLS failure — keyed by the
applied policy (tlsa when DANE authenticated the smarthost, else
no-policy-found) and the smarthost host. pepsi-tlsrpt(1) compiles these
into daily reports; recording is skipped for a report message itself
(state.tlsrpt).
70.1.11.1.4. Configuration¶
Pipeline wiring and the operational options (SERVER_NAME, the connection
timeouts, the retry-backoff schedule and MAX_LIFETIME, DELAY_DSN_AFTER,
MAX_HOP_COUNT and the DANE DNS settings) live in the stage’s own
[stage-<name>] section (PROGRAM = pepsi-stage-relay-to-smarthost); the
upstream smarthosts are defined once in the shared
[pepsi-stage-relay-to-smarthost-mta-<name>] sections (HOST, PORT,
MODE, TLS_VERIFY/TLS_CA/DANE, the AUTH credentials,
HELO_NAME and DOMAINS/CATCH_ALL). All are documented in
pepsi.conf(5).
70.1.11.1.5. Authentication¶
When relaying through a smarthost, Pepsi proves its own identity to that upstream
MTA via SMTP AUTH (RFC 4954), configured per smarthost with the
[pepsi-stage-relay-to-smarthost-mta-<name>] AUTH option. The mechanisms
currently supported are:
none(default) — no client authentication.plain— SASLPLAIN(RFC 4616): theUSERNAME/PASSWORDare sent base64-encoded.login— SASLLOGIN: the legacy two-step user/password exchange used by some older smarthosts.cram-md5— SASLCRAM-MD5(RFC 2195): a challenge/response in which Pepsi proves knowledge of thePASSWORDviaHMAC-MD5over the server’s challenge, so the password itself is never sent. Weaker than SCRAM (no salting, no mutual authentication, MD5) — preferscram-*where available.digest-md5— SASLDIGEST-MD5(RFC 2831): a salted challenge/response that also authenticates the server to Pepsi viarspauth(a mismatch is a permanent failure). Deprecated — RFC 6331 movedDIGEST-MD5to Historic status; it is provided only for legacy smarthosts that offer nothing better, and Pepsi emits a warning (both from pepsi-setup(1) at configuration time and in the relay logs the first time it is used). It is never selected byauto. Preferscram-*orcram-md5.scram-sha-1/scram-sha-256— SASLSCRAM-SHA-1/SCRAM-SHA-256(RFC 5802 / RFC 7677): a salted challenge/response that, unlikecram-md5, also authenticates the server to Pepsi — the exchange aborts (a permanent failure) if the server’s signature does not verify against thePASSWORD, or if the server accepts the login without returning itsserver-finalsignature at all (so a peer cannot dodge mutual authentication by replying235early).SCRAM-SHA-256is preferred.USERNAME/PASSWORDare normalised with SASLprep (RFC 4013).scram-sha-1-plus/scram-sha-256-plus— the channel-binding-PLUSvariants of the above, binding the SASL exchange to the underlying TLS channel withtls-server-end-point(RFC 5929; a hash of the server certificate). This detects a man-in-the-middle that terminates TLS and re-originates it. Requires an encryptingMODE(there is no channel to bind otherwise), and the smarthost must advertise the-PLUSmechanism.auto— negotiate the strongest password mechanism the smarthost advertises in itsEHLO, from the sameUSERNAME/PASSWORD. Preference order:SCRAM-SHA-256-PLUS>SCRAM-SHA-256>SCRAM-SHA-1-PLUS>SCRAM-SHA-1>CRAM-MD5>LOGIN>PLAIN(the-PLUSvariants are only chosen on a TLS session). The recommended setting when you do not need to pin a specific mechanism.external— SASLEXTERNAL(RFC 4422 App. A): the smarthost authenticates Pepsi by the TLS client certificate it presents during the (mutual-TLS) handshake, so no password is sent. RequiresTLS_CLIENT_CERT/TLS_CLIENT_KEYto be configured (see Mutual TLS below). An optionalUSERNAMEis sent as the SASL authorization identity; when omitted the smarthost derives the identity from the certificate.oauth— SASL OAuth 2.0 bearer token, required by large providers such as Gmail and Microsoft 365. The concrete mechanism is chosen automatically from the smarthost’sEHLO, preferring the standardisedOAUTHBEARER(RFC 7628) and falling back to the de-factoXOAUTH2.USERNAMEis the account address sent in the SASL exchange andTOKEN_FILEis the path to a file holding the current OAuth 2.0 access token (leading/trailing whitespace is trimmed). The file is re-read on every delivery, so an external refresher can replace an expired token without restarting Pepsi.Access tokens are short-lived (typically about an hour). The relay stage only reads the token; keeping
TOKEN_FILEcurrent is an external job. Pepsi ships pepsi-helper-token-refresh(1) for this — a service that obtains fresh tokens from the provider’s token endpoint and rewrites the file — but it is optional and not part ofpepsi.target; a site may use any equivalent (a cron job, a cloud agent). A delivery attempted while the token is missing, empty, expired or otherwise rejected is treated as a transient failure, so the message stays queued and is retried once a fresh token is in place rather than being bounced.For the relay worker (run as
pepsi) to read a token written by the refresher, the stage binary is installed SGID thepepsi-tokengroup and the token files are group-readable to it (mode0640in the SGID/var/pepsi/tokensdirectory). See the Extending the Pipeline manual chapter for the full refresh contract and how to substitute your own refresher.ntlm— the de-facto MicrosoftNTLMmechanism (no RFC), for older Exchange / Office 365 hybrid deployments. Only NTLMv2 is implemented (NTLMv1 is cryptographically broken and never sent). Because Pepsi only ever runs NTLM over TLS, the response always carries Extended Protection for Authentication (EPA): theMsvAvChannelBindings(tls-server-end-point, RFC 5929) andMsvAvTargetName(thesmtp/<host>SPN) AV pairs plus the message-integrity code, so it authenticates against EPA-enforcing servers. The requiredNT_DOMAINis the Windows domain and the optionalNT_WORKSTATIONis a cosmetic client name;USERNAME/PASSWORDare the account credentials. NTLM is weak (it uses only the NT hash) and does not authenticate the server to Pepsi — prefergssapi,scram-*oroauthwhere the smarthost supports them.pepsi-setupwarns when it is configured.gssapi— SASLGSSAPI/ Kerberos (RFC 4752), for Kerberized smarthosts (typically an on-premise Exchange in an Active Directory realm). Authentication uses a Kerberos service ticket forSERVICE_NAME(a host-based service name, defaultsmtp@<HOST>) obtained from a Kerberos credential cache; after the GSS token exchange the SASL security layer is negotiated to no security layer (the session is already protected by TLS). No password is configured.Pepsi only reads the credential cache and never holds the long-term Kerberos key: an external process — a keytab plus
k5start/cron— must keep a ticket-granting ticket current. The cache is the ambientKRB5CCNAME(set on the dispatcher service environment) unless the per-MTAKRB5CCNAMEoption overrides it. For the relay worker (run aspepsi) to read a cache written by the refresher, place aFILE:cache under the SGID/var/pepsi/krb5directory (grouppepsi-token, which the stage binary already runs SGID for OAuth/mTLS). A delivery attempted while the cache is missing or the ticket has expired is a transient failure (the message stays queued, likeoauth). See the Extending the Pipeline manual chapter for the refresh contract.
All the password-based mechanisms (plain, login, cram-md5,
digest-md5, the scram-* family, auto and ntlm) as well as
oauth and gssapi send or derive a credential (or a security context), so
Pepsi only offers them once the session is encrypted (use MODE = tls or
starttls); each is refused if the smarthost does not advertise the
corresponding AUTH mechanism. The challenge-response mechanisms (cram-md5,
digest-md5, scram-*, ntlm) never transmit the password itself, but
Pepsi still requires TLS for them. external and gssapi send no password
over SMTP — the credential is the TLS client certificate or a Kerberos ticket —
but likewise require an encrypted session. For ntlm and gssapi a
MODE = plain smarthost is rejected at configuration time.
70.1.11.1.6. Mutual TLS¶
Independently of (or together with) AUTH, a smarthost can be configured to
present a client certificate during the TLS handshake (mutual TLS):
TLS_CLIENT_CERT— path to the PEM certificate chain to present.TLS_CLIENT_KEY— path to the matching PEM private key.
Both must be set together, and only with an encrypting MODE (tls or
starttls). The certificate is presented on every connection and is loaded
fresh each time, so a renewed certificate is picked up without restarting Pepsi.
It applies to PKIX, TLS_VERIFY = no and DANE sessions alike (the client
certificate is orthogonal to how the server certificate is validated).
The certificate alone satisfies many smarthosts (transport-layer mutual TLS,
with AUTH = none). Setting AUTH = external additionally issues SMTP
AUTH EXTERNAL so the smarthost ties the SMTP session to the presented
certificate; it can also be combined with AUTH = plain/oauth if a
smarthost wants both a client certificate and a password/token.
The private key is secret material. As with the OAuth token files, the relay
worker (run as pepsi) reads it through the SGID pepsi-token group: on
a Debian install drop the cert and key into /var/pepsi/tls (created SGID
pepsi-token), so the key inherits group pepsi-token and is readable by
the stage. Keep the key mode 0640 (or tighter) and never world-readable.
Note
Future work. Common smarthost client-authentication methods that Pepsi does not yet support, in rough order of demand:
SASL ``GSSAPI``/Kerberos (RFC 4752) and ``NTLM`` (legacy Microsoft Exchange).
70.1.11.1.7. State¶
Inputs (read from the row’s state; all optional):
state.dsn— the RFC 3461 parameters; propagated to the smarthost when it advertisesDSNand consulted to gate failure/success/delay reports.state.origin— the 8BITMIME/SMTPUTF8 hints used by content downgrading.
Outputs (merged into the row’s state):
On a transient failure (
pause):attempts,last_errorand, once a delay DSN has been emitted,delay_sent.On a permanent failure with a
BOUNCE_STAGE(reroute), or on a delay warning (an enqueued clone): astate.bounceobject (kind=permanentordelay). For an SMTP-level failure it also records the structured next-hop detail —remote_mta(the smarthost),smtp_code,enhanced_status,phaseandreply_text— so the bounce can state precisely why the smarthost refused the message.On a successful relay with
ORIGINATE_SUCCESS_DSNandNOTIFY=SUCCESS: astate.bounceobject withkind = success.On a permanent failure with no
BOUNCE_STAGE(fail):last_error.
state.dsn and state.origin are always preserved. The state layout is
described in full in pepsi.state(7).
Transitions (the retry schedule is RETRY_INITIAL / RETRY_FACTOR / RETRY_MAX_INTERVAL, bounded by MAX_LIFETIME):
successful relay → finish (the row is deleted), or advance to NEXT_STAGE if one is set; with [pepsi] ORIGINATE_SUCCESS_DSN and a recipient that asked for
NOTIFY=SUCCESSit instead reroutes to BOUNCE_STAGE to emit a positive (Action: delivered) DSN;transient failure → pause for the next retry interval;
still queued past DELAY_DSN_AFTER with a
NOTIFY=DELAYrecipient → enqueue a one-shot delay-DSN clone at BOUNCE_STAGE (the original stays queued);permanent failure (including a recipient with no matching MTA), or MAX_LIFETIME exhausted → reroute to BOUNCE_STAGE, or fail (terminal
failed) if no BOUNCE_STAGE is configured;an undeliverable bounce (null sender) is never re-bounced — it is simply discarded (this stage has no postmaster double-bounce copy).
70.1.11.1.8. Commands¶
- worker
Run as a persistent pepsi-dispatch(1) worker, reading message ids on standard input.
70.1.11.1.9. Global Options¶
- -c FILE, –config FILE
Read the configuration from FILE instead of searching the default locations.
- -L LOGLEVEL, –log LOGLEVEL
Set the logging verbosity (default
info).- -v, –verbose
Show log messages from all sources.
- -h, –help; -V, –version
Print a usage summary / the version and exit.
70.1.11.1.10. Exit Status¶
- 0
The message was processed (delivered, paused, rerouted, failed or discarded).
- 1
An error occurred (message not found or not
running, misconfigured stage, or a database error). The reason is written to the log.
70.1.11.1.11. Examples¶
Run the egress stage on message 42 (it must be running):
pepsi-stage-relay-to-smarthost -c /etc/pepsi/pepsi.conf 42
70.1.11.1.12. See Also¶
pepsi-config(1), pepsi-stage-bounce(1), pepsi-stage-dkim-sign(1), pepsi-stage-relay-to-internet(1), pepsi-dispatch(1), pepsi.conf(5), pepsi.state(7), pepsi-setup(1)
70.1.11.1.13. Bugs¶
Report bugs to the Pepsi issue tracker.