DMARC, SPF, and DKIM: The Complete Email Authentication Guide for Businesses
If someone can send emails pretending to be your company, your reputation and clients are at risk. Learn how to configure DMARC, SPF, and DKIM correctly.
The problem: anyone can send email as your company
SMTP has no sender authentication. It was designed in 1982, for a network where everyone knew everyone. The `From:` field your mail client displays is free text — anyone can put anything in it.
An attacker can send your customers an invoice that appears to come from `billing@yourcompany.com`, with their own bank details on it. With no email authentication in place, nothing technical prevents this.
The consequences are specific:
- Invoice fraud. A customer pays a real invoice for a real amount into an account that isn't yours. It surfaces weeks later, when you chase the payment.
- Internal impersonation. Someone in finance gets an urgent, confidential wire request "from the CEO".
- Deliverability. Your legitimate mail lands in spam, because filters can't tell it apart from the mail impersonating you.
- Domain reputation. Your domain shows up on blocklists for spam somebody else sent in your name.
The three protocols, and how they relate
SPF, DKIM and DMARC are almost always explained as three separate things. They aren't: SPF and DKIM are verification mechanisms, and DMARC is the policy that decides what the result means. Without DMARC, SPF and DKIM produce a verdict nobody is obliged to act on.
SPF — which servers may send as you
SPF publishes, in a DNS TXT record, the list of servers authorised to send mail for your domain.
For a Microsoft 365 tenant the minimum record is:
```
v=spf1 include:spf.protection.outlook.com -all
```
The final qualifier is the part most people get wrong:
| Qualifier | Meaning | Effect |
|---|---|---|
| `-all` | Hard fail | Reject anything unauthorised |
| `~all` | Soft fail | Mark suspicious, deliver anyway |
| `?all` | Neutral | Says nothing. Equivalent to no SPF |
| `+all` | Pass all | Authorises the entire internet to impersonate you |
`~all` is the default in many guides and the value we find most often. Its real effect depends on the receiver — some treat it as a failure, others ignore it. Once DMARC is at `p=reject` the qualifier matters less, but until then `~all` is an open door.
#### The 10-lookup limit: the most common silent failure
This is the error that breaks SPF without anyone noticing. RFC 7208 §4.6.4 caps evaluation of an SPF record at 10 mechanisms that require a DNS lookup. These count:
`include:` · `a` · `mx` · `ptr` · `exists:` · and each level's `redirect=`.
These don't: `ip4:`, `ip6:`, `all`.
The trap is that the limit is recursive. An `include:` doesn't cost one lookup — it costs one lookup plus everything that record includes in turn. A single marketing-platform `include:` can consume four or five on its own.
A record that looks perfectly reasonable:
```
v=spf1 include:spf.protection.outlook.com include:_spf.google.com
include:servers.mcsv.net include:sendgrid.net
include:_spf.salesforce.com mx a -all
```
…blows the limit comfortably. When it does, the evaluation result isn't "fail" — it's `permerror`. That distinction is the important part: most receivers treat `permerror` as SPF not verified rather than SPF failed. Your legitimate mail starts failing DMARC alignment intermittently, differently per receiver, and the record looks completely fine to the eye.
How to fix it:
1. Remove `include:` entries for services you no longer use. Most common cause, cheapest fix.
2. Replace `include:` with `ip4:`/`ip6:` where the provider publishes stable ranges. Those cost no lookups.
3. Drop `mx` and `a` if an `include:` already covers your mail servers.
4. Never use `ptr`. RFC 7208 formally discourages it.
DKIM — that the message wasn't altered, and that it came from you
DKIM cryptographically signs each message. The private key lives on your mail server; the public key is published in DNS. The receiver verifies the signature.
Microsoft 365 publishes it as two CNAMEs per domain:
```
selector1._domainkey.yourdomain.com
selector2._domainkey.yourdomain.com
```
Two, because Microsoft rotates keys by alternating between the selectors. Publish only one and signing breaks at the first rotation.
The detail almost nobody mentions: Microsoft 365 signs all outbound mail from day one — but for a custom domain it signs with the tenant's `onmicrosoft.com` domain until you explicitly enable DKIM. That signature is valid, and it does not align with your domain, so it contributes nothing to DMARC. A freshly configured tenant has DKIM "working" and still gets no DMARC benefit from it.
DMARC — the policy, and the reports
DMARC publishes at `_dmarc.yourdomain.com` what a receiver should do when a message doesn't authenticate, and where to send you reports.
```
v=DMARC1; p=reject; rua=mailto:dmarc@yourcompany.com; sp=reject; adkim=s; aspf=s
```
Three possible policies:
1. `p=none` — do nothing, just report. A starting point, not a destination.
2. `p=quarantine` — send unauthenticated mail to spam.
3. `p=reject` — refuse unauthenticated mail at the server.
`p=none` protects nothing at all. It's the equivalent of installing cameras and never watching them. It's also the most common state we find: the domain publishes DMARC, the administrator considers it done, and impersonation remains exactly as possible as before.
Alignment: why your mail passes SPF and still fails DMARC
This is the section missing from almost every guide, and the reason `p=reject` migrations break legitimate mail.
DMARC doesn't only check that SPF or DKIM passed. It checks that the domain that passed matches the `From:` domain the user actually sees. That's alignment.
A message has two different senders:
- The `Return-Path` (or `MAIL FROM`), which is what SPF is evaluated against. The user never sees it.
- The header `From:`, which is what appears in the mail client.
A marketing platform typically sends with `Return-Path: bounces@platform.com` and `From: you@yourcompany.com`. SPF passes — the server is authorised for `platform.com`. And DMARC fails, because the verified domain isn't the `From:` domain.
Two modifiers control how strict the comparison is:
- `aspf=r` / `adkim=r` (relaxed, the default): the organisational domain is enough. `mail.yourcompany.com` aligns with `yourcompany.com`.
- `aspf=s` / `adkim=s` (strict): the domain must match exactly.
Two practical conclusions follow:
DKIM is more robust than SPF for alignment. A DKIM signature travels with the message and survives forwarding; SPF is evaluated against the last server that delivered, so forwarding breaks it almost every time. Where a service lets you sign with your own domain, do it — that's what keeps DMARC standing when someone forwards your mail.
Never turn on strict mode before reading the reports. `adkim=s` breaks any legitimate service that signs with a subdomain.
The rollout, without breaking mail
The expensive failure isn't having no DMARC. It's switching to `p=reject` and blocking your own invoicing for a week.
Week 1 — inventory. List everything that sends mail as your domain: Microsoft 365, the CRM, the marketing platform, invoicing, the ERP, website forms, e-signature tools. The list is always longer than the administrator remembers. Publish SPF covering them, and verify you're under 10 lookups.
Week 2 — DKIM. Enable it in Microsoft 365 and publish both CNAMEs. Then, in every third-party service that allows it, configure signing with your own domain rather than accepting the provider's.
Week 3 — DMARC in observation. Publish `p=none` with `rua=`. Change nothing else.
Weeks 4–7 — read. Aggregate reports arrive as daily XML from each large receiver. You're looking for one thing: sources sending legitimate mail of yours that are not aligned. Each one is a service to fix before you harden the policy.
Week 8 — quarantine. Move to `p=quarantine`, ideally with `pct=` to ramp gradually. Watch for two weeks.
Week 10 — reject. `p=reject`, plus `sp=reject` for subdomains. Without `sp=`, subdomains inherit the parent policy — but stating it explicitly avoids surprises when somebody creates `mail.yourcompany.com` next year.
You keep reading the reports afterwards. DMARC isn't a project that closes; it's a configuration that's maintained.
The transport layer: MTA-STS, TLS-RPT, DNSSEC and BIMI
SPF, DKIM and DMARC authenticate who sends. They say nothing about whether the connection carrying the message is encrypted, or whether someone can downgrade it.
MTA-STS (RFC 8461) requires delivery to your domain to use valid TLS, closing the downgrade attack where an intermediary strips `STARTTLS` and the mail travels in clear text. It's published in two parts: a TXT record at `_mta-sts.yourdomain.com` and a policy file served over HTTPS at `mta-sts.yourdomain.com`.
One detail matters here: the policy has three modes, and `mode: testing` protects nothing. Announcing MTA-STS without `mode: enforce` is precisely the same placebo as `p=none` — the configuration exists, it looks right in a shallow audit, and it changes the handling of exactly zero messages.
TLS-RPT (RFC 8460) is how you find out TLS delivery is failing. Published at `_smtp._tls.yourdomain.com`. Without it, MTA-STS fails silently.
DNSSEC signs your DNS answers. Without it, everything above rests on records an attacker in a network position can forge: your SPF, DKIM and DMARC are only as trustworthy as the DNS publishing them.
BIMI displays your brand's logo in the mail client, and requires DMARC at `p=quarantine` or `p=reject` plus a Verified Mark Certificate. It's the only one of the four whose benefit is commercial before it's technical — and it's only reachable once everything else is genuinely done.
How to read a DMARC aggregate report
`rua=` brings you a compressed XML per receiver, per day. It's the document that decides whether you can harden your policy, and almost nobody explains it. The structure that matters:
```xml
<record>
<row>
<source_ip>203.0.113.45</source_ip>
<count>128</count>
<policy_evaluated>
<disposition>none</disposition>
<dkim>fail</dkim>
<spf>pass</spf>
</policy_evaluated>
</row>
<identifiers>
<header_from>yourcompany.com</header_from>
</identifiers>
<auth_results>
<spf><domain>platform.com</domain><result>pass</result></spf>
</auth_results>
</record>
```
Read it like this:
- `count` is how many messages came from that IP that day. It tells you whether the source is meaningful or noise.
- `policy_evaluated` is the result after alignment. It's the one that governs.
- `auth_results` is the raw result, before alignment.
The example above is exactly the case that breaks migrations: `auth_results` says `spf: pass`, and `policy_evaluated` says `spf: pass` with `dkim: fail`. The message survives because DMARC passes if either mechanism aligns. If that platform stopped aligning SPF — a provider change, a new bounce domain — those 128 daily messages would start being rejected the day you set `p=reject`.
You are looking for one thing in every report: rows where `header_from` is your domain, the volume isn't trivial, and `policy_evaluated` shows `fail` on both. Each one is either your mail about to be blocked, or somebody impersonating you. Telling them apart is the real work, and it's done by `source_ip`: if the IP belongs to a provider you recognise, it's yours and needs fixing; if it doesn't, it's the impersonation DMARC exists to stop.
Forensic reports (`ruf=`) are a different thing: one per failed message, including headers. Most large receivers no longer send them for privacy reasons, so don't build your process on them.
Configuration by provider
Most domains send from four or five different systems. These are the ones we find most often in Microsoft 365 tenants.
Microsoft 365. `include:spf.protection.outlook.com` in SPF; DKIM is enabled per domain in the Defender admin centre (Policies → Rules → DKIM) and publishes the two selector CNAMEs. A newly added domain has DKIM disabled even though mail is already flowing.
Google Workspace, where both coexist: `include:_spf.google.com`, with DKIM generated in the admin console under the `google` selector.
Mailchimp / Intuit. `include:servers.mcsv.net`. Signs with its own domain by default, which does not align. You have to configure domain authentication in the platform so it signs with yours.
SendGrid. `include:sendgrid.net`, and its "Domain Authentication" publishes CNAMEs under a subdomain of yours. That's the correct mode: a signature aligned with your domain.
Salesforce / Dynamics. `include:_spf.salesforce.com` and equivalents. Both support their own DKIM signing, and both arrive misaligned out of the box.
Invoicing systems and ERPs. The worst in practice: many send over authenticated SMTP from a fixed IP with no DKIM support. For those, a direct `ip4:` in SPF and alignment via SPF, with no DKIM. It works, and it breaks on any forward.
Diagnosis: symptom, cause, fix
| Symptom | Most likely cause | Fix |
|---|---|---|
| Invoicing mail lands in spam, but only at Gmail | DMARC at `p=none` and no DKIM alignment | Sign with your domain in the invoicing system |
| SPF "passes" but DMARC fails | Misalignment: the `Return-Path` belongs to the provider | Configure domain authentication at the provider |
| It worked, then stopped, with no changes | The 10-lookup limit was crossed when a third-party `include:` grew | Audit the lookup budget; replace `include:` with `ip4:` |
| Fails only when someone forwards | SPF does not survive forwarding, by design | Rely on DKIM for alignment, not SPF |
| DKIM broke after a few months | Only one selector was published | Publish both `selector1` and `selector2` |
| DMARC reports never arrive | `rua=` points at a mailbox on another domain with no authorisation record | Use a mailbox on your own domain, or publish the external authorisation record |
That second-to-last case deserves a note: if your `rua=` points at a domain other than the one publishing DMARC, the receiving domain must publish an authorisation record (`yourdomain.com._report._dmarc.receiver.com`). Without it most receivers send nothing, and the administrator concludes "DMARC doesn't work".
Frequently asked questions
How long does a DNS change take to propagate?
Whatever the record's TTL says, typically 5 minutes to an hour. Lower the TTL to 300 seconds before a policy migration, not during one.
Can I have two SPF records?
No. A domain with two `v=spf1` records produces `permerror` and everything fails. If you need to cover more services, combine them into a single record while respecting the lookup limit.
Does DMARC protect my subdomains?
Yes, by inheritance, unless the subdomain publishes its own policy. Declare `sp=` explicitly — it's what protects the subdomains that don't exist yet, which are the preferred vector for impersonating a brand.
What about domains that don't send mail?
Publish anyway. A parked domain with no SPF or DMARC is spoofable, and it's the one an attacker will choose precisely because nobody watches it. For those: `v=spf1 -all` and `v=DMARC1; p=reject;`.
Is BIMI worth it?
Only once `p=quarantine` or `p=reject` is genuinely working. A VMC costs an annual fee and requires a registered trademark. It's the last step, not a shortcut.
Does this protect me from phishing?
From exact impersonation of your domain, yes. It does not protect against lookalike domains (`yourcompany-mx.com`), or forged display names on somebody else's domain. Those need different defences: lookalike-domain monitoring and anti-spoofing rules inside the tenant itself.
Checking your own configuration
simiriki's free audit resolves your DNS live and evaluates ten email-authentication controls, each against its RFC: SPF presence and strictness (including RFC 7208's 10-lookup limit), the Microsoft 365 DKIM selectors, DMARC presence and enforcement level, and the transport layer — MTA-STS with a `mode: enforce` check, TLS-RPT, DNSSEC and BIMI.
When a record can't be determined — the resolver returns SERVFAIL, or the MTA-STS policy isn't reachable — the control is reported as not determined, never as a pass and never as a failure. An invented verdict about your mail is worse than no verdict at all.
Is your business protected?
A free Microsoft 365 audit—the automated scan delivers a preview in 90 seconds. Find risks before they become incidents.