SPF, DKIM and DMARC: every tag and switch explained

Most SPF, DKIM and DMARC records are copied from somewhere and never read again. That works until a new newsletter tool arrives, SPF hits its lookup limit, or DMARC goes to p=reject and the payslips stop arriving. This guide is the reference we use ourselves: every mechanism, tag and switch of the three standards, what it does, what the default is, and when you would change it. DMARC is covered as updated in May 2026 (RFC 9989, 9990 and 9991), including the new np, psd and t tags.

You can build every record in this guide with the generator below. It runs in your browser, counts SPF lookups, creates DKIM keys locally and explains what each part of your record means.

Optional. With your domain we fill in the host names and you can load the records you have now. Everything is built in your browser; 365Fanatic stores nothing.

Who sends mail for you?

Always check the include in the service's documentation; it can change. Many newsletter and transactional services (SES, SendGrid, Mailgun, Postmark) use their own domain or a subdomain of yours as the envelope sender. Their include then belongs there, not on your main domain, and DKIM takes care of DMARC alignment.

Your own servers

With Microsoft 365 you usually don't need these: mx then points to Microsoft, which the include already covers. Each one costs a DNS lookup.

IP addresses cost no DNS lookup; every include does, plus everything inside that include.

What happens to everything else? all

With DMARC it makes little difference to the receiver whether you pick -all or ~all: DMARC decides. Without DMARC, -all is the clearest choice. We deliberately don't offer +all (anyone may send).

Advanced: redirect, exists, exp and no-mail domain

Useful when many domains share one policy. A redirect only applies when the record has no all; we leave all out in that case.

We don't offer ptr: the standard advises against it and large receivers ignore it.

Read an existing SPF record

Done? Check the result with our email security check. What every tag means is in the guide to every tag.

Looking for the step-by-step rollout? This guide is the reference. For the order of work in Microsoft 365, from inventory to p=reject, read SPF, DKIM and DMARC for Microsoft 365. For DNSSEC and DANE, read DNSSEC and DANE for Exchange Online.

How the three fit together

An email has two sender addresses. The envelope sender (MAIL FROM, shown afterwards as Return-Path) is used by servers for bounces. The header From is what the recipient sees in Outlook. They are often different: a newsletter service sends with its own envelope address and your From address.

  • SPF checks whether the sending server’s IP address is allowed to send for the envelope domain.
  • DKIM checks a cryptographic signature in the message, made with the key of the domain in the signature’s d= tag.
  • DMARC ties them to what the recipient sees: a message passes DMARC when SPF passes for a domain that aligns with the header From, or when DKIM passes with a d= that aligns with the header From. One of the two is enough.

That last point explains most real-world problems. SPF can pass for bounces.mailservice.example while your DMARC still fails, because that domain has nothing to do with yours. The fix is usually DKIM with your own domain, not a longer SPF record.

SPF

SPF (Sender Policy Framework, RFC 7208) is a TXT record on the envelope domain that lists who may send for it. Receivers evaluate the record from left to right and stop at the first mechanism that matches. The qualifier of that mechanism is the result.

Type Name Value (example)
TXT contoso.nl (or @) v=spf1 ip4:192.0.2.10 include:spf.protection.outlook.com -all

Version

v=spf1 must be the first thing in the record, followed by a space. A domain name can have exactly one SPF record. Two TXT records that both start with v=spf1 give a permerror, and SPF then fails for all mail. The old SPF record type (type 99) is obsolete; use TXT.

Qualifiers

Every mechanism can have a qualifier in front of it. Without one, + applies.

Qualifier Result Meaning
+ pass Allowed to send. The default, so you never need to write it.
- fail Not allowed. Used almost only as -all at the end.
~ softfail Probably not allowed; accept but treat as suspicious. Used as ~all during a transition.
? neutral No statement, the same as having no record for this sender.

Mechanisms

Mechanism Matches when DNS lookups
all Always. Put it last: everything after it is ignored. 0
ip4:192.0.2.10
ip4:198.51.100.0/24
The sending IP is this IPv4 address or in this range. 0
ip6:2001:db8::/32 The sending IP is this IPv6 address or in this range. 0
a
a:mail.contoso.nl
a/24
The sending IP is one of the A/AAAA addresses of the current domain (or the given one). An optional prefix length widens it to the network. 1
mx
mx:contoso.nl
mx/24//64
The sending IP is one of the addresses of the MX hosts. The first number is the IPv4 prefix, the one after // the IPv6 prefix. 1, plus the address lookups of each MX host (at most 10 MX hosts)
include:spf.protection.outlook.com The SPF record of that domain gives pass for this IP. Its -all does not end your record: a fail inside an include just means “no match here”. 1, plus everything inside it
exists:%{i}._spf.contoso.nl The (macro-expanded) name has an A record. Used for per-IP allow lists. 1
ptr The reverse DNS name of the IP ends in the domain. Don’t use it: the RFC advises against it, it is slow, and large receivers ignore it. 1, plus up to 10 names

Modifiers

Modifier What it does
redirect=_spf.contoso.nl If nothing matched, use that domain’s SPF record as if it were this one. Handy when many domains share one policy. It only applies when the record has no all, and it costs one lookup.
exp=explain._spf.contoso.nl A TXT record with an explanation the receiver may put in its rejection message. Rarely used and not counted as a lookup.

Receivers ignore modifiers they don’t know, so an SPF record with an unknown name=value is still valid.

Results

Result When
pass A mechanism with + matched.
fail / softfail / neutral A mechanism with -, ~ or ? matched, usually all.
none The domain has no SPF record.
temperror A DNS time-out; the receiver may try again later.
permerror The record is broken: two SPF records, a syntax error, more than 10 lookups, or more than 2 void lookups.

The limits that break SPF

  • 10 DNS lookups. include, a, mx, ptr, exists and redirect count, including everything nested inside an include. ip4, ip6, all and exp don’t. Over ten gives permerror for all mail. The generator’s “Count all lookups” button resolves the includes and gives you the real number.
  • 2 void lookups. A lookup that returns nothing (a removed include, a typo) counts as void; the third one gives permerror. Clean up includes of services you no longer use.
  • 255 characters per string. A TXT record can hold several strings of up to 255 characters, which the receiver glues together without a space. Most DNS providers split long values for you; if yours doesn’t, split them yourself and keep the spaces inside the strings.

Macros

Macros let exists, include and redirect build a name from the message being checked. You will rarely write them yourself, but you may see them in a provider’s record.

Macro Expands to
%{s} The envelope sender, for example [email protected]
%{l} / %{o} The local part (user) and the domain (contoso.nl) of the envelope sender
%{d} The domain whose record is being evaluated
%{i} The sending IP address (IPv4 dotted, IPv6 as dot-separated nibbles)
%{h} The HELO/EHLO name of the sending server
%{v} in-addr for IPv4, ip6 for IPv6
%{ir} A letter r reverses the parts, so %{ir} of 192.0.2.10 is 10.2.0.192. A number keeps only that many parts from the right.

SPF in Microsoft 365

For a domain that only sends through Exchange Online the record is v=spf1 include:spf.protection.outlook.com -all. You don’t need mx or a: your MX points to Microsoft, which the include already covers. For domains that never send mail, publish v=spf1 -all. Exchange Online itself evaluates inbound SPF as part of composite authentication (compauth in the Authentication-Results header); you can read those results with our header analyzer.

DKIM

DKIM (DomainKeys Identified Mail, RFC 6376) adds a DKIM-Signature header to every outgoing message. The receiver fetches the public key from DNS and checks that the signed headers and the body were not changed. Because the signature travels inside the message, DKIM usually survives forwarding, which SPF does not.

The key record

The public key lives at <selector>._domainkey.<domain>. The selector is a name you choose, so one domain can have several keys: one per service, and a new one for each rotation.

Tag Values Meaning
v= DKIM1 Version. Optional, but if present it must come first.
k= rsa (default), ed25519 Key type. Ed25519 (RFC 8463) gives short keys but is not verified everywhere yet, so use it only next to an RSA signature.
p= base64 The public key. Empty (p=) means the key is revoked; signatures with this selector then fail.
h= sha256 Hash algorithms this key may be used with. sha1 must no longer be used for signing (RFC 8301), so h=sha256 is a safe restriction.
s= * (default), email Service type. email limits the key to email.
t= y, s, or y:s Flags. y: testing, receivers must not treat a failed signature differently from an unsigned message. Remove it after testing. s: strict, the i= in a signature must be exactly the d= domain, not a subdomain.
n= text Notes for people. Ignored by receivers.

Key length: use 2048-bit RSA. 1024-bit keys are considered too weak for new use, and signatures made with keys shorter than 1024 bits are not accepted as valid (RFC 8301). A 2048-bit key is about 400 characters, so it spans two TXT strings. 4096-bit keys work in theory, but many DNS hosts and some receivers struggle with records that long.

The signature header

You don’t write the signature yourself, but reading it tells you who signed and why DKIM or DMARC failed. The header analyzer shows these fields for any message you paste.

Tag Meaning
v=1 Signature version.
a= Algorithm: rsa-sha256 or ed25519-sha256. rsa-sha1 is obsolete.
d= The signing domain. This is the domain DMARC compares with the header From.
s= The selector; together with d= it points to the key: s._domainkey.d.
h= The header fields that are signed, for example from:to:subject:date:message-id. From must always be in it. Listing from twice (“oversigning”) stops anyone adding a second From header.
bh= Hash of the body. A mismatch means the body changed on the way (a footer or disclaimer added).
b= The signature itself.
c= Canonicalization for header/body: simple or relaxed. relaxed/relaxed tolerates changes in white space and header case, and is the common choice. The default is simple/simple.
i= The identity of the user or agent, for example @contoso.nl. Must be the d= domain or a subdomain of it.
l= Body length: only this many bytes were signed. Avoid it; it lets anyone append content after the signed part.
t= / x= Signing time and expiry time (Unix timestamps).
q= How to fetch the key: always dns/txt.
z= Copies of the original headers, for troubleshooting. Rare.

DKIM in Microsoft 365

Microsoft signs every message with your onmicrosoft.com domain by default. That signature is valid, but it does not align with your own domain, so it doesn’t help DMARC. Enable DKIM per custom domain:

PowerShell
Connect-ExchangeOnline
New-DkimSigningConfig -DomainName contoso.nl -KeySize 2048 -Enabled $false
Get-DkimSigningConfig -Identity contoso.nl | Format-List Selector1CNAME, Selector2CNAME

Publish two CNAME records, selector1._domainkey and selector2._domainkey, pointing to the values shown. Since 2025 new targets end in dkim.mail.microsoft, with a part that differs per tenant, so always copy them rather than typing them from an example. Microsoft keeps the private keys; the second selector is for the next rotation. Then switch signing on and, later, rotate:

PowerShell
Set-DkimSigningConfig -Identity contoso.nl -Enabled $true
Rotate-DkimSigningConfig -Identity contoso.nl -KeySize 2048

Every other service that sends with your domain (newsletter, CRM, ticketing, invoicing) needs its own DKIM with d= your domain, usually through CNAME records the service gives you. For a mail server you run yourself, the generator can create the key pair in your browser.

DMARC

DMARC (Domain-based Message Authentication, Reporting and Conformance) is a TXT record at _dmarc.<domain>. It tells receivers what to do with mail that fails alignment, and where to send reports. In May 2026 RFC 7489 was replaced by RFC 9989 (the protocol), RFC 9990 (aggregate reports) and RFC 9991 (failure reports). The version stays DMARC1 and existing records keep working.

Type Name Value (example)
TXT _dmarc.contoso.nl v=DMARC1; p=reject; np=reject; rua=mailto:[email protected]

Alignment

DMARC passes when at least one of these is true:

  • SPF passes, and the envelope domain aligns with the header From domain;
  • DKIM passes, and the signature’s d= aligns with the header From domain.

Relaxed alignment (the default) accepts any domain under the same organisational domain: news.contoso.nl aligns with contoso.nl. Strict alignment requires the exact same domain. RFC 9989 finds the organisational domain with a short walk up the DNS tree, looking for DMARC records, instead of the Public Suffix List that RFC 7489 relied on.

All DMARC tags

Tag Values (default) Meaning
v= DMARC1 Version. Must come first.
p= none, quarantine, reject Policy for the domain. none: only report. quarantine: treat failing mail as suspicious (junk folder). reject: refuse it. Always set it.
sp= as p Policy for subdomains that exist. Leave it out unless subdomains need a different policy.
np= as sp, then p New in RFC 9989. Policy for subdomains that do not exist in DNS, such as invoice.contoso.nl made up by a fraudster. np=reject is safe early on, because no legitimate mail comes from names that don’t exist.
adkim= r (relaxed), s Alignment mode for DKIM.
aspf= r (relaxed), s Alignment mode for SPF.
rua= mailto: addresses, comma separated Where aggregate reports go: daily XML summaries per sending IP, with counts and SPF/DKIM/DMARC results (RFC 9990). The single most useful tag.
ruf= mailto: addresses Where failure reports go: one report per failing message (RFC 9991). They can contain personal data and few large receivers send them.
fo= 0 (default), 1, d, s, colon separated When to send a failure report: 0 when SPF and DKIM both fail to produce an aligned pass, 1 when either fails, d when DKIM fails, s when SPF fails. Only meaningful with ruf.
t= n (default), y New in RFC 9989. Testing mode: receivers apply the policy one step lighter, so reject is handled as quarantine and quarantine as none. Replaces the old trick with pct=0. Receivers that don’t know the tag yet ignore it and apply p as written.
psd= u (default), y, n New in RFC 9989. Whether this domain is a public suffix (y, for registries of domains like .nl), an organisational domain (n) or unknown (u). Normal organisations leave it out.
pct= 0–100 Removed in RFC 9989. The percentage of failing mail the policy applied to. In practice only 0 and 100 worked reliably. New receivers ignore it; use t=y instead.
rf= afrf Removed. Failure report format; there was only one.
ri= seconds (86400) Removed. Requested report interval; receivers sent daily reports regardless.

Reports to another domain

If rua or ruf points to an address on a different domain, for example a report service, that domain has to confirm it accepts reports for you. It does so with a TXT record contoso.nl._report._dmarc.reportservice.example containing v=DMARC1. Without it, receivers drop the reports. Report services publish this for their customers; the generator shows the record when you enter an external address.

Aggregate reports arrive as compressed XML files, dozens a day for a normal organisation. Nobody reads them by hand. In our experience DMARC Advisor is the best tool to receive and read them: it groups the reports per sending source, alerts you to new or failing sources and guides you step by step to p=reject.

Moving from none to reject

  1. p=none with rua, and np=reject straight away. Read the reports for two to four weeks.
  2. Fix every legitimate source that fails: DKIM with your domain first, SPF second.
  3. p=quarantine, optionally with t=y for a week while you watch the reports.
  4. p=reject, again optionally with t=y first. Then remove t=y.
  5. Keep reading the reports. New tools that send as you appear every year.

DMARC in Microsoft 365

For outbound mail, Microsoft 365 needs DKIM on your custom domain; SPF alone rarely aligns for everything. Microsoft does not host your DMARC record: you publish it in your own DNS, also for the domains that never send mail. For inbound mail, Exchange Online Protection checks the sender’s DMARC. What happens to failing messages is set in the anti-phishing policy under Honor DMARC record policy when the message is detected as spoof, with separate actions for senders at p=quarantine and p=reject. Messages that fail show dmarc=fail and a compauth reason in their headers.

The other records around them

SPF, DKIM and DMARC protect your domain against spoofing. Three smaller standards complete the picture, and the generator builds them too.

Record Where Tags
MTA-STS TXT _mta-sts plus a file at https://mta-sts.<domain>/.well-known/mta-sts.txt TXT: v=STSv1; id= (change the id whenever the file changes). File: version: STSv1, mode: testing/enforce/none, one mx: line per mail server (a wildcard like *.mail.protection.outlook.com covers one label), max_age: in seconds, up to one year.
TLS-RPT TXT _smtp._tls v=TLSRPTv1; rua=mailto:… (or https:). Daily JSON reports about failed encrypted deliveries to you. Set it up before MTA-STS goes to enforce.
BIMI TXT default._bimi v=BIMI1; l= HTTPS link to an SVG Tiny Portable/Secure logo; a= HTTPS link to a VMC or CMC certificate. Only works with DMARC at quarantine or reject.

Inbound protection of the connection itself comes from DNSSEC and DANE. For Exchange Online that means signing your zone and moving your MX to the DNSSEC-signed *.mx.microsoft endpoint; the legacy mail.protection.outlook.com endpoint has no DANE. If you run MTA-STS as well, list both endpoints while you move.

Checklist

  • One SPF record per sending domain, under ten lookups, ending in -all (or ~all during the transition).
  • No includes for services that don’t send with that exact envelope domain.
  • DKIM with 2048-bit keys for Microsoft 365 and every other sender, with d= your domain.
  • DMARC with rua, np=reject, and a plan from none to reject.
  • Non-sending domains: v=spf1 -all, DMARC p=reject, and a null MX (0 .).
  • TLS-RPT, then MTA-STS; DNSSEC and DANE when your DNS host supports DNSSEC.
  • Check the result with our email security check.

Frequent errors and how to fix them

These are the messages people type into search when the module will not start. The exact wording is here so this page can be found.

The term 'Connect-ExchangeOnline' is not recognized
Module not installed, or the wrong host. Install-Module ExchangeOnlineManagement, then open a new window.
A window handle must be configured / MsalClientException
MSAL/WAM handler bug: no window for sign-in. As administrator: winget install --id Microsoft.PowerShell --source winget, then pwsh. ISE still hosts 5.1; the Exchange module does run there. Staying in ISE: Connect-ExchangeOnline -DisableWAM. Headless scripts: certificate or managed identity. Full explanation.
Error Acquiring Token: System.NullReferenceException / 0x80070520
WAM sign-in (module 3.7.0+) failed. NullReferenceException: another module in the same window (usually Microsoft Graph) loaded a different MSAL version; new window, Connect-ExchangeOnline first, or -DisableWAM. 0x80070520 "A specified logon session does not exist": PowerShell runs as another account or in Task Scheduler; start as yourself, or use a certificate. Full explanation.
WinRM cannot process the request / CreateExoPSSession
Legacy V1/V2 remoting. Use module V3+ and Connect-ExchangeOnline (no separate WinRM session).
AADSTS50076 / MFA required
Interactive: complete MFA. For scripts: certificate (app-only), not a user password.
Unauthorized / Access Denied
The account or app registration has no Exchange role (for example Exchange Administrator). App-only: Exchange.ManageAsApp plus that role.
Could not create SSL/TLS secure channel
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 — or use PowerShell 7.
Fail to create a runspace / connecting to outlook.office365.com
Legacy remote PowerShell. Update to ExchangeOnlineManagement V3; REST is the default. Disconnect-ExchangeOnline -Confirm:$false between jobs.
The term 'Get-Mailbox' is not recognized
You are not connected, or the REST session expired. Connect-ExchangeOnline again; do not mix with a leftover MSOnline session.

Questions about this, or need help setting it up? We are happy to think along.

Message on LinkedIn →