Mail spoofing: anyone can write your return address

SMTP never checks the return address, so anyone on the internet can send mail as your domain. I did exactly that to a client on a recent engagement, and they had no clue it was me. Here's how to test yours and what actually closes it (spoiler: not your mail server).

Marko ZivanovicAug 06, 20269 min read

01 The Big PictureFor leadership · no jargon

Okay, so last time we covered open mail relays: your server happily forwarding strangers' mail. This time the flip side, found on a recent engagement. I sent the client an email from their own domain, call it client.com, addressed to one of their people, from my own machine on a completely unrelated network. No credentials, no relay, no exploit. It landed in their inbox looking like it came from a colleague, and they had no clue it was me until I told them. This guide walks through why that works on basically every domain that hasn't done the homework, how to test yours, and the fix, which, awkwardly, mostly doesn't live on your mail server at all.

The root cause is ancient and structural: email was designed for a small, mutually trusting research network, and the protocol simply never checks who sent anything. Every letter has two addresses on it: the envelope sender, which machines look at, and the "From" line you actually see in your mail client. They can say completely different things, and nothing in the protocol minds. Think of the postal service: you can write any return address you like on an envelope, and the postman will deliver it, because nobody checks return addresses. Mail spoofing is writing "client.com" in the corner of your envelope and watching it sail through.

Now the bit that matters for the fix. An open relay was a problem on *your* server, so you could close it on your server. Spoofing is different: my server sent that mail, yours only received it, so there is nothing on your box that stops me writing your return address. What you can do is publish instructions in DNS, a guest list of servers genuinely allowed to send as you, a wax seal only your real mail carries, and a standing order telling every receiving server on the planet what to do when a letter fails those checks. That's SPF, DKIM and DMARC respectively, and the catch is that they only work if the *receiving* side bothers to enforce them. The good news: since Google, Yahoo and Microsoft made these records mandatory for bulk senders in 2024-2025, enforcement has become the norm, so the fix genuinely works now. The work is an afternoon of DNS records plus the slightly longer job of finding everything that legitimately sends mail as you before you start binning the rest.

02 Technical BreakdownFor engineers

An email has three "from" identities, and keeping them apart is 90% of understanding spoofing. The envelope sender (the MAIL FROM from the SMTP conversation, later visible as Return-Path) is what machines route bounces to. The header From: is what your mail client renders, and it's part of the message content, unchecked by the protocol. The third is the DKIM signing domain, of which more below. An attacker passes whatever checks exist on *his own* domain while writing your domain in the header, and your users see a colleague's name.

SPF (RFC 7208) is the guest list: a TXT record naming the IPs allowed to send for a domain. Its two famous flaws: it checks the envelope sender, not the header From: your users see (the RFC itself admits SPF-passing mail can contain other false identities, §11.2), and it breaks on forwarding, because the forwarder's IP isn't on your list. The record also has a hard budget of ten DNS lookups (§4.6.4), counted recursively across nested include: chains, and blowing it returns a permerror that DMARC treats as a failure. Admins discover this when a sixth vendor include silently kills the whole record. Ending qualifier debates (-all vs ~all, hard vs soft fail) are mostly moot once DMARC enforces, since both count as an SPF fail; the RFC 7489 §10.1 nuance is that some receivers reject on -all before DKIM even gets evaluated, which is why soft fail is the gentler choice during rollout and -all is the end state for domains that send nothing at all.

DKIM (RFC 6376) is the wax seal: the sending server signs selected headers and the body with a private key, and receivers verify against the public key you publish at <selector>._domainkey.yourdomain. Because the signature travels with the message, it survives forwarding, which makes it the resilient leg of the whole stack. Key sizes live in RFC 8301: 1024-bit minimum, 2048-bit recommended. The classic failure isn't weak crypto, it's third parties: SendGrid, Mailchimp and friends sign with *their own* domain by default (d=sendgrid.net and the like), so DKIM passes beautifully and still doesn't prove anything about your domain.

DMARC (RFC 7489) is where it comes together, and its actual contribution is alignment: the domain that passed SPF, or the domain that signed with DKIM, must match the header From: domain (relaxed mode accepts subdomains of the same organisational domain, strict demands an exact match). Then the policy says what failing mail deserves: p=none (deliver, just tell me), p=quarantine (spam folder), p=reject (bin it at the door). Plus rua reporting, so receivers send you daily XML digests of who sent mail as you and how it fared. One update worth knowing: as of 2026 DMARC has been republished as RFCs 9989-9991 ("DMARCbis"), which keeps records at v=DMARC1 but retires the pct percentage tag, so the old "quarantine 25% of failing mail" ramp no longer exists; quarantine *is* the soft landing now.

Here's the trap that fills r/sysadmin threads, and it's the one that got my client: spoofing a domain *to its own users* keeps working even when all three records exist. The reasons are all receiver-side. p=none enforces nothing, it's monitoring, not protection, and plenty of estates park there for years. On Microsoft 365, mail stamped SCL:-1 ("trusted") by an over-generous allow list or connector skips spam filtering *and* DMARC enforcement. And until recently there was Direct Send: an unauthenticated path into any M365 tenant's smart host, designed for printers, that attackers abused from May 2025 onward to send mail from your own accepted domain to your own users, bypassing filtering entirely. Varonis counted 70+ organisations hit with voicemail-themed QR-code phish; Microsoft's own guidance, and the fix, is Set-OrganizationConfig -RejectDirectSend $true. Microsoft's January 2026 threat intel report on exactly these campaigns recommends strict DMARC reject plus SPF hard fail, for what that's worth.

Two honest caveats before the commands. DMARC only covers exact-domain spoofing: lookalike domains (yourcornpany.com) and display-name tricks ("CEO Name <[email protected]>") are explicitly out of scope (RFC 7489 §2.4), and the defence there is user training plus impersonation rules, not DNS. And MTA-STS/TLS-RPT, which you'll see recommended alongside, are transport security for mail delivered *to* you; they do nothing about someone forging your From:. Worth having, different problem.

03 Affected

Every domain on the internet, sending or not. Domains with no DMARC record or p=none are spoofable today against receivers that enforce, and against sloppy receivers basically everyone is spoofable. The highest-risk setups: Microsoft 365 tenants with Direct Send still on (default for older tenants), anything sitting behind a third-party gateway that doesn't enforce DMARC or that stamps trusted SCL values, and estates with sprawling third-party senders where nobody dares leave p=none.

Parked and non-sending domains are affected too, and attackers love them because a spoofed invoice from a domain that "obviously belongs to the company" reads as legitimate. Cloud tenants get no free pass here: Google and Microsoft provide the signing and the enforcement, but the records are yours to publish, and M365 doesn't even enable DKIM signing for custom domains until you do it.

04 Detection

Before we change anything, let's see what we're dealing with. The static checks first:

$ dig +short TXT client.com

The SPF record. You want exactly one v=spf1 record ending in ~all or -all. ?all or +all means anyone may send as you (yes, +all authorises the entire internet, and yes, people publish it). Two SPF records on one domain is a permerror and fails everything.

$ dig +short TXT _dmarc.client.com

The DMARC record. p=quarantine or p=reject is protected; p=none is monitoring only; no answer at all means spoofable. Check sp= while you're here: without it, subdomains inherit p, which is usually what you want, but a forgotten sp=none undoes the lot.

$ dig +short TXT selector1._domainkey.client.com

DKIM lives under selector records, and selectors aren't enumerable: read the s= tag in the DKIM-Signature header of real outbound mail, or check your ESP's docs. selector1/selector2 is Microsoft 365, google is Google Workspace.

Then the live test, the one from the engagement. From your own box, on an unrelated network, send mail as their domain to an address you control at the client:

$ swaks --server mail.client.com --from [email protected] --to [email protected] --header "Subject: Q3 invoices" --body "can you approve this today"

No credentials, no relay, just their MX and a barefaced lie in the From:. If it lands in the inbox (mine did), spoofing is wide open. Their server never forwarded anything; it just believed the label, which is the whole difference from an open relay.

$ swaks --server gmail-smtp-in.l.google.com --from [email protected] --to [email protected]

The same spoof aimed at Gmail tests receiver-side enforcement: 550-5.7.26 ... rejected due to domain's DMARC policy means the records work. Delivered to inbox means they don't.

For bulk recon, Bishop Fox's Spoofy or the older mailspoof script will grade a domain's SPF/DMARC posture from DNS alone, and mxtoolbox does the same in a browser; dmarc-tester.com sends you a live spoof as proof. On M365, the spoof intelligence insight in the Defender portal lists every sender caught spoofing your domains in the last week, and compauth results in the X-Forefront-Antispam-Report header tell you how individual messages were judged. The finding itself, though, should come from a human sending the lie, like on the engagement above (hi again). One caveat before you report "critical, domain spoofable": a spoof landing in *junk* is a different conversation from one landing in the inbox, so note where it went.

05 Remediation

01 SPF: publish the guest list

One TXT record, naming everything that genuinely sends as you, nothing else:

v=spf1 include:spf.protection.outlook.com include:_spf.google.com -all

Substitute your actual senders. Keep the ten-lookup budget in mind: includes nest recursively, so six vendor includes can silently be fifteen lookups. Drop mx and a mechanisms for ip4: entries where you can; they cost lookups. ~all during rollout, -all once DMARC enforces (and -all immediately on domains that never send).

If marketing, the CRM and the ticketing system all send as you, consider moving bulk mail to a subdomain (news.client.com) with its own records: it keeps the main record under budget and quarantines the deliverability drama. Flattening services exist but go stale as vendors rotate IPs; a subdomain ages better.

02 DKIM: seal everything that legitimately sends

Microsoft 365: publish the two selector1/selector2 CNAMEs it gives you, then actually enable signing in the Defender portal, because it is off until you do. Google Workspace: generate a 2048-bit key in the admin console, publish the TXT record, then click "Start authentication"; forgetting that click is a classic.

Third parties are where this actually breaks. Each vendor must either sign with *your* domain (SendGrid domain authentication, Mailchimp's k1._domainkey CNAME, SES's three CNAMEs per identity) or send from a subdomain you delegate to them. A vendor whose DKIM d= is their own domain passes DKIM and fails DMARC alignment, which is the most confusing failure mode in the whole stack: SPF pass, DKIM pass, DMARC fail.

03 DMARC: the standing instruction

Start with monitoring, and point the reports somewhere you'll actually read them:

_dmarc.client.com TXT "v=DMARC1; p=none; rua=mailto:[email protected]"

Aggregate reports arrive daily as gzipped XML from every receiver that honours reporting. Raw XML is misery; feed it to parsedmarc plus a dashboard, or a free analyser (Postmark's weekly digest, Valimail Monitor). Two to four weeks of this maps every legitimate sender you forgot existed: the marketing platform, the invoicing SaaS, the website contact form, that one multifunction printer.

Then walk the policy up: p=quarantine, watch the reports for legitimate mail suddenly landing in junk, fix those senders' alignment, and finally p=reject. The old advice ramped with pct=25 increments; DMARCbis retired pct, so quarantine is the gradual step now. Set sp= deliberately (usually sp=reject at the end state), and note that failures appearing in your reports after enforcement is the system *working*, not a breach; every DMARC rollout thread on reddit has someone panicking at their own report data.

04 Microsoft 365: close the internal doors

Set-OrganizationConfig -RejectDirectSend $true

Kills the unauthenticated Direct Send path that the 2025 campaigns abused to spoof tenants to themselves. Devices that genuinely used it (printers, scanners, apps) move to authenticated SMTP on 587 or an IP-scoped inbound connector. If the cmdlet errors about org customisation, run Enable-OrganizationCustomization once first.

Then the trust audit: check the anti-phish policy honours DMARC ("Honor DMARC record policy when the message is detected as spoof", on by default in current tenants, worth confirming), and hunt for SCL:-1 stamps in transport rules, allow lists and inbound connectors, because "treat our own domain as trusted" quietly disables every check above. A transport rule that rejects or quarantines external mail whose From: domain is one of your accepted domains is a solid seatbelt while the DNS work settles; expect to carve out exceptions for phishing-simulation vendors and any partner that legitimately sends as you, and expect false positives while you find them.

05 Everyone else, and the parked domains

Google Workspace mostly aligns out of the box once the three records exist. On-prem Exchange and other gateways: the same inbound principle applies, reject unauthenticated mail claiming your own domains at the perimeter. And for domains that should never send mail at all (parked, acquired, defensive registrations): v=spf1 -all, a DMARC p=reject, and a null MX record (RFC 7505). Ten minutes, and they stop lending credibility to phish.

06 The extras, honestly labelled

MTA-STS (RFC 8461) with TLS-RPT (RFC 8460) enforces TLS for mail delivered *to* you and reports handshake failures: good hygiene, nothing to do with spoofing. BIMI puts your logo next to mail in Gmail, Yahoo and Apple Mail, but only once DMARC is at enforcement and you've bought a Verified Mark Certificate; it's the carrot for finishing the job, not a control in itself.

06 Verification

Records published? Prove the lie stopped working, the same way it was told:

$ swaks --server mail.client.com --from [email protected] --to [email protected]

The same test from Detection, re-run from outside. You want rejection or, at minimum, the junk folder with a spoof warning. Inbox placement of unauthenticated own-domain mail means something inbound still trusts it: SCL:-1 stamps, a gateway, Direct Send.

$ dig +short TXT _dmarc.client.com

p=reject (or p=quarantine mid-rollout), sp= set deliberately, rua pointing at a mailbox someone reads.

Then read the reports for a couple of weeks. Failing entries from infrastructure you don't recognise, being rejected by receivers, is the sound of it working. Legitimate senders showing up as failures means you're not done aligning. On M365, compauth=pass in message headers and an empty-ish spoof intelligence insight are the internal equivalents. And if you want an external opinion, a phishing-simulation vendor's domain spoof test (KnowBe4's is the well-known one) will happily try to spoof you and report where the mail landed.

07 Rollback

The realistic rollback is legitimate mail dying at p=reject: the forgotten invoicing SaaS, the marketing platform somebody signed up for with a company card, the printer. Fix forward where possible: align the sender properly (vendor DKIM on your domain or a delegated subdomain), because that's the config you wanted all along. If the business is bleeding while you sort it, drop to p=quarantine, not p=none: quarantine keeps failing mail out of the inbox while you work, and since DMARCbis removed pct, that policy step *is* your partial rollout.

Rolling SPF back from -all to ~all is harmless and buys slack for forwarded mail during the transition. What you shouldn't do is rip the records out or bounce back to p=none indefinitely to make the tickets stop; that's how estates end up in the "monitored since 2019, protected never" thread. Future you, reading spoofed invoices addressed to the FD, will have opinions.

08 References

If I've got anything wrong here, or you've hit an edge case I haven't covered, do say: corrections and questions are always welcome :)

exit(0);

// Patched, or stuck?

Ship the fix. Prove it’s closed.

If this runbook doesn’t fit your stack, send the CVE — an operator writes a verified path, usually within a business day. Or contribute your own and get co-authored credit.

// Dispatch

New guides, straight to your inbox.