SPF and DMARC are two DNS records that tell other mail servers how to treat messages claiming to come from your domain. SPF lists the servers allowed to send mail for the domain. DMARC ties SPF and DKIM to the visible From address, tells receivers what to do with mail that fails, and sends you reports about who is sending as your domain. In this tutorial you will inventory your mail sources, publish an SPF record and a DMARC record, verify both, and tighten the DMARC policy step by step until spoofed mail is rejected.
Prerequisites
To follow this tutorial, you need:
- A domain you control, with access to its DNS zone. This guide uses
your_domain; replace it with your own. - A mail server that sends for the domain, for example Postfix on a CubePath VPS running Ubuntu 24.04, with its public IP address. This guide uses
203.0.113.10asyour_server_ip. - DKIM signing already working, as described in How to Set Up DKIM with OpenDKIM and Postfix. DMARC works with SPF alone, but forwarded mail usually breaks SPF, so DKIM is what keeps legitimate mail passing.
- The
digcommand, provided by thebind9-dnsutilspackage on Ubuntu (sudo apt install bind9-dnsutils).
How SPF, DKIM and DMARC fit together
| Record | Where it lives | What it checks |
|---|---|---|
| SPF | TXT on your_domain | The sending IP is authorized for the envelope sender domain (the MAIL FROM, also called Return-Path) |
| DKIM | TXT on selector._domainkey.your_domain | The message carries a valid signature from the domain |
| DMARC | TXT on _dmarc.your_domain | SPF or DKIM passed and the domain that passed matches the From header domain |
The last point is called alignment, and it is what makes DMARC useful. A spammer can pass SPF for their own domain while putting your domain in the From header; DMARC fails that message because the domains do not match. A message passes DMARC when at least one of SPF or DKIM passes and is aligned.
Step 1 - Listing everything that sends mail as your domain
An SPF record that forgets a legitimate sender causes that sender's mail to fail. Before writing anything, list every system that sends with an @your_domain address:
- Your own mail server (its IPv4 and, if it sends over IPv6, its IPv6 address).
- A hosted mailbox provider such as Google Workspace or Microsoft 365.
- Newsletter and transactional services (Mailchimp, Brevo, Amazon SES, Postmark and so on).
- Web applications, monitoring tools, printers and anything else that sends notifications.
Each provider documents the SPF include: value to use, and most also let you set up DKIM for your domain. Check whether an SPF record already exists, because a domain must have only one:
dig +short TXT your_domain
"google-site-verification=..."
If you see a string starting with v=spf1, edit that record in the next step instead of creating a second one. Two SPF records make SPF return permerror and fail for all mail.
Step 2 - Building the SPF record
An SPF record starts with v=spf1, lists the allowed senders as mechanisms, and ends with an all mechanism that says what to do with everything else. The mechanisms you will use most:
| Mechanism | Meaning |
|---|---|
ip4:203.0.113.10 | This IPv4 address (a CIDR range like 203.0.113.0/28 also works) |
ip6:2001:db8::10 | This IPv6 address or range |
mx | The servers listed in the domain's MX records |
a | The IP addresses of the domain's A/AAAA record |
include:_spf.google.com | Everything the other domain's SPF record authorizes |
-all | Fail everything not listed |
~all | Soft fail everything not listed (accepted but treated as suspicious) |
For a domain whose mail is sent only by your own server:
v=spf1 ip4:203.0.113.10 -all
For your own server plus Google Workspace and Amazon SES:
v=spf1 ip4:203.0.113.10 include:_spf.google.com include:amazonses.com -all
Keep these rules in mind:
- Ten DNS lookups at most. Every
include,a,mx,existsandredirectcosts a lookup, including the ones nested inside included records. Past ten, SPF returnspermerror.ip4andip6cost nothing, so prefer them for your own servers. - Use
-allor~all, never+all, which authorizes the whole Internet. With DMARC in place,~allis common and safe, because DMARC decides the final outcome. Use-allonce you are sure the list is complete. - Do not use
ptr. It is deprecated and many receivers ignore it.
For domains and subdomains that never send mail, publish a record that authorizes nothing, so nobody can use them for spoofing:
v=spf1 -all
It is also good practice to give your server's hostname (the name it uses in HELO, such as mail.your_domain) its own record, because some receivers check SPF on the HELO name:
v=spf1 a -all
Step 3 - Publishing and verifying the SPF record
In your DNS provider, create or edit the TXT record on the domain root:
| Field | Value |
|---|---|
| Type | TXT |
| Name / Host | @ (or your_domain) |
| Value | v=spf1 ip4:203.0.113.10 -all |
Add the record for mail.your_domain the same way if you use one. After a few minutes, query it:
dig +short TXT your_domain | grep spf1
"v=spf1 ip4:203.0.113.10 -all"
To see the result a receiver reaches, send a message from your server to a Gmail address and open Show original. The authentication summary should read:
SPF: PASS with IP 203.0.113.10
Step 4 - Creating the DMARC record
A DMARC record is a TXT record on _dmarc.your_domain made of tag=value pairs:
| Tag | Meaning |
|---|---|
v=DMARC1 | Version, must come first |
p= | Policy for the domain: none (monitor only), quarantine (send to spam) or reject |
rua= | Address that receives daily aggregate reports, as a mailto: URI |
sp= | Policy for subdomains; defaults to the value of p |
adkim= / aspf= | Alignment mode: r (relaxed, the default: mail.your_domain aligns with your_domain) or s (strict, exact match) |
Start with a monitoring policy. It changes nothing for delivery but gives you reports on every source that sends as your domain:
v=DMARC1; p=none; rua=mailto:dmarc-reports@your_domain
Create a mailbox or alias for dmarc-reports@your_domain first. Large receivers send one compressed XML report per day, and busy domains get many, so a dedicated address keeps them out of personal inboxes.
NoteIf the reports go to an address on a different domain, that domain must authorize it by publishing a TXT record
your_domain._report._dmarc.other_domainwith the valuev=DMARC1. Otherwise receivers will not send the reports there.
Step 5 - Publishing and verifying the DMARC record
Create the TXT record:
| Field | Value |
|---|---|
| Type | TXT |
| Name / Host | _dmarc (or _dmarc.your_domain) |
| Value | v=DMARC1; p=none; rua=mailto:dmarc-reports@your_domain |
Verify it:
dig +short TXT _dmarc.your_domain
"v=DMARC1; p=none; rua=mailto:dmarc-reports@your_domain"
Send another test message to Gmail and check Show original:
SPF: PASS with IP 203.0.113.10
DKIM: 'PASS' with domain your_domain
DMARC: 'PASS'
Step 6 - Reading the aggregate reports
After a day or two, reports start arriving as .zip or .xml.gz attachments. Each one lists, per sending IP, how many messages the receiver saw and whether SPF and DKIM passed and aligned. You can inspect one on the server with standard tools:
zcat google.com!your_domain!1758758400!1758844799.xml.gz | less
The important part of each <record> looks like this:
<row>
<source_ip>198.51.100.25</source_ip>
<count>42</count>
<policy_evaluated>
<disposition>none</disposition>
<dkim>fail</dkim>
<spf>fail</spf>
</policy_evaluated>
</row>
For each source IP that fails, decide whether it is legitimate:
- A service you use (a CRM, a newsletter tool): add its
include:to SPF and set up DKIM for your domain in that service. - Forwarded mail: SPF breaks on forwarding, but DKIM usually survives. If DKIM passes, there is nothing to fix.
- Unknown IPs: most likely spoofing. This is exactly what the policy will block.
Reading XML by hand does not scale. For regular use, send the reports to a DMARC analysis service or run an open source parser such as parsedmarc, which turns them into dashboards.
Step 7 - Moving to quarantine and reject
Once the reports show that all legitimate sources pass DMARC for a couple of weeks, tighten the policy. Change the record to quarantine, so failing mail goes to spam:
v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@your_domain
Watch the reports for another two to four weeks. If no legitimate mail fails, switch to reject, so failing mail is refused outright:
v=DMARC1; p=reject; rua=mailto:dmarc-reports@your_domain
Keep rua in place after reaching reject. The reports are how you notice a new service someone in your organization starts using without configuring SPF and DKIM.
For domains that never send mail, you can publish the strict policy right away, together with v=spf1 -all:
v=DMARC1; p=reject
Troubleshooting
SPF: PERMERRORin the headers. The domain has two SPF records, or the record needs more than ten DNS lookups. Merge the records into one and replacea/mxlookups for your own servers withip4/ip6.- SPF passes but DMARC fails. The envelope sender domain differs from the
Fromdomain, which is common with third-party senders that use their own bounce domain. Configure DKIM for your domain in that service, or set up its custom return-path option. - No DMARC reports arrive. Check the
ruaaddress for typos, confirm the mailbox receives external mail, and, if it is on another domain, publish the_report._dmarcauthorization record. Remember that reports are sent once a day. - Legitimate mail lands in spam after switching to
quarantine. Look up the source IP in the reports, fix its SPF or DKIM, and go back top=nonein the meantime if the impact is large.
Conclusion
Your domain now publishes an SPF record listing its legitimate senders and a DMARC policy that receivers enforce, with daily reports that show who sends as your domain. Together with DKIM, this is the authentication baseline that Gmail, Yahoo and Microsoft expect from senders. Next, make sure your server's reverse DNS matches its hostname and review the other deliverability factors in How to Prevent Your Emails from Going to Spam.
