- Two from addresses, one loophole
- SPF: the list of servers allowed to send for your domain
- DKIM: a signature that travels with the message
- DMARC: alignment plus a published policy
- The rollout: from p=none to p=reject without losing mail
- What still breaks at p=reject
- Domains that never send email
- Checking your work
- The order to do things in
Nothing in SMTP stops a stranger sending email as you. The protocol will happily carry a message with your domain in the From header from any server on earth, which is why receiving mail systems now lean on three DNS records to decide what to trust: SPF, DKIM and DMARC. This guide explains what each record actually checks, how to publish all three correctly and how to walk a domain from monitoring mode to a full reject policy without losing legitimate mail on the way.
This stopped being optional a while ago. Since February 2024 Google and Yahoo have required every sender to pass SPF or DKIM, and anyone sending more than 5,000 messages a day to Gmail to pass both, publish a DMARC record and align the visible From domain. Microsoft brought Outlook.com broadly in line during 2025. Fall short and your mail gets rejected or spam-foldered regardless of how legitimate the content is. Deliverability and spoofing protection are now the same project.
Two from addresses, one loophole
Every email has two senders, and the difference between them underpins everything else in this article.
The first is the envelope from, also called the return path, the bounce address or RFC5321.MailFrom. It is exchanged during the SMTP conversation before any content is transferred, and it is where bounces go. Your recipient never normally sees it. The second is the header From, RFC5322.From, the address displayed in the mail client.
These two can be completely different, and often are for good reasons. A marketing platform sends with its own bounce domain in the envelope so it can process delivery failures, while your domain sits in the visible From. A mailing list relays your message with the list's envelope address. The trouble is that attackers exploit the same gap: put their own domain in the envelope, your domain in the header From, and the recipient sees mail apparently from you.
Keep this split in mind. SPF checks the envelope. DKIM signs the message. Neither of them, on its own, says anything about the From address a human actually reads. DMARC is the piece that ties authentication back to the visible From, and the concept it uses to do that is called alignment. We will get there.
SPF: the list of servers allowed to send for your domain
SPF is a single TXT record on your domain that lists which servers may use it in the envelope from. When a message arrives, the receiver takes the connecting IP address and the envelope from domain, fetches that domain's TXT record and evaluates it left to right until something matches. If the envelope from is empty, as it is for bounce messages, the HELO domain is checked instead.
A typical record looks like this:
example.com. IN TXT "v=spf1 ip4:203.0.113.25 include:_spf.google.com ~all"
Reading left to right: mail is authorised if it comes from 203.0.113.25, or from any server allowed by Google's own SPF record, and anything else soft-fails. The building blocks are mechanisms and qualifiers.
The mechanisms you will actually use are ip4 and ip6 for literal addresses or CIDR ranges, include to pull in another domain's record (this is how every hosted provider works), a and mx to authorise whatever your A or MX records point at, and all as the catch-all that ends the record. Each mechanism takes an optional qualifier: + pass (the default), - fail, ~ softfail and ? neutral. In practice the only qualifier you write by hand is the one on all.
Evaluation stops at the first match, so order matters slightly, and all must come last because it matches everything.
Check what a domain currently publishes with dig:
dig +short TXT example.com
One rule that trips people up constantly: a domain gets exactly one SPF record. If dig shows two strings both starting v=spf1, receivers return a permanent error and treat your mail as unauthenticated. This usually happens when a marketing tool's setup guide says "add our SPF record" and someone adds a second record instead of merging the include into the existing one. Merge them.
The ten lookup limit
SPF caps the number of DNS lookups a single evaluation may trigger at ten. include, a, mx, exists and redirect each cost one, and nested includes count too, so a single include:_spf.google.com actually costs four because Google's record fans out into three netblock includes. ip4 and ip6 cost nothing.
Blow past ten and the result is a permanent error, which can never produce a pass. A broken SPF record quietly downgrades your whole domain to DKIM-only authentication, and most people find out from their DMARC reports rather than from the record itself.
Counting by hand means recursively expanding every include, which is tedious, so paste your record into any SPF checker and let it count. If you are over the limit, three fixes work in rough order of preference. Replace a and mx with literal ip4 entries for servers you control. Drop the include for any provider whose mail is DKIM-signed with your domain, because under DMARC a source only needs one aligned mechanism and DKIM is usually the better one. As a last resort, flattening services expand your includes into raw IPs and republish automatically, at the cost of tying your DNS to their update cycle.
While you are in there, note that a single TXT string maxes out at 255 characters. Longer records are legal but must be split into multiple quoted strings inside the one record, which receivers concatenate:
example.com. IN TXT ( "v=spf1 ip4:203.0.113.25 ip4:198.51.100.0/24 "
"include:_spf.google.com include:spf.mtasv.net ~all" )
Softfail or hardfail
The eternal question: end the record with ~all or -all?
Once DMARC enters the picture the answer matters less than people think. DMARC only asks whether SPF produced a pass. Softfail, fail, neutral and permerror are all equally "not a pass" as far as DMARC alignment is concerned. What -all changes is the behaviour of receivers that check SPF but not DMARC, some of which will reject at SMTP time before DMARC is ever consulted. That sounds attractive until a forwarded message from your MD bounces because the forwarder's IP was not in your record.
The pragmatic position for a domain that sends mail: publish ~all, let DMARC make the enforcement decision. Reserve -all for domains that send nothing at all, which we will cover near the end. And never publish +all, which authorises the entire internet and is treated by some filters as worse than no record.
Where SPF falls over
Two structural weaknesses explain why SPF alone was never enough.
Forwarding breaks it. When a university mailbox auto-forwards to Gmail, Gmail sees the university's server IP with your envelope from, and your record has never heard of the university. Some forwarders rewrite the envelope to fix this (the scheme is called SRS) but plenty do not, so a percentage of perfectly legitimate mail will always fail SPF. You cannot fix this from your side.
And it says nothing about the visible From. An attacker who controls attacker.example can pass SPF with flying colours while displaying your domain to the victim, because SPF only ever looked at the envelope.
Both weaknesses are why the next two records exist.
DKIM: a signature that travels with the message
DKIM takes a different approach. Instead of validating the connection, the sending server signs the message with a private key, covering a hash of the body and a chosen set of headers. The matching public key is published in your DNS. Any receiver, at any hop, can verify the signature, which means DKIM survives forwarding intact. The signature lives in the message, not the connection.
Here is a signature header, trimmed and annotated:
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com;
s=mail2026; h=from:to:subject:date:mime-version;
bh=uMixy0BsCqhbru4fqPZQdeZY5Pq865sMVGHY48OZo9Y=;
b=Gx41P8beVCT7q3EXAMPLEONLYc2hpZnR5IGxpdHRsZSBzdHJpbmc...
The tags that matter: d= is the signing domain, and it is the domain DMARC will later compare against the From header, so it is the single most important tag in this whole article. s= is the selector, which tells the receiver where in DNS to find the public key. h= lists the headers covered by the signature, bh= is the body hash and b= is the signature itself. c=relaxed/relaxed sets the canonicalisation, which tolerates the whitespace and line-wrapping mangling that mail servers inflict in transit. Use relaxed unless you enjoy debugging signatures broken by an extra space.
Selectors are the reason one domain can run many keys at once. The receiver looks up the public key at <selector>._domainkey.<domain>, so s=mail2026 and d=example.com resolve to a TXT record at mail2026._domainkey.example.com. Google signs with the selector google, your transactional provider uses its own, your own server uses another, and none of them collide. Selectors are also what make key rotation painless.
Generating and publishing a key for your own server
If you run your own MTA, OpenDKIM is the standard signing filter for Postfix and friends. Generate a keypair:
opendkim-genkey -b 2048 -d example.com -s mail2026 -D /etc/opendkim/keys/example.com
That writes mail2026.private, which the MTA signs with, and mail2026.txt, which contains the DNS record ready to paste. Use 2048-bit RSA. 1024-bit keys still verify but are considered weak and some receivers score them down. An Ed25519 variant of DKIM exists and produces pleasingly short records, but receiver support remains patchy, so RSA 2048 is the safe default and dual-signing with both is the only sensible way to use Ed25519 today.
The published record looks like this, split into two strings because a 2048-bit key does not fit in 255 characters:
mail2026._domainkey.example.com. IN TXT ( "v=DKIM1; k=rsa; "
"p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0v3xEXAMPLE"
"...rest of the base64 public key..." )
Point OpenDKIM at the private key, restart Postfix, send yourself a test message and confirm the signature verifies before moving on.
Hosted senders: usually just CNAMEs
For hosted providers you never touch a key. Google Workspace has you generate the keypair in the admin console and paste one TXT record at google._domainkey. Most transactional and marketing providers hand you two or three CNAME records pointing into DNS they control, which lets them rotate keys on their side without ever bothering you again.
One step in those dashboards deserves special attention. Out of the box, most providers sign with their own domain, so d= says something like d=espmail.net. That signature verifies, and the message will show dkim=pass, but it can never align with your From domain, so it does nothing for your DMARC. Every provider has a setting called something like custom domain, domain authentication or sender domain verification. Completing it switches the signature to d=yourdomain.com. If you do only one thing per provider, do that.
Rotating keys
Because selectors namespace the keys, rotation is undramatic: generate a new keypair under a new selector, publish the new public key, switch the signer to the new private key, and leave the old record in DNS for a week or two so in-flight mail still verifies. Then delete it. Every six or twelve months is a reasonable cadence, plus immediately if a signing host is compromised or someone with access to the keys leaves.
DMARC: alignment plus a published policy
Here is the gap the first two records leave open. An attacker sends from their own infrastructure with attacker.example in the envelope and a valid DKIM signature for d=attacker.example, while the From header says [email protected]. SPF passes. DKIM passes. The victim sees your bank's name. Every mechanism did its job and the mail is still a forgery, because nothing ever compared the authenticated domains with the displayed one.
DMARC closes the loop with one concept and one publication. The concept is alignment: authentication only counts if the authenticated domain matches the header From domain. The publication is a policy record telling receivers what to do when nothing aligned, plus where to send you reports about it.
Alignment, the part everyone gets wrong
A message passes DMARC when at least one of these two things is true:
SPF produced a pass and the envelope from domain aligns with the header From domain. Or DKIM produced a pass and the d= domain aligns with the header From domain.
One aligned pass is enough. Both failing is the only way to fail DMARC.
Alignment comes in two strengths. Relaxed, the default, requires the same organisational domain, so mail.example.com in the envelope aligns with example.com in the From. Strict requires an exact match. You can tighten either check with aspf=s or adkim=s in your record, but relaxed is right for almost everyone and strict mostly generates support tickets.
A concrete case makes this click. Your marketing platform sends with its own bounce domain in the envelope, so SPF passes for the platform's domain but does not align with yours. The same message carries a DKIM signature with d=example.com because you completed the custom domain step. SPF is unaligned, DKIM is aligned, DMARC passes. This is the normal state of affairs for hosted senders, and it is why DKIM alignment is the thing to chase per source, with SPF as the bonus rather than the foundation.
The record
DMARC lives in a TXT record at the _dmarc label:
_dmarc.example.com. IN TXT "v=DMARC1; p=none; rua=mailto:[email protected]"
The tags: p= is the policy for mail that fails, one of none, quarantine or reject. rua= is where aggregate reports go. ruf= requests per-message failure reports, which most large receivers no longer send for privacy reasons, so do not build a process around them. sp= sets a separate policy for subdomains, which otherwise inherit p=. pct= samples the policy onto a percentage of failing mail. adkim= and aspf= set alignment strictness as above.
Two things to understand about p=none. It is not "DMARC off". It switches reporting on, satisfies Gmail's bulk sender minimum and costs you nothing, which is exactly why every rollout starts there. But it also does precisely nothing to spoofing, so it is a starting point, not a destination. A depressing number of domains have sat at p=none since 2019.
If your rua address is on a different domain to the one publishing the record, the receiving domain has to authorise it, otherwise report senders ignore the address. The authorisation is a TXT record at yourdomain.com._report._dmarc.reportingdomain.com containing v=DMARC1. In practice every hosted report processor publishes this on their side automatically for the address they give you, so you only need to know this exists when reports mysteriously fail to arrive.
Aggregate reports, and how to actually read them
Every major receiver that saw mail claiming to be from your domain sends a daily XML report, gzipped, to your rua address. Inside is a row per source IP with counts and results. A trimmed record looks like this:
<record>
<row>
<source_ip>203.0.113.25</source_ip>
<count>412</count>
<policy_evaluated>
<disposition>none</disposition>
<dkim>pass</dkim>
<spf>fail</spf>
</policy_evaluated>
</row>
<identifiers>
<header_from>example.com</header_from>
</identifiers>
<auth_results>
<dkim><domain>example.com</domain><result>pass</result></dkim>
<spf><domain>bounce.espmail.net</domain><result>pass</result></spf>
</auth_results>
</record>
The reading order matters. auth_results holds the raw outcomes: here SPF genuinely passed, but for the provider's bounce domain. policy_evaluated holds the aligned outcomes, which are the ones DMARC scored: SPF fail because it did not align, DKIM pass because d= matched the From. One aligned pass, so the message passed DMARC and the disposition was none. This exact pattern, raw SPF pass but aligned SPF fail with DKIM carrying the day, is what healthy hosted sending looks like, and misreading it as a problem is the most common beginner mistake with these reports.
Raw XML is fine for a low-volume domain and miserable at any scale, so use a processor. parsedmarc is the open source standard, a self-hosted Python tool that parses everything into searchable data. Postmark runs a free service that emails you a weekly human-readable digest, which is plenty for most small domains. dmarcian, EasyDMARC and Valimail are the usual hosted options when you want dashboards and history across many domains.
The rollout: from p=none to p=reject without losing mail
This is where most DMARC projects stall, because moving to enforcement means being confident you have found every legitimate source of mail for your domain. Here is the sequence that works.
Start with an inventory, and be paranoid about it. The mailbox provider and the marketing platform are the easy ones. The ones that bite you are the CRM sending quotes, the helpdesk sending ticket replies, the billing system, the website contact form posting through PHP mail(), the CI server emailing failures, the HR platform, and the ancient cron job on a server nobody has logged into since 2021. You will not find them all by asking around, which is fine, because the reports will find them for you. That is what the monitoring phase is for.
Authenticate each source you know about. DKIM signed with your domain first, via the custom domain step in each provider's dashboard, because aligned DKIM survives forwarding and is sufficient on its own. Add the provider's include to your SPF record where it fits inside the lookup budget and where the provider's envelope actually aligns. For servers you run, set up both.
Publish p=none with a rua address and wait. Two to four weeks of reports is the minimum, and stretch it longer if your organisation has monthly cycles like billing runs or a newsletter that only goes out on the first Tuesday. You are waiting for every legitimate sender to show up in the data at least once.
Then work the failures. Rows where DKIM passes but is unaligned mean a provider still signing with its own domain: go and finish their domain authentication step. Rows from IPs you recognise with no DKIM at all are your own systems that need a signer. Scattered low-volume rows from unrelated networks where DKIM still passes are forwarding, which is fine and needs nothing from you. Rows where nothing passes and nothing aligns, from networks you have never heard of, are the spoofing you are here to stop, and they need nothing from you either, except perhaps a moment of motivation.
When a clean fortnight goes by with every legitimate source aligned, tighten to p=quarantine. If the volume is high enough to be nervous about, ramp with pct: p=quarantine; pct=25 applies quarantine to a quarter of failing mail and leaves the rest alone, and you step it up to 50 and then 100 as confidence grows. Watch the reports at each step. Quarantined legitimate mail shows up as complaints about things landing in spam, which is recoverable in a way that rejected mail is not, and that is exactly why quarantine sits in the middle of the path.
Move to p=reject and stay there. This is the destination: receivers refuse failing mail outright, and spoofed messages never reach a human. Keep the reports flowing after you arrive, because six months from now marketing will sign up for a new tool, point it at your domain and tell nobody. The rua mailbox is how you find out before their campaign lands in spam.
Finally, think about subdomains. By default your policy covers them through organisational domain matching, unless a subdomain publishes its own record. If subdomains send mail, give the important ones explicit records. If they never should, sp=reject at the top pins them down even while you are still ramping the main policy.
What still breaks at p=reject
Enforcement surfaces a few structural cases worth recognising rather than panicking about.
Plain forwarding continues to work, because DKIM survives it and one aligned pass is all DMARC needs. The place it gets messy is mailing lists that modify messages in transit, adding a subject tag or a footer, which breaks the body hash and kills the signature. The ecosystem's answer has been From rewriting: modern list software detects an enforcing DMARC policy on the author's domain and rewrites the From to the list's own address, which is why you see "Jane Smith via Dev List" in your inbox. Nothing for you to fix, but useful to recognise when a user asks why the list changed their name.
ARC is the other piece of forwarding machinery you will meet in headers. Intermediaries that modify mail can cryptographically seal the authentication results they saw on arrival, and a receiver that trusts the intermediary can honour the original results even though the signature broke in transit. You publish nothing for ARC and configure nothing. Just recognise arc=pass in an Authentication-Results header as "a trusted forwarder vouched for this" when you are debugging a delivery.
The last recurring case is third-party systems sending as individual users, calendar tools and ticketing systems being the usual suspects. The fix is always the same: their custom domain feature, or accepting mail sent from their domain with your address in Reply-To instead of From.
Domains that never send email
Every organisation owns domains that send nothing: the .co.uk that redirects to the .com, the product name registered defensively, the stale campaign microsite. Attackers love these precisely because nobody is watching them, and locking one down takes three records and two minutes:
parked-domain.co.uk. IN TXT "v=spf1 -all"
_dmarc.parked-domain.co.uk. IN TXT "v=DMARC1; p=reject"
parked-domain.co.uk. IN MX 0 .
The SPF record authorises nobody, this being the one place hardfail is unambiguous. The DMARC record tells receivers to reject anything claiming to be from the domain. The null MX declares that the domain accepts no mail either, so senders fail fast instead of queueing for days. Do this for every domain in the registrar account, not just the ones with websites.
Checking your work
Three dig commands confirm what the world sees:
dig +short TXT example.com
dig +short TXT mail2026._domainkey.example.com
dig +short TXT _dmarc.example.com
For a live end-to-end check, send a message to a Gmail account and use Show original, which displays SPF, DKIM and DMARC verdicts at the top with the domains each one evaluated. When you need more forensic detail, learndmarc.com gives you a throwaway address to send to and plays back the entire evaluation step by step, including alignment, which makes it the fastest way to diagnose a provider that signs with the wrong domain.
The order to do things in
Inventory every sender, then get aligned DKIM working for each one, because that is the mechanism that both survives forwarding and drives alignment. Tidy SPF into a single record under ten lookups with ~all. Publish DMARC at p=none with aggregate reports going somewhere a human or a parser will read them. Give it a few weeks, fix what the reports surface, then step through quarantine to reject. Lock every parked domain with the three-record treatment while you are at it.
None of the individual steps is difficult. The value is in finishing: a domain at p=reject with clean reports cannot be impersonated at any receiver that honours DMARC, and its legitimate mail carries the authentication that inbox providers now demand. Set a reminder to skim the reports monthly, and the whole thing runs itself.
Related articles
Tutorials PHP JavaScript Cyber Security
Understanding Cross-Site Scripting (XSS) Attacks
Cross-Site Scripting (XSS), one of the most prevalent web application vulnerabilities. Learn through examples, see how it affects PHP and JavaScript-based applications, and get insights into preventing these attacks.