13. Microsoft Exchange as a gateway¶
Pepsi can sit in front of and behind a Microsoft Exchange or Exchange Online deployment as a transparent crypto gateway: inbound mail is decrypted and verified before Exchange stores it, outbound mail is signed and encrypted after Exchange has sent it. Users get S/MIME and OpenPGP without touching their clients.
This chapter describes the supported topologies, the exact Exchange connector settings, the operational traps that are specific to Exchange Online, and the loop prevention that this deployment shape makes mandatory rather than optional.
Warning
Nothing in this chapter has been tested against a real Exchange or Exchange
Online tenant. Pepsi’s own test suite uses a plain SMTP stand-in and
.invalid domains. The connector settings below follow Microsoft’s
documented behaviour and the experience of an earlier gateway of this design,
but expect to adjust them, and read Troubleshooting before
concluding that Pepsi is at fault.
13.1. Topologies¶
Three arrangements are supported. Which one applies decides what the rest of this chapter asks you to configure.
Both directions (the full gateway). One Pepsi instance is the organisation’s MX and the smarthost Exchange sends through. This is the arrangement that gives end-to-end crypto in both directions.
Internet ──▶ pepsi-ingress :25 ──▶ arc ──▶ decrypt ──▶ aliases ──▶ route ──┬─▶ exchange (smarthost)
└─▶ internet (direct MX)
Exchange ──▶ pepsi-ingress :587 ──▶ encrypt ──▶ srs ──▶ dkim-sign ──▶ internet
(authenticated)
Front-only. Pepsi is the MX and hands everything to Exchange; Exchange’s own outbound path is left alone. Inbound mail is decrypted and verified; outbound mail is untouched. A real deployment for an organisation that cannot change its outbound flow.
Behind-only. Exchange keeps the MX; Pepsi is only the smarthost Exchange relays through. Outbound mail is signed and encrypted; inbound is untouched. Suits an organisation whose inbound already passes through a compliance or filtering gateway.
The two partial cases are not second-class: the routing stage, the loop
prevention and the ADDRESS_FAMILY control all behave the same in each. What
differs is only which half of the pipeline you configure.
13.2. The routing decision¶
A gateway in front of another mail system has one routing decision on every inbound message: does this recipient belong to the system behind me, or to the internet?
Getting it wrong is not a delivery failure. The domains behind the gateway are exactly the domains whose public MX is the gateway, so handing one to a direct-to-MX relay posts the message back into Pepsi’s own ingress, which accepts it, relays it out again, and repeats.
pepsi-stage-route exists for this decision:
[stage-route]
PROGRAM = pepsi-stage-route
# Recipients at a domain we serve go to Exchange...
MANAGED_STAGE = exchange
# ...everything else out to the public internet.
NEXT_STAGE = internet
MANAGED_DOMAINS defaults to [pepsi-ingress] ACCEPTED_DOMAINS, so the set
of domains treated as “behind the gateway” cannot drift from the set ingress
accepts. Add ROUTES only for exceptions — a legacy subdomain delivered
locally, a partner domain with its own next hop.
In a behind-only deployment there is no routing decision to make: Pepsi only ever relays outward, and the routing stage is not needed.
13.3. Exchange Online: the tenant endpoint¶
Important
Point the smarthost at the tenant-specific endpoint, never at your own domain’s public MX.
Your domain’s MX record points at Pepsi (that is the point of the gateway).
Configuring Exchange Online as example-com.mail.protection.outlook.com
reaches Microsoft; configuring it as mail.example.com reaches Pepsi, and
every message loops.
The tenant endpoint is shown in the Microsoft 365 admin centre under the
domain’s DNS records, and has the form <tenant>.mail.protection.outlook.com
— where <tenant> is normally your domain with dots replaced by hyphens
(example.com → example-com). Regional tenants may differ (for instance
<tenant>-ch.mail.protection.outlook.com); use whatever the admin centre
shows, not a constructed guess.
[pepsi-stage-relay-to-smarthost-mta-exchange]
HOST = example-com.mail.protection.outlook.com
PORT = 25
MODE = starttls
TLS_VERIFY = yes
AUTH = none
DOMAINS = example.com example.net
# Exchange Online rejects a sending IPv6 address with no PTR; see below.
ADDRESS_FAMILY = ipv4
AUTH = none is correct here: Exchange Online authenticates an inbound
connector by the sending IP address or by the TLS certificate, not by
SASL. Pepsi still uses STARTTLS, which Exchange Online requires.
pepsi-setup refuses a smarthost whose host is this machine and whose port is
one an ingress listener is bound to, so the most direct form of this mistake is
caught before it can loop.
13.4. Address family selection¶
Exchange Online rejects mail from a sending IPv6 address whose reverse DNS
(PTR) is missing, with a 550 5.7.1 ... S820 error. A dual-stacked host
prefers IPv6, so a machine whose IPv4 reverse DNS is correct and whose IPv6
reverse DNS was never delegated delivers fine to everyone except Exchange
Online.
The fix is to reach that one destination over IPv4. ADDRESS_FAMILY takes
any (the default), ipv4 (also spelled v4/v4only) or ipv6
(v6/v6only).
Precedence is: the ``[…-mta-*]`` entry, then the owning ``[stage-*]`` section, then ``any``. The per-entry level is the one that matters: pinning the whole stage to IPv4 would also force every other destination onto IPv4, including one that publishes only AAAA.
This is a third place to look when connections go somewhere unexpected. When a
delivery is using a family you did not expect, check the MTA entry first, then
the stage section, and remember that the configured family is intersected with
what the local host can actually route — pinning ipv6 on a host with no
global IPv6 route leaves nothing to connect to.
The better fix, where you control the reverse zone, is to publish the PTR
record. ADDRESS_FAMILY is for when you do not.
13.5. Loop prevention¶
A gateway on both sides of another MTA is the classic loop topology, and Pepsi has three independent defences. None of them is optional in this deployment.
1. Configuration refusal (``pepsi-setup``). Two checks, both errors rather than warnings:
A routing stage from which a served domain can reach
pepsi-stage-relay-to-internet.pepsi-setupwalks forward from every stage a managed recipient can reach and refuses if a direct-to-MX relay is among them. (BOUNCE_STAGEis not followed — a bounce is a new null-sender message about a delivery that already failed, and it legitimately reaches a relay.)A smarthost whose
HOSTis this host — its[pepsi-ingress] HOSTNAME,localhost, or a loopback address — and whosePORTis one an ingress listener is bound to. Both halves are required: a site may legitimately run an unrelated relay on the same host, or reach a different machine on port 25.
2. The hop count (``MAX_HOP_COUNT``). Both relay stages count Received:
header fields and refuse to relay a message with more than MAX_HOP_COUNT
(default 30, RFC 5321 §6.3). This is the backstop, not the defence: by the time
it fires, thirty copies of the message have crossed the gateway, and the bounce
it produces is addressed back into the loop.
3. Exchange’s own connector scoping. Restrict the inbound connector to Pepsi’s IP addresses (below), so a message arriving from anywhere else is not treated as gateway traffic.
13.6. Exchange connector configuration¶
The exact settings depend on whether Exchange sends to Pepsi, receives from Pepsi, or both.
13.6.1. Inbound to Exchange (Pepsi → Exchange)¶
In Exchange Online this is an inbound connector (Exchange admin centre → Mail flow → Connectors → From: Your organisation’s email server, To: Office 365):
Setting |
Value |
|---|---|
Connector type |
From your organisation’s email server to Office 365 |
How to identify the sender |
By verifying that the sending server’s IP address matches — list every
address Pepsi sends from (see |
Reject messages if not sent over TLS |
Yes |
On on-premises Exchange the equivalent is a Receive Connector on the
Hub Transport / Frontend role, with RemoteIPRanges set to Pepsi’s addresses
and PermissionGroups including AnonymousUsers (with
Ms-Exch-SMTP-Accept-Any-Recipient granted if Exchange must accept for domains
it is not authoritative for).
Note
If the connector identifies Pepsi by IP, and Pepsi is pinned to IPv4 for this destination (Address family selection), list the IPv4 address. Listing only the IPv6 address of a host that will never use it is a silent authentication failure.
13.6.2. Outbound from Exchange (Exchange → Pepsi)¶
In Exchange Online this is an outbound connector (From: Office 365, To: Partner organisation), with routing set to Route email through these smart hosts naming Pepsi’s hostname, and Always use Transport Layer Security on with certificate validation against Pepsi’s name.
On the Pepsi side, Exchange arrives at a submission listener:
[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
13.6.3. Authenticating the connector¶
There are two ways to let Exchange relay through Pepsi, and they are not equivalent.
SASL with a dedicated account (recommended). Exchange authenticates with a
username and password. Because a submission listener enforces RFC 6409 §6 — a
sender may only use an address the authenticated identity is entitled to — the
connector’s account needs a USERNAME_MAP entry permitting the served
domains, or every message Exchange relays is refused:
# /etc/pepsi/username.map — the connector account may send as any of our users.
exchange-connector *@example.com *@example.net
Without that entry the account may send only as itself, and the first message
Exchange relays gets a 550.
Network trust (``MYNETWORKS``). Simpler — list Exchange’s addresses and skip
authentication — but it carries the self-relay risk this chapter is about: any
host that can reach the listener from a trusted address can relay through it,
and a mistake in the range turns Pepsi into an open relay. Prefer SASL; if you
use MYNETWORKS, list individual addresses rather than ranges, and never
include loopback.
13.7. The Outlook add-in¶
Pepsi’s encryption stage acts on two headers a message can carry:
X-Pepsi-Sign and X-Pepsi-Encrypt. The add-in is what lets a user set
them from Outlook.
Enable the routes and reload pepsi-httpd:
[pepsi-httpd]
ADDIN = yes
# Only needed when the request's own Host header does not name this gateway
# (for example behind a proxy that rewrites it).
# ADDIN_URL = https://mail.example.com
The manifest is then served from https://<gateway>/addin/manifest.xml, with
the gateway URL and a per-deployment identifier substituted for you.
Deployment is by sideloading. In the Microsoft 365 admin centre, go to Settings → Integrated apps → Upload custom apps, choose Provide link to manifest file, and give it that URL. There is no AppSource listing: the manifest’s content depends on your gateway’s URL, so there is no single artifact to publish, and a store review sitting between you and a fix is a poor trade for one page of JavaScript.
Users without the add-in get the same effect from a subject keyword —
[sign], [encrypt] or [secure] anywhere in the Subject. The
encryption stage acts on it and removes it before the message is sent, so the
recipient never sees it. The keywords are configurable
(SUBJECT_KEYWORDS_SIGN, SUBJECT_KEYWORDS_ENCRYPT,
SUBJECT_KEYWORDS_BOTH).
Note
A subject keyword means required, not try: a message a user deliberately
marked [encrypt] is not sent in the clear if a recipient has no key. What
happens instead is ON_NO_KEY’s business.
13.8. The X-Pepsi-* header contract¶
Three fields, one namespace, and one rule that makes the whole thing meaningful.
Field |
Meaning |
|---|---|
|
Request: sign this message. |
|
Request: encrypt this message. |
|
Result: what the inbound stage found. Semicolon-separated |
Requests are honoured only for locally submitted mail
(state.local_origin) and are stripped from outbound mail
unconditionally, whatever STRIP_REQUEST_HEADERS says: a control channel
that survived onto the wire would be one the next hop could be told to obey.
The result header can be believed because the inbound stage removes the entire ``X-Pepsi-*`` namespace from every message it sees — including one it is disabled for, one addressed to a domain it does not serve, and one it is about to split — before adding its own. It follows that a message that did not traverse a decrypt stage carries no trustworthy result header, and nothing should present one as authoritative.
Pepsi-Origin is deliberately not in this namespace and is never stripped:
the payment-correlation path keys on it.
The result header’s signature value is one of none, valid,
valid-untrusted, invalid or unverifiable.
Danger
valid-untrusted means the signature verified against a key that is not
anchored in any trust path — anyone can produce one. It must never be
rendered as verified in a user interface, a task pane or a mail client rule.
13.9. Identity auto-provisioning¶
[pepsi-crypto] AUTO_CREATE_IDENTITY (on by default) makes Pepsi generate
a key for a local sender who has none, the first time that sender explicitly
asks for protection — with the add-in, or with a subject keyword. It does not
fire on ordinary mail.
This is how an organisation gets end-to-end coverage without per-user work: a user ticks the box, a key appears, and it is published so correspondents can use it.
It is also how an organisation publishes, over time, the address of every person
who ever asked for a signed message. An auto-created identity is marked
published, and a published identity is served from this host’s own Web Key
Directory — which is exactly what makes it usable, and exactly what makes the
address discoverable by anyone who guesses it. With [pepsi-keydiscovery]
VKS_PUBLISH on it is additionally uploaded to a public key server, from which
it cannot be withdrawn at all.
Both halves are wanted for a general-purpose deployment and neither is obviously right for an organisation whose address list is itself sensitive, or one bound by a policy on disclosing staff addresses. The dials, from most to least automatic:
leave both on — full coverage, and every sender who ever ticked the box is discoverable at their address;
leave
AUTO_CREATE_IDENTITYon butVKS_PUBLISHoff — discoverable via this host’s WKD only, so withdrawal means unpublishing one identity rather than asking a key server to forget;turn
AUTO_CREATE_IDENTITYoff — key provisioning becomes an explicit act by an operator with pepsi-keys, and a user who asks for encryption without a key gets whateverON_NO_KEYsays instead.
13.10. Troubleshooting¶
- Messages loop between Pepsi and Exchange.
The smarthost is pointed at your public MX rather than the tenant endpoint, or a served domain is routed to the direct-to-MX stage. Run
pepsi-setup check: both are refused, so a configuration that still loops means the running configuration is not the one you validated. Look for the loop inpepsi-queue— a message with a largeReceived:count is the tell.- Exchange Online rejects with 550 5.7.1 … S820.
The sending IPv6 address has no
PTR. Publish one, or setADDRESS_FAMILY = ipv4on that MTA entry (Address family selection).- Exchange Online rejects with 550 5.7.64 TenantAttribution.
The inbound connector does not recognise the sender. Check that the connector lists the address Pepsi actually sends from — the IPv4 one if
ADDRESS_FAMILYpins it.- Everything Exchange relays gets 550 5.7.1 From address not permitted.
RFC 6409 §6: the connector’s SASL account may only send as itself. Give it a
USERNAME_MAPentry covering the served domains.- A delivery is using the wrong address family.
Precedence is the
[…-mta-*]entry, then the[stage-*]section, thenany— and the result is intersected with what the host can route.- X-MS-* headers.
Exchange adds and consumes a number of
X-MS-*headers. Pepsi does not read, rewrite or remove them; only theX-Pepsi-*namespace is touched. Be aware, though, that re-encoding a message for a next hop that lacks8BITMIMErewrites the body and so invalidates any signature over it — see SMTP Protocol Extensions.
13.11. What Pepsi does not do¶
Journaling and archiving are out of scope. Exchange deployments frequently
have a retention requirement, and Pepsi does not answer it: there is no archive
store, no retention engine, no search and no legal-hold facility. See
docs/ROADMAP.md.
Warning
[pepsi] MAIL_LOG is not an archive, and must not be presented to an
auditor as one. It records no message content at any setting — at most
the envelope, the outcome and (at full) the Subject: line — and its
rows are pruned after [pepsi-admin] MAIL_LOG_RETENTION_DAYS, thirty
days by default. It is an operational log with a retention limit, which is
the opposite of a retention mechanism.
Discovering that difference during an audit is a failure one sentence can
prevent, so here it is: if you have a retention obligation, satisfy it with
Exchange’s own journaling or a dedicated archiving product, and treat
MAIL_LOG as telling you what happened last month, not what was said last
year.
The narrower question a crypto gateway is actually expected to answer — was
this message encrypted, and what did we decide about it — is what MAIL_LOG
does answer, when it is enabled. Note that turning it on means the deployment
keeps a record of who corresponds with whom; pepsi.conf(5) discusses that
trade-off.