<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-us">
  <title>HELO, world</title>
  <subtitle>Field notes from the engineers building email security at Mailprotector.</subtitle>
  <link href="https://mailprotector.engineering/feed/main" rel="self"/>
  <link href="https://mailprotector.engineering/"/>
  <id>https://mailprotector.engineering/</id>
  <updated>2026-07-13T00:00:00.000Z</updated>
  <entry>
    <title>If phishing simulations work, why do they need a mandate?</title>
    <link href="https://mailprotector.engineering/articles/phishing-sims-mandate-engineering/"/>
    <id>https://mailprotector.engineering/articles/phishing-sims-mandate-engineering/</id>
    <updated>2026-07-13T00:00:00.000Z</updated>
    <published>2026-07-13T00:00:00.000Z</published>
    <author><name>Ben Hathaway</name></author>
    <summary>Phishing simulations produce compliance artifacts, and that&apos;s why they persist even though they don&apos;t protect users. The technical case for moving the trust decision off the content an attacker controls.</summary>
    <content type="html"><![CDATA[<p>Most security programs can’t answer that question without admitting the truth: phishing simulations aren’t in your stack because they protect users. They’re there because an insurer, a framework, or a client checkbox requires them.</p>
<p>That’s not protection. That’s user-blame security.</p>
<p>Mailprotector sells in the same channel as every sim vendor—same conferences, same insurance forms, same client requests. I first made this argument out loud at <a href="https://rejectioncon.com">RejectionCon</a>, a virtual conference for the talks too spicy for the mainstream stages, at an event sponsored in part by companies that sell phishing sims. So when I say the model is broken, I’m not saying it from outside the market; I argued it on a stage those vendors helped pay for. A partner once told me the sim renewal was easy to justify because the insurance form asked for it. That sentence explains the whole category better than most marketing does.</p>
<p>This is the technical version of the argument: what the simulation actually assumes, why that assumption was always fragile, why AI broke it, and what a defensible model looks like at the system level.</p>
<h2>A simulation is a test of the user</h2>
<p>That only makes sense if you’ve already decided the user is the control. And that decision is the foundation underneath the entire awareness-and-training category: malicious messages will reach people, so people have to catch them.</p>
<p>For that to work, three conditions have to hold on every dangerous message, under load, while the user does the job they were actually hired to do:</p>
<ol>
<li>The message carries an observable tell—bad grammar, an off sender, a malformed link.</li>
<li>The user perceives the tell.</li>
<li>The user acts correctly on it.</li>
</ol>
<p>This is a detection pipeline with a human as the final classifier. Each stage has a false-negative rate above zero. The stages are serially dependent, so the per-message miss rate is the product of three imperfect steps, evaluated thousands of times a year, against an adversary who only needs one pass. The expected number of successful attacks over any realistic time horizon converges on one. The math was never in the defender’s favor; it just looked survivable while attacks were cheap and crude.</p>
<h2>Detection is adversarial classification, and the attacker owns the inputs</h2>
<p>Strip away the human and the same problem shows up in the filter. Content-based email security—the deny-list posture every traditional gateway shares—is a classifier trying to separate malicious from benign on features extracted from the message: language patterns, known-bad URLs, sender reputation, structural heuristics.</p>
<p>In adversarial classification the attacker controls the input distribution. They can sample your decision boundary as many times as they like and ship only the variants that land on the benign side. Your false-negative rate isn’t a fixed property of the model; it’s whatever the attacker drives it to. Defense that operates on message content scales with attacker volume and attacker iteration speed. You don’t win that race. You stay in it.</p>
<p>Both the human and the filter were leaning on the same crutch: phishing used to be expensive to produce well, so most of it was produced badly. Broken grammar, generic framing, obvious bait. Those weren’t fundamental properties of an attack. They were artifacts of cost.</p>
<h2>AI removed the cost</h2>
<p>Producing a custom, well-written, context-aware phishing message used to require research, writing skill, and patience. That didn’t scale, so attackers ran volume instead—spray-and-pray against weak filters and weaker training. AI collapsed the marginal cost of a tailored attack to roughly zero.</p>
<p>The right mental model is a JIT compiler. Instead of precompiling a generic payload ahead of time, you generate the exact artifact at the moment it’s needed, specialized for the conditions in front of it. Phishing is now generated per-target, per-context, on demand.</p>
<p>Before writing this, I gave an off-the-shelf model a public LinkedIn excerpt, our company bio, and one public podcast quote, and asked it to write a phishing message targeting our CEO. 38 words of prompt. A few seconds to generate. The output had clean grammar, no manufactured urgency, and context pulled straight from public material: a recent conference appearance, the right vendor relationships, the right tone. No payload in the first message—just bait for a reply. The reply would carry a legitimate Calendly link, with the actual payload deferred to the booking confirmation.</p>
<p>Run that against the three-stage human pipeline. Stage one fails at the source: there is no tell. The grammar is correct, the tone is right, the context is accurate, the first message is benign by construction. Stages two and three never get a signal to act on. “Look closely” assumes there is something to look at.</p>
<h2>Authentication is not trust</h2>
<p>The one tell in that generated message was a homograph in the from address—a Unicode character visually identical to a Latin one, swapped into a look-alike domain.</p>
<p>That message can pass SPF, DKIM, and DMARC cleanly. Those protocols authenticate that a domain authorized the mail and that it wasn’t tampered with in transit. They say nothing about whether the domain is one the recipient should trust. An attacker who registers a look-alike domain and configures it correctly is fully authenticated. The green checkmarks are real. They’re just answering a different question than the one the user has.</p>
<p>This is the gap the whole industry papered over. We deployed authentication and treated it as a proxy for trust. It was never that. Authentication answers “did this domain really send this?” Trust answers “should this sender reach this person at all?” A model built on the first question can’t defend against an attacker who simply satisfies it.</p>
<h2>Default-deny is the only posture that survives</h2>
<p>If the user can’t be the control and content filtering is a race you stay in rather than win, the system has to make the trust decision, and it has to make it on something the attacker doesn’t control.</p>
<p>Network engineers already accepted this tradeoff decades ago. Default-allow firewalls (permit everything, enumerate badness) lost to default-deny: permit nothing, enumerate the known-good. Email security never made that move. The mailbox still defaults to delivering anything that reaches it and trying to scan out the bad afterward.</p>
<p>Zero trust inverts it. Nothing earns implicit access by arriving. A new sender is held at the boundary and resolved by an explicit decision before it ever competes for the user’s attention. Once a sender is trusted, that state persists, so the decision is made once and carried forward rather than re-litigated by a human on every message at 4:55 on a Friday.</p>
<p>The substitution is the whole point. Traditional filtering asks, on every message, whether the message looks malicious enough to block—a content question the attacker can tune against. Zero trust asks, once per sender: <em>should this sender get direct access to this user?</em>—a relationship question the attacker can’t satisfy by writing a better email. The trust boundary moves the decision off the feature space the attacker controls and onto one they don’t.</p>
<p>This is what <a href="https://mailprotector.com/products/shield/">Shield</a> is built around. Most email security chases the malicious set, which is unbounded and adversarial. Shield models the wanted set—the senders and patterns a user actually transacts with, which is small, stable, and not attacker-controlled—and lets everything outside it fall away by default.</p>
<h2>Awareness belongs at the decision, not in a quarterly module</h2>
<p>This doesn’t kill awareness. It moves it to the only place it’s useful: the moment of the actual decision, with the actual evidence.</p>
<p>When a borderline message does surface, the system should show its work—why this sender is unusual, why the relationship doesn’t fit, what the homograph in the from address actually is. The CEO message above passed every authentication check; a tool that can flag the homograph and explain it is doing the teaching that the annual module pretended to do. One of our partners, Zachary Kinder at <a href="https://net-tech.us/">Net-Tech Consulting</a>, called this “organic security awareness training.” Education bound to a real decision, instead of punishment after a manufactured failure.</p>
<h2>Why the broken model persists</h2>
<p>None of this is hard to see, which raises the obvious question: if user-detection was always fragile and AI has now broken it, why is the simulation category still growing?</p>
<p>Because the simulation produces artifacts and protection doesn’t. A click-rate trend, a completion record, a failure email—all documentable, all screenshot-able, all exactly what the questionnaire wants. A trust boundary that prevented an attack from reaching a user generates no comparable artifact. So the industry optimized for what’s measurable: a program that manufactures evidence of activity and, when the activity fails, routes blame to the user. A falling click rate mostly proves users are learning to recognize the tests.</p>
<h2>What I’d ask you to do</h2>
<p>You probably can’t drop sims tomorrow. The renewal still asks, the auditors still check. If a client requires the artifact, produce it.</p>
<p>But treat it as an artifact, not an architecture. Build the stack so protection happens at the trust boundary, upstream of the user, on inputs the attacker doesn’t control—and let the report be a byproduct you generate for compliance, not the thing your security actually rests on.</p>
<p>Before you send the next failure email, pull up the report that produced it and answer one honest question: what changed for that client because this report exists? If the answer is “nothing,” you already know what this post was for.</p>
<p>Don’t let the insurance questionnaire become the architecture. Give users fewer impossible decisions instead of more trick questions.</p>
]]></content>
    <category term="Phishing"/>
    <category term="Zero Trust"/>
  </entry>
  <entry>
    <title>How Microsoft 365 decides which tenant email belongs to</title>
    <link href="https://mailprotector.engineering/articles/how-microsoft-decides-which-tenant/"/>
    <id>https://mailprotector.engineering/articles/how-microsoft-decides-which-tenant/</id>
    <updated>2026-06-30T00:00:00.000Z</updated>
    <published>2026-06-30T00:00:00.000Z</published>
    <author><name>Mailprotector</name></author>
    <summary>A new mail-flow rollout made Microsoft 365 attribute every customer&apos;s mail to our tenant. The undocumented rule behind it, and the fix.</summary>
    <content type="html"><![CDATA[<h2>TL;DR</h2>
<p>We’ve been building a new outbound mail-routing design for <a href="https://mailprotector.com/products/shield/">Shield</a>, one that hands the
final leg of delivery back to each customer’s Microsoft 365 tenant. It relies on an Exchange
Online connector type called <strong>OnPremises</strong>, and we learned the hard way that for OnPremises
connectors, Exchange Online attributes traffic to a tenant by the TLS certificate the sender
presents, not by the message’s From or To addresses. Every Shield tenant relays through one
shared certificate, <code>courier.shield.security</code>. Its domain, <code>shield.security</code>, happened to be
an accepted domain in our own tenant. So the moment we cut our corporate tenant over to the
new flow, Microsoft started attributing every customer’s and partner’s relayed mail to us
and tripped its own abuse detection, blocking connectors and outbound. Rolling the change
back put customer and partner mail right within minutes, but Microsoft kept our own tenant’s
outbound relay blocked for 47 hours. The fix is the pattern Microsoft now prescribes: a
unique certificate identity per tenant.</p>
<h2>What we were building</h2>
<p>The stickiest part of deploying mail security in front of Microsoft 365 is almost always
DNS. Every record a partner has to touch to stand Shield up is another step that can go
wrong and another support ticket down the line. So the goal of this mail-flow design was to
get a Shield deployment down to as few moving parts as possible, and taking DNS changes off
the list entirely is a big win for our partners.</p>
<p>Outbound is what usually forces those changes. If we deliver filtered mail to the internet
ourselves, someone has to publish SPF and DKIM records authorizing <em>our</em> infrastructure to
send as <em>their</em> domain. That’s a support burden and a deliverability risk.</p>
<p>So we built an outbound path that hands the <strong>last mile back to Microsoft</strong>: filtered mail
is relayed <em>into</em> the customer’s own Microsoft 365 tenant, where Microsoft performs final
internet delivery, signing with the customer’s own DKIM keys and sending from the customer’s
own infrastructure. No DNS changes required to deploy.</p>
<p>The flow looks like this:</p>
<p><img src="https://mailprotector.engineering/diagrams/shield-mailflow.svg" alt="Outbound mail flow: a message goes from the sender to Microsoft 365, out to Shield for filtering, then is relayed back into the Microsoft 365 tenant over an OnPremises connector before Microsoft delivers it to the internet."></p>
<p>That “relay back into the tenant” leg requires an Exchange Online <strong>inbound connector</strong>,
and for it to be allowed to relay outbound to the internet, the connector must be of type
<strong>OnPremises</strong>.</p>
<h2>A quick primer on Exchange Online connectors</h2>
<p>Connectors are how Exchange Online decides whether to trust mail arriving from outside the
service, and what it’s allowed to do. Two things matter here.</p>
<p>The first is how the connector identifies incoming mail. An inbound connector can be
matched either by the sender’s IP address or by the TLS certificate the sending server
presents (the certificate’s subject/SAN domain). With <code>RestrictDomainsToCertificate</code>
turned on, the certificate becomes the authoritative identity for that connection.</p>
<p>The second is the connector <em>type</em>. A <strong>Partner</strong> connector represents another organization
or a cloud service. An <strong>OnPremises</strong> connector represents <em>your own</em> servers, the hybrid
“this is my on-premises mail environment” case. OnPremises is also what unlocks relaying
outbound to the internet <em>through</em> the tenant.</p>
<p>The docs don’t make the consequence obvious. For an OnPremises connector, the certificate
domain is an identity Exchange Online resolves against accepted domains, and accepted
domains are globally unique to a single tenant. So the certificate does more than
authenticate a connection; it identifies a tenant. The message envelope, who the mail is
from and who it’s to, plays no part in that attribution.</p>
<h2>The incident</h2>
<p>This was a careful rollout, not a flag flipped on a hunch. The new flow had run for weeks on
our staging tenant, then on the production tenant we use for demos. Both behaved perfectly,
inbound and outbound. So on a Wednesday morning we deployed the same change to our own
corporate tenant, <a href="http://mailprotector.com">mailprotector.com</a>, the one with real traffic, distribution groups, and the
better part of two decades of accumulated configuration. That was the natural last step.</p>
<p>It went sideways within the hour, in three escalating ways.</p>
<p>First, Microsoft blocked our inbound connector, the door their service uses to accept our
relayed mail. Their automated abuse detection had flagged the traffic as suspicious and blocked
the connector. We eventually found where they’d tucked the block away, cleared it, and mail
flowed again for a little while.</p>
<p>Then the worse one: Microsoft disabled outbound relay for our entire tenant. SMTP sending
was switched off at the tenant level, so we couldn’t get our own mail out the door at all.
That stayed stuck for a day or two while we waited on Microsoft to lift it.</p>
<p>The third symptom is the one that told us this was bigger than our tenant. Partners started getting
bounces about a “connector threshold exceeded.” Their mail, on their own tenants, was
failing because of something we’d changed on ours. Microsoft 365 is a multi-tenant service;
one tenant’s mess is not supposed to land in another tenant’s mail flow. The failures were
intermittent, too, which is the worst way for anything to break, because half-working is far
harder to diagnose than fully broken.</p>
<p>It didn’t help that Microsoft was having its own Exchange Online delivery problems that same
week, which nudged our first instinct in exactly the wrong direction. We rolled the new flow back instead. Within about fifteen minutes, partner mail
was flowing normally again, and Shield was fully operational. Our own outbound on Microsoft
365 stayed dark far longer: Microsoft kept it blocked for 47 hours, releasing it on their
schedule, not ours, and much of that stretch we spent unable to get a useful answer out of
their support, who left us in a queue for fourteen hours before emailing back.</p>
<h2>Root cause: attribution follows the certificate, not the envelope</h2>
<p>Every Shield tenant’s outbound relay runs through one shared component we call Courier, and
every Courier connection presents the same TLS certificate: <code>courier.shield.security</code>. One
name, for the whole fleet.</p>
<p>Separately, and this is the detail that had been harmless for years, <code>shield.security</code> was
an accepted domain on the <a href="http://mailprotector.com">mailprotector.com</a> tenant. We’d added it early in development. It
did nothing visible. It just sat in the accepted-domains list.</p>
<p>The new flow changed one more thing. It flipped the relay connector from type <strong>Partner</strong> to
type <strong>OnPremises</strong>, and OnPremises is exactly where attribution stops following the envelope
and starts following the certificate.</p>
<p>Put the three together and the trap springs. Mail arrives over an OnPremises connector
presenting <code>courier.shield.security</code>. Exchange Online resolves that certificate name against
accepted domains, finds <code>shield.security</code> sitting in the <a href="http://mailprotector.com">mailprotector.com</a> tenant, and
concludes the traffic is ours. Not just our traffic, but every customer’s and every
partner’s mail, because all of it rides the same Courier certificate. Our tenant became the
attribution sink for the entire fleet, Microsoft’s abuse detection saw one tenant apparently
firehosing the internet, and it started slamming doors.</p>
<p>That is why our staging and demo tenants came through clean. Neither had <code>shield.security</code>
in its accepted domains, so the certificate name resolved to nothing in particular and
attribution behaved. Only <a href="http://mailprotector.com">mailprotector.com</a> had all three pieces lined up at once: the
OnPremises connector, the shared certificate, and the matching accepted domain. We’d tested
everywhere except the one tenant where the latent setting lived.</p>
<p>Microsoft has since written this configuration up as an anti-pattern in a Message Center
notice, <strong>MC1226222</strong>: an OnPremises connector that authenticates with a certificate whose
subject/SAN is <em>not</em> an accepted domain of the tenant. That’s the shape of our incident
exactly. The notice landed in early 2026, after we’d already designed and built this
path. It’s admin-facing and not part of the public Microsoft Learn docs; the
canonical copy lives in each tenant’s Message Center, and it’s <a href="https://mc.merill.net/message/MC1226222">mirrored
publicly</a> for anyone without a tenant to check.</p>
<p>That same notice finally puts the attribution precedence in writing. For mail arriving over
an OnPremises connector, Exchange Online resolves the tenant in this order:</p>
<ol>
<li>the domain on the TLS certificate the sending server presents,</li>
<li>the P1 MailFrom (envelope sender) domain,</li>
<li>the P1 RcptTo (recipient) domain.</li>
</ol>
<p>The certificate sits at the top, above both envelope fields. Public Microsoft Learn docs
still don’t lay this order out, so the rule worth internalizing is the one we learned by
getting it wrong: for OnPremises relay, a certificate whose domain is an accepted domain in
some tenant attributes the mail to that tenant. Exchange Online treats the certificate
domain as the connector’s tenant identity, and so should you.</p>
<h2>The fix: one certificate identity per tenant</h2>
<p>Microsoft’s prescribed fix, the one MC1226222 now spells out, is to give <strong>each tenant its
own certificate</strong> whose subject/SAN domain is a <strong>verified, accepted domain in that specific
tenant</strong>. That makes attribution unambiguous by construction:</p>
<ul>
<li>Each tenant’s relay leg presents a distinct certificate.</li>
<li>That certificate’s domain is verified and accepted only in the one tenant it belongs to
(verified domains are globally unique), so Exchange Online can only attribute it there.</li>
<li>Attribution is isolated per customer, and so is any block that follows from it: one
customer’s volume, complaints, or abuse flag can’t bleed onto another, or onto us.</li>
<li>Microsoft 365 still DKIM-signs as the customer, because the customer’s real domains remain
their own accepted domains; the certificate domain is purely the connector’s identity.</li>
</ul>
<p>In our case the certificate domain is a per-tenant hostname, a generated string unique to
each customer, under a namespace we control. That lets us automate both certificate issuance
and the Microsoft 365 domain verification without any customer DNS changes. The connector’s
<code>TlsSenderCertificateName</code> is set to that per-tenant domain, and that same domain is
registered as an <strong>accepted domain</strong> in the tenant. One value, two places, and the identity
lines up end to end. The mechanism that swept the whole fleet into one tenant becomes the
control point we use to keep every tenant separate.</p>
<h2>Takeaways</h2>
<ol>
<li>For OnPremises connectors, the TLS certificate domain <em>is</em> the tenant identity.
Attribution doesn’t follow the message envelope, so design as if the certificate names
the tenant.</li>
<li>Never share one connector certificate across tenants you don’t want coupled. Even when it
appears to work, you’ve tied every customer’s deliverability to a single connector
identity Microsoft can act against all at once. Give each tenant its own.</li>
<li>Watch your <em>own</em> tenant. Our trap was that our product domain was an accepted domain in
the company’s own Microsoft 365 tenant, so the day that tenant ran an OnPremises connector
keyed to the shared certificate, it swept up the entire fleet’s mail.</li>
<li>The certificate’s subject/SAN has to be an accepted domain of the attributing tenant for
a healthy OnPremises relay. If it isn’t, you’re in the anti-pattern Microsoft now warns
about in MC1226222.</li>
<li>“It works in the demo” can hide a coupling bug. Our staging and demo tenants passed clean
because they lacked the one setting that mattered, which masked the problem right up to the
tenant that had it. When a design happens to work but rests on a shared global identity,
treat that as a risk, not a validation.</li>
</ol>
<p>We’re moving Shield to the per-tenant certificate model across the board. The automation
behind it, issuing and rotating a certificate per tenant and verifying each domain in
Microsoft 365 without customer DNS changes, is its own story, and one we’ll tell in a
follow-up.</p>
]]></content>
    <category term="Postmortem"/>
    <category term="Exchange"/>
    <category term="Microsoft 365"/>
    <category term="Deliverability"/>
  </entry>
  <entry>
    <title>HELO, world!</title>
    <link href="https://mailprotector.engineering/articles/helo-world/"/>
    <id>https://mailprotector.engineering/articles/helo-world/</id>
    <updated>2026-06-01T00:00:00.000Z</updated>
    <published>2026-06-01T00:00:00.000Z</published>
    <author><name>Ben Hathaway</name></author>
    <summary>Why an email security company is starting an engineering blog, and what we plan to put here.</summary>
    <content type="html"><![CDATA[<p>The first thing two mail servers say to each other is hello.</p>
<p>One opens a TCP connection on port 25, the other answers with a greeting, and then the client introduces itself with a single line:</p>
<pre><code>HELO mail.example.com
</code></pre>
<p>That command is as old as I am. It was specified in <a href="https://www.rfc-editor.org/info/rfc821/">RFC 821</a> in 1982, back when the entire list of hosts on the internet fit in a text file you could download. <code>HELO</code> is misspelled for the most boring reason imaginable: early SMTP commands were four characters, so <code>HELLO</code> lost a letter and never got it back. We’ve all been saying hello wrong to each other for more than forty years and the mail still gets through.</p>
<p>We liked that. A company that runs email infrastructure, naming its blog after the handshake that starts every message, with the same typo every engineer recognizes from their first <code>hello, world</code> program. So, HELO, world.</p>
<h2>Why bother</h2>
<p>Mailprotector has been doing email security for more than 25 years. In that time we’ve accumulated the kind of knowledge that doesn’t show up in product docs. We know why we don’t greylist, after watching it delay real mail and annoy users. We know mail moves only as fast as your slowest DNS lookup, that a perfectly valid message can sit in a queue for hours, and how attackers really probe a filter, which is rarely how it gets written up. We once lost a Thanksgiving to a single inefficient regex rule that slowed every system to a crawl.</p>
<p>Most of that lived in our heads, internal runbooks, and the memory of whoever was on call the night it broke. That’s a bad place for it. When a new engineer joins, they relearn it the slow way. When we solve something genuinely interesting, it stays inside the building.</p>
<p>This blog is where we move that knowledge into the open. Some of it will be useful to other people building email systems. Some of it will only matter to us, written down so we stop solving the same problem twice.</p>
<h2>What goes here</h2>
<p>A few kinds of posts, roughly:</p>
<p>Stories from the edge, where our servers meet the rest of the internet. Most of the interesting bugs in email live in the gap between what the RFCs say and what real senders actually do.</p>
<p>Deep dives on the parts of email that are easy to get wrong. SPF, DKIM, DMARC, TLS, the difference between a 4xx and a 5xx and why getting that wrong costs you real mail.</p>
<p>Notes on how we work. How we test a system whose whole job is to talk to untrusted strangers, how we ship without paging ourselves, and how we design security products people actually enjoy using.</p>
<p>The occasional honest postmortem. The ones where we caused the problem ourselves are usually the most instructive, so we’ll write those too.</p>
<p>We’re going to try to write the way we’d explain something to a colleague at the next desk. Specific, a little opinionated, with the actual commands and the actual numbers where we can share them.</p>
<h2>A small promise</h2>
<p>We’ll keep it real. No invented metrics to make a point land, no pretending a fix was clean when it took three tries. If a post says we lost a weekend to something dumb, we lost a weekend to something dumb.</p>
<p>The mail server is waiting for the next line. This is ours.</p>
]]></content>
    <category term="SMTP"/>
  </entry>
</feed>
