Design an email security gateway
Design the inbound mail filter that sits in front of 100,000 mailboxes: MX cut-over, SPF, DKIM and DMARC evaluation with alignment, URL rewriting with time-of-click checks, attachment sandboxing sized in VMs, quarantine, and a false-positive process users trust.
01Understand the problem and size it
- Place the gateway in the mail flow with MX records and lock the mailbox provider so it cannot be bypassed
- Evaluate SPF, DKIM and DMARC correctly, including alignment with the visible From domain
- Design URL rewriting with time-of-click checks and attachment sandboxing, sized in messages per second and sandbox VMs
- Run quarantine and false-positive handling without allow-lists that attackers can ride
What they probe: why SPF pass does not mean the sender is who the From line says, what happens to mail sent straight to the mailbox provider, how long sandboxing delays mail, why URLs are rewritten, how a user gets a wrongly quarantined invoice back. Where candidates slip: treating SPF as anti-spoofing, bouncing spam after acceptance, allow-listing by sender address, no plan for links weaponised after delivery.
An email security gateway (SEG) is an SMTP relay that receives a company's inbound mail first, inspects it, and forwards what is safe to the mailbox provider (Exchange Online, Google Workspace). The company points its MX record (the DNS record that tells senders where to deliver mail for a domain) at the gateway.
Functional requirements
- Receive all inbound mail for the company's domains over SMTP and deliver clean mail to the mailbox provider.
- Authenticate senders (SPF, DKIM, DMARC, ARC) and detect spoofing, lookalike domains and business email compromise (BEC).
- Rewrite URLs for time-of-click checks and sandbox unknown attachments.
- Quarantine suspicious mail with user and admin release, and remediate mail found malicious after delivery.
Non-functional requirements
- 100,000 mailboxes; added delivery latency < 30 s p95 for mail without attachments needing detonation.
- Never lose an accepted message: once we answer
250 OK, we own it. - Phishing catch rate high, but false positive rate on business mail < 0.01 %.
- Scale with bursts (Monday 9 a.m., marketing blasts).
Back of the envelope
mailboxes 100,000
inbound per mailbox 100 messages / day (incl. spam) = 10 M messages / day
average 10 M / 86,400 s ≈ 116 messages / s
peak (x5) ≈ 580 messages / s
average size 75 kB -> 750 GB / day, ~70 MB/s at peak
messages with attachments 20 % = 2 M / day
unknown after hash cache + static AV 5 % of those = 100,000 detonations / day
detonation rate 100,000 / 86,400 ≈ 1.16 / s
sandbox time 180 s per sample
concurrent VMs 1.16 x 180 ≈ 208 on average, ~1,040 at peak (x5)
spool (durable queue) 1 h of peak backlog: 580 x 3,600 x 75 kB ≈ 157 GB02High-level design: the inbound pipeline
- EDGE
- SERVICE
- DATA
- STREAM
- THREAT
- Live flow
- Lookup / async
- Connection: TLS (STARTTLS), connecting IP reputation and DNS blocklists, per-IP connection and rate limits. Reject here with a
5xxand the sender's own server generates any bounce. - Envelope: valid recipient check (
RCPT TOagainst the directory, so mail to non-existent users is refused, not bounced later), size limits. - Spool: write the full message to a replicated durable queue and only then answer
250 OK. - Authentication: SPF, DKIM, DMARC, ARC; results stamped into an
Authentication-Resultsheader. - Content and threat: spam and phishing classifiers, BEC and impersonation checks, URL analysis and rewriting, attachment static analysis, sandbox for unknowns.
- Policy and delivery: deliver, deliver with banner, quarantine, or drop; relay to the mailbox provider over TLS on a connector that accepts only our IPs.
SPF, DKIM, DMARC: what each one proves
| Check | Standard | Proves | Does not prove |
|---|---|---|---|
| SPF | RFC 7208 | The connecting IP is authorised to send for the envelope sender domain (MAIL FROM, or HELO) | Anything about the visible From header; breaks on forwarding; max 10 DNS lookups |
| DKIM | RFC 6376 | The signing domain (d=) signed these headers and body, unchanged in transit; key at selector._domainkey.domain | That d= matches the From header |
| DMARC | RFC 7489 | The visible From domain aligns with a passing SPF or DKIM domain; tells you the owner's policy (p=none/quarantine/reject) | That the sender is honest; a lookalike domain can pass its own DMARC |
| ARC | RFC 8617 | A chain of signed authentication results from intermediaries (mailing lists, forwarders) | Anything if you do not trust the sealing intermediary |
; DNS published by the sender domain
example.com. TXT "v=spf1 include:_spf.google.com ip4:203.0.113.0/24 -all"
s1._domainkey.example.com. TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqh...IDAQAB"
_dmarc.example.com. TXT "v=DMARC1; p=reject; rua=mailto:dmarc@example.com; adkim=r; aspf=r"
; Header our gateway adds after evaluation (RFC 8601)
Authentication-Results: mx1.gw.cyart.io;
spf=pass smtp.mailfrom=bounce.example.com;
dkim=pass header.d=example.com header.s=s1;
dmarc=pass (p=reject) header.from=example.com
; A spoof that passes SPF but fails DMARC
MAIL FROM:<x@attacker.net> -> spf=pass smtp.mailfrom=attacker.net
From: "CEO" <ceo@cyart.io> -> dkim=none, no aligned domain
-> dmarc=fail (p=reject) header.from=cyart.ioAlignment is the key idea: with relaxed alignment (aspf=r, adkim=r), bounce.example.com aligns with example.com because they share an organisational domain. The attacker in the last example passes SPF for their own domain, but nothing aligns with cyart.io, so DMARC fails and the domain owner's p=reject tells us to refuse it.
03Deep dives: URLs, attachments, quarantine
URL rewriting and time-of-click
Attackers send links that are clean at delivery and weaponise them hours later, after the gateway has scanned them. So the gateway rewrites every URL to point at its own click service, which checks the destination when the user clicks: reputation, a fresh crawl in an isolated browser, lookalike detection, and the policy for that user.
original : https://docs.vendor-invoices.com/inv/8841
rewritten: https://click.gw.cyart.io/v2/r?u=aHR0cHM6Ly9kb2NzLnZlbmRvci1pbnZvaWNlcy5jb20vaW52Lzg4NDE
&m=4f2a9c... (message id) &r=jdoe &sig=HMAC-SHA256(key, u|m|r)[:16 bytes, base64url]
click service on GET:
1. verify sig; reject if invalid -> never an open redirect
2. decode u, re-check verdict (cache 5 min), detonate in isolated browser if unknown
3. clean -> 302 to original
unknown -> interstitial warning or open in remote browser isolation
bad -> block page, log click by (message, recipient), alert SOCIf the click service redirects to any u= parameter, attackers use your trusted domain to launder their phishing links. Sign the URL together with the message and recipient and refuse anything that does not verify.
Attachment sandboxing
- Cheap first: hash lookup against known good and known bad, then static analysis (file type by magic bytes, macros, embedded objects, archive contents). Most attachments never reach the sandbox.
- Detonate unknowns in instrumented VMs (Windows with Office, a PDF reader, a browser) for about 3 minutes, watching for process launches, network calls and file drops. Sized above: around 200 VMs on average, 1,000 at peak, so autoscale and cap per tenant.
- Delay trade-off: holding the whole message for 3 minutes annoys users. Dynamic delivery sends the body immediately with a placeholder and attaches the file once clean.
- Password-protected archives cannot be inspected; try passwords found in the message body, otherwise quarantine with a banner or block by policy.
Quarantine and post-delivery remediation
- Quarantine is a store keyed by message, with the verdict, reasons and the original raw message, kept 30 days.
- Users get a daily digest and can release low-risk categories (bulk, spam); phishing and malware need admin release.
- When a verdict changes after delivery (a URL turns bad, a sandbox result arrives late, a user reports phishing), the gateway pulls the message from mailboxes through the provider's API and records who had opened it or clicked.
04False positives, failure modes and trade-offs
False positives without weakening the filter
- Allow-list entries must require authentication: "allow vendor.com when DMARC passes", never "allow From: vendor.com", which any spoofer can write.
- Release requests and "not junk" reports feed a review queue and the classifiers, with per-tenant thresholds.
- Track false positives per rule and per model version; a rule that quarantines invoices from one supplier gets fixed, not allow-listed globally.
- Show users why in the quarantine view ("external sender impersonating an executive") so they can judge.
If you accept a message and later bounce it, the bounce goes to the forged envelope sender: an innocent third party. That is backscatter, and it gets your gateway blocklisted. Decide spam at SMTP time with a 5xx, or accept and quarantine silently.
Failure modes
| Failure | Behaviour | Why |
|---|---|---|
| Gateway region down | Multiple MX records with priorities across regions; senders retry for days | SMTP's retry is a built-in buffer |
| Mailbox provider unreachable | Spool holds mail and retries; alert at 15 min backlog | We already said 250 OK |
| Sandbox saturated | Dynamic delivery holds attachments longer; never release unscanned files | Fail safe on the dangerous part only |
| Direct-to-provider delivery | Provider connector accepts only gateway IPs or a certificate | Otherwise attackers skip the gateway via the provider's own MX host |
| DNS timeouts on SPF/DKIM | Result temperror; do not treat as fail or pass; score as neutral | Avoid quarantining good mail on resolver blips |
Trade-offs to state
| Choice | Gain | Cost |
|---|---|---|
| Gateway in front (MX) vs API-based scanning inside the mailbox | Blocks before delivery, works with any provider | Extra hop, MX change, bypass risk if the provider is not locked down |
| URL rewriting | Time-of-click protection, click telemetry | Breaks some links and signatures, users see odd URLs |
| Dynamic delivery | Fast mail flow | Users see placeholders, more complexity |
| Strict DMARC enforcement | Kills exact-domain spoofing | Misconfigured legitimate senders get rejected; mitigate with ARC and per-sender overrides that still need DKIM |
05Final design, follow-ups and self-test
"The MX points at the gateway in two regions and the mailbox provider only accepts mail from us, so there's no bypass. We reject cheaply in the SMTP session, spool durably before saying 250, then authenticate: SPF only covers the envelope sender, so the real anti-spoofing check is DMARC alignment against the visible From. Links are rewritten to a signed click service because they get weaponised after delivery, and unknown attachments go to a sandbox, about 200 VMs on average for 100k mailboxes, with dynamic delivery so the body isn't held. Quarantine has user release for spam, admin release for phishing, and verdicts that change later trigger a pull from mailboxes."
Q1How do you handle mail from a mailing list that breaks DKIM by adding a footer?+
Trust ARC seals from known intermediaries; the list's ARC chain shows DKIM passed on arrival. Without a trusted ARC chain, treat it as a DMARC failure under the owner's policy.
Q2How do you catch BEC that has no link and no attachment?+
Impersonation features: display name matches an executive but the domain is external, lookalike domains (edit distance, homoglyphs), first-time sender, reply-to differs from From, urgency and payment language. Add a banner or quarantine.
Q3A user reports a phish that reached 400 inboxes. What happens?+
The report opens a case, the verdict updates, the gateway searches for the same sender, URLs and hashes across the tenant and pulls all copies via the provider API, then lists who clicked from the click logs.
Q4Why not just run the sandbox on every attachment?+
Cost and latency: 2 M attachments a day at 3 minutes each is about 4,200 concurrent VMs on average. Hash and static filtering remove most of them first.
A message has `MAIL FROM:<x@attacker.net>` (SPF passes for attacker.net) and `From: ceo@cyart.io`, with no DKIM signature. cyart.io publishes `p=reject`. What should the gateway do?