← Back to Blog

Two Checks Passed. The Message Was Still Forged.

22 September 2026 Security Operations 9 min read
ONE MESSAGE, THREE DOMAINS, TWO PASSES, NO PROTECTION Each check answers a question about one domain. Only alignment asks whether that domain is the one the reader saw. ENVELOPE SENDER bounce@mail-partner-cdn.net what SPF was checked against SIGNING DOMAIN d=mail-partner-cdn.net what DKIM was checked against VISIBLE FROM accounts@northgateeng.com the only one the reader sees spf=pass (for mail-partner-cdn.net) dkim=pass (for mail-partner-cdn.net) ALIGNMENT: does the passing domain match northgateeng.com? SPF domain: no. DKIM domain: no. Neither pass counts for the From. dmarc=fail action=quarantine header.from=northgateeng.com Either an attacker who found any signing domain will do, or your own marketing platform set up exactly as its vendor said. The results cannot tell you which. The domains can.

Two green results and a quarantined message. The results were correct. They were about the wrong domain.

An analyst pulls the header from a message that landed in a finance mailbox claiming to be from the company's own accounts team. The first line of the authentication results says spf=pass. The next says dkim=pass. The message was quarantined anyway, and the ticket asks why the filter is blocking legitimate internal mail.

It is not internal, it is not legitimate, and the filter did exactly what it was told. The two passes are real. They just belong to somebody else.

What a pass is actually about

Every authentication result in a mail header is a statement about one specific domain, and the domain is written next to the result. SPF answers whether the connecting server was authorized to send for the envelope domain, the address the sending system used for bounces, which the reader never sees. DKIM answers whether the message carries a valid signature from the signing domain, whichever domain the sending system chose to sign with. Neither check looks at the From line.

Here is the header from the ticket, in the format Exchange Online actually stamps rather than the tidy version in the RFC:

Authentication-Results: spf=pass (sender IP is 198.51.100.24)
 smtp.mailfrom=bounce.mail-partner-cdn.net; dkim=pass (signature was verified)
 header.d=mail-partner-cdn.net; dmarc=fail action=quarantine
 header.from=northgateeng.com; compauth=fail reason=000

The format is worth a sentence, because it is the one you will actually meet. Exchange Online omits the authserv-id that the RFC puts first, carries a parenthetical reason beside each verdict, and appends its own compauth composite at the end. A header that opens with a server name and has no compauth came from somewhere else, which matters when you are deciding whose evaluation you are reading.

Read the domains, not the verdicts. SPF passed for bounce.mail-partner-cdn.net. DKIM passed for mail-partner-cdn.net. The From line, the only domain a person in finance will ever see, is northgateeng.com. Neither pass was issued to it.

DMARC is the check that asks the question the other two skip: does either passing domain match the visible one? That comparison is alignment, and here both sides fail it. So dmarc=fail, and because the domain publishes a policy that asks receivers to act on failure, the message went to junk.

Now hold two explanations against that header, because you cannot separate them from the results alone. This is either an attacker who has learned that a passing signature from any domain is cheap, or the organization's own marketing platform configured exactly as its vendor's setup guide said, quietly sending as the platform rather than as the company. The header looks identical in both cases. Which one you are looking at is a question about your own sender inventory, and that is the part most estates have never written down.

Numbers to compare against your own estate. A domain with correct signing across its senders sits well above 90 percent aligned in its DMARC aggregate reports; the residual is mostly forwarding, and forwarding costs roughly 2 percent of legitimate mail once a policy is enforced, since a forwarded message keeps its signature and loses its envelope. An SPF record is evaluated against a hard limit of 10 DNS lookups, and a record at 11 returns a permanent error for every sender at once with no error message anywhere you will see it. Under the 2026 revision of DMARC, RFC 9989, the pct= tag is removed and ignored: a receiver applying it is running an old implementation, and a rollout plan built on it has no mechanism underneath.

Find the same shape in your own telemetry

The header above is one message. The same pattern, a pass that belongs to a domain other than the From, is visible across a whole tenant in Defender's EmailEvents table, which carries the authentication verdicts as a JSON string per message. This query pulls out inbound mail that displays your domain, passed at least one check, and still failed DMARC. Every row is either an impersonation or one of your own senders that nobody got signing as you.

// Table: EmailEvents (Microsoft Defender for Office 365, Advanced Hunting / Sentinel)
// Hypothesis: mail displaying our domain that passed SPF or DKIM for SOME domain
// and still failed DMARC is either a forgery or an unaligned sender of ours.
// Replace northgateeng.com with the domain you own.
EmailEvents
| where Timestamp > ago(30d)
| where EmailDirection == "Inbound"
| where SenderFromDomain =~ "northgateeng.com"
| extend Auth = parse_json(AuthenticationDetails)
| extend SPF = tostring(Auth.SPF), DKIM = tostring(Auth.DKIM), DMARC = tostring(Auth.DMARC)
| where DMARC =~ "fail" and (SPF =~ "pass" or DKIM =~ "pass")
| summarize Messages = count(),
            Recipients = dcount(RecipientEmailAddress),
            FirstSeen = min(Timestamp), LastSeen = max(Timestamp),
            Delivered = countif(DeliveryAction == "Delivered"),
            Junked = countif(DeliveryAction == "Junked"),
            SampleSubject = any(Subject)
    by SenderIPv4, SenderMailFromDomain, SPF, DKIM
| order by Messages desc
// Read the rows in two groups. A source with steady weekday volume and an
// envelope domain you recognize as a platform you pay for is YOUR sender,
// authenticating as the platform: a ticket to get it signing as you.
// A source with no history, high volume, and recipients you have never
// written to is somebody else's campaign. Delivered > 0 on that row means
// your policy is still at none.

Two things about the query are deliberate. It filters on SenderFromDomain, the visible From, rather than on SenderMailFromDomain, because the question is what the reader saw, and grouping by the envelope domain afterwards is what separates a platform you pay for from a stranger. And it keeps rows where either check passed, because a forgery that passes nothing is already caught by every filter on earth; the interesting population is the one that passed something and still failed to be you.

The Delivered column is the one to read first. If every row shows delivered and nothing junked, the domain's DMARC policy is p=none, which asks receivers to report failures and take no action. That is monitoring, and it protects nobody; the forged invoice arrives exactly as the real one does.

The same read without a SIEM

Most of what governs this is in public DNS, so the posture of any domain, yours or a supplier's, is readable in about ninety seconds with nothing but a resolver. This checks the three records that matter, and counts the one thing that breaks silently.

# Any domain, any machine. Replace the domain.
d=northgateeng.com

# 1. Is there a policy, and does it ask receivers to act?  p=none means no.
dig +short TXT _dmarc.$d

# 2. The SPF record, and how many DNS lookups it spends against the limit of 10.
#    include:, a, mx, redirect= and exists: each cost one; ip4:/ip6: cost nothing.
dig +short TXT $d | grep -i v=spf1
dig +short TXT $d | grep -i v=spf1 | grep -oE '(include:|redirect=|\ba\b|\bmx\b|exists:)' | wc -l

# 3. Does an invented subdomain inherit anything?  Nothing back means the receiver
#    walks up to the organizational record; check that record carries np= or sp=.
dig +short TXT _dmarc.zq7x-not-a-name.$d

The third query is the one people skip. A name that has never existed in your zone returns nothing, so the receiver walks up to the organizational record, and under RFC 9989 that record's np= tag is what governs it. A record with no np= and no sp= leaves every invented subdomain, invoices., payments., hr., at whatever the apex policy is, and an apex at p=none means an attacker can pick any name under your domain and be delivered.

The lookup count is the number to write down. Eight is a conversation with a supplier; ten is the ceiling; eleven is a permanent error that fails every sender you have, and the first sign of it is usually a customer asking why your invoices are in junk.

On the Microsoft side, one more check answers whether your own platform is signing as you or as itself. Exchange Online rotates between two selectors on its own schedule, and a tenant that published only one CNAME signs perfectly until the rotation and then fails totally, with no change on the administrator's side.

# Exchange Online PowerShell. Shows both selector CNAMEs the tenant expects to exist.
# If only one of these is published in your DNS, you are one rotation from a
# total DKIM failure that will look, in the reports, like every sender broke at once.
Connect-ExchangeOnline
Get-DkimSigningConfig -Identity northgateeng.com |
    Select-Object Domain, Enabled, Selector1CNAME, Selector2CNAME, RotateOnDate

# Then confirm both names resolve from outside the tenant:
Resolve-DnsName -Type CNAME selector1._domainkey.northgateeng.com
Resolve-DnsName -Type CNAME selector2._domainkey.northgateeng.com

The detection for the other direction

Everything above is about mail claiming your domain. The mirror case is a forged message that authenticates flawlessly, because the attacker registered a lookalike domain and configured SPF, DKIM and DMARC on it correctly. Three passes, all real, all for a domain the reader will misread. No authentication mechanism detects that, by design; the passes are honest. The detection is on the display name and the domain edit distance, and it belongs in a Sigma rule rather than a policy.

title: Inbound Mail Impersonating Internal Display Name From External Domain
id: 3f7a2c1e-8b4d-4e6a-9c2f-1d5e7b8a9c0d
status: experimental
description: External sender using the display name of an internal executive or finance role. Field names follow the Defender XDR EmailEvents schema; map them to your backend.
references:
    - https://attack.mitre.org/techniques/T1656/
    - https://attack.mitre.org/techniques/T1566/
logsource:
    product: m365
    service: exchange
detection:
    selection_name:
        SenderDisplayName|contains:
            - 'Rachel Okafor'
            - 'Accounts Payable'
            - 'Finance Team'
    filter_internal:
        SenderFromDomain: 'northgateeng.com'
    condition: selection_name and not filter_internal
falsepositives:
    - Staff mailing from a personal address, which is itself worth a conversation
    - A partner whose staff share a name with yours
level: medium

The names in the rule are the ones an attacker would use against you, which means they are the ones on your website's leadership page and the two or three role mailboxes that approve money. Five is about right; twenty makes the rule fire on every partner whose finance team shares a common name, and the rule stops being read.

Read the two detections as a pair. The KQL query above finds mail that fails to be you. This rule finds mail that succeeds at looking like you while being, in every technical sense, honestly somebody else. Authentication closes the first box completely once a policy is enforced. It never touched the second, and an organization that credits it with both is the one that gets surprised.

The sentence that keeps the control honest. An enforced DMARC policy means nobody can send mail claiming to be from your exact domain and have it delivered by a receiver that participates. Lookalike domains, display-name forgery, and mail from a compromised account of yours are all outside that sentence, and a phish from any of them is not a failure of the policy. Say the boundary out loud before the incident, because the day after it, the policy will be blamed for the thing it never claimed.

What to do this week

  1. Run the KQL above against your tenant for the last 30 days and sort the rows into two piles: senders you pay for that are authenticating as themselves, and sources you cannot name. The first pile is tickets; the second is the reason the policy exists.
  2. Count your SPF lookups with the one-liner and write the number down with today's date. At eight or above, find the two terms authorizing hosts that do not send mail (a and mx are the usual suspects) and remove them before a supplier's restructure pushes you over.
  3. Publish both DKIM selector CNAMEs, and confirm both resolve from outside the tenant. If you can only find one, you have not had a rotation yet; you will.
  4. Read the policy tag on your own _dmarc record. If it says p=none and has for more than a quarter, you have the reports and none of the protection, and the KQL results from step one are the sender inventory that makes moving it safe.
  5. Deploy the Sigma rule with your own five names in it, then run the ATT&CK T1656 impersonation technique through your mail flow from an external free-mail account. If the message lands in an inbox with no warning banner, the display-name gap is open regardless of what your DMARC policy says.

A pass is a fact about a domain. Before it means anything, ask which domain it was issued to, and whether that is the one the person reading the message believed they were hearing from. Most of email authentication is that one question, asked carefully, three times.

Ridgeline Cyber Defence Written by security professionals. Published weekly on Tuesdays.

Related Articles

30 April 2026

The First 15 Minutes of a BEC Investigation Determine Everything That Follows

BEC investigation: the queries and evidence sources you check in the first 15 minutes determine whether you catch the at

27 May 2026

Vulnerability Exploitation Just Overtook Credential Theft as the #1 Breach Vector. But Is It Really This Bad?

DBIR 2026: 31% of breaches start with exploitation, credentials dropped to 13%. What this means for your detection prior

20 May 2026

Credential Access Detection Beyond LSASS, The Five Techniques Your Rules Are Missing

LSASS dump detection is table stakes. Kerberoasting, DCSync, DPAPI abuse, SAM extraction, and token theft each need diff