To set up DMARC, first make sure SPF or DKIM passes and aligns with your From domain, then publish a TXT record at _dmarc.example.com such as v=DMARC1; p=none; rua=mailto:dmarc@example.com. Collect the aggregate reports for a few weeks, fix every legitimate sender that fails, and only then tighten the policy to quarantine and eventually reject.
DMARC (RFC 7489) ties SPF and DKIM to the address people actually see in the From header, tells receivers what to do with mail that fails, and asks them to send you reports. Without it, anyone can put your domain in a From header and SPF and DKIM have nothing to say about it.
We use example.com throughout.
What DMARC protects against, and what it doesn't
DMARC stops other people from sending mail with your exact domain in the From header, once you enforce it. That covers the most damaging kind of spoofing: fake invoices or password-reset messages that appear to come from billing@example.com.
It does not stop lookalike domains such as examp1e.com or example-billing.com, and it does not stop someone using your company name as a display name with an unrelated address. Those need user awareness and, sometimes, registering the obvious lookalikes yourself.
DMARC also does not improve placement on its own. It tells receivers your mail is genuinely yours; whether they put it in the inbox still depends on your reputation. What it does do is make your authentication consistent and visible, which is the foundation everything else rests on, and it is required for bulk senders to Gmail and Yahoo.
Step 1: Get SPF and DKIM passing and aligned
DMARC passes if either of these is true:
- SPF passes, and the envelope sender (Return-Path) domain aligns with the From domain.
- DKIM passes, and the signature's
d=domain aligns with the From domain.
"Aligns" means the same organizational domain in relaxed mode (the default), or the exact same domain in strict mode.
| From | Return-Path | DKIM d= | Relaxed result | Strict result |
|---|---|---|---|---|
| hello@example.com | bounce@example.com | example.com | pass | pass |
| hello@example.com | bounce@mail.example.com | example.com | pass | pass (via DKIM) |
| hello@example.com | bounce@provider.example | provider.example | fail | fail |
The last row is the classic problem: a provider passes SPF and DKIM for its own domain, and DMARC still fails for yours. Fix it by completing the provider's custom domain setup so it signs with d=example.com.
Check a real message from each sender before publishing anything. In Gmail, open it, choose "Show original" and look for dkim=pass header.i=@example.com and spf=pass.
Step 2: Create a mailbox for reports
Reports arrive as compressed XML attachments, daily, from every receiver that processes your mail. Use a dedicated address. On our own servers, dmarc@ delivers to a local mailbox that nothing else uses, so reports never clutter a human inbox.
If you forward it instead, a Postfix virtual alias is one line in /etc/postfix/virtual:
dmarc@example.com reports@your-inbox.example
sudo postmap /etc/postfix/virtual && sudo systemctl reload postfix
Step 3: Publish a p=none record
| Field | Value |
|---|---|
| Type | TXT |
| Host | _dmarc |
| Value | v=DMARC1; p=none; rua=mailto:dmarc@example.com |
That is enough to start. The tags you are likely to use:
| Tag | Example | Meaning |
|---|---|---|
v |
DMARC1 |
Version. Must come first. |
p |
none / quarantine / reject |
Policy for the domain |
rua |
mailto:dmarc@example.com |
Where to send aggregate reports |
sp |
reject |
Policy for subdomains (defaults to p) |
adkim |
r or s |
DKIM alignment: relaxed (default) or strict |
aspf |
r or s |
SPF alignment: relaxed (default) or strict |
pct |
25 |
Apply the policy to a percentage of failing mail |
ruf |
mailto:... |
Forensic reports; few receivers send them |
Our own domains, which only send from servers we control, run:
v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com; adkim=s; aspf=s
Strict alignment suits us because every message is signed with the exact From domain. If you use providers that send from subdomains, stay relaxed.
Reports on another domain
If rua points at a different domain, for example a reporting service, that domain must publish an authorization record or receivers will not send reports:
example.com._report._dmarc.reports.example.net TXT "v=DMARC1"
Reporting services set this up on their side.
Step 4: Read the aggregate reports
Each report lists, per sending IP: how many messages, the SPF and DKIM results, whether they aligned, and what the receiver did. A trimmed record looks like this:
<record>
<row>
<source_ip>203.0.113.10</source_ip>
<count>42</count>
<policy_evaluated>
<disposition>none</disposition>
<dkim>pass</dkim>
<spf>pass</spf>
</policy_evaluated>
</row>
<identifiers>
<header_from>example.com</header_from>
</identifiers>
</record>
Sort the source IPs into three groups:
- Yours and passing. Good.
- Yours and failing. A forgotten tool, a provider without custom DKIM, or a forwarder. Fix these before enforcing.
- Not yours. Spoofing or spam using your domain. This is what enforcement will stop.
Reading XML by hand gets old fast. A parser or reporting tool that groups by IP and sender makes this a five-minute weekly job. For a quick look without any tool, unpack the attachment (unzip for .zip, gunzip for .gz) and print one line per row:
python3 - report.xml <<'EOF'
import sys, xml.etree.ElementTree as ET
root = ET.parse(sys.argv[1]).getroot()
for r in root.iter('record'):
row = r.find('row')
pe = row.find('policy_evaluated')
print(row.findtext('source_ip'), row.findtext('count'),
'dkim=' + pe.findtext('dkim'), 'spf=' + pe.findtext('spf'),
r.findtext('identifiers/header_from'))
EOF
To identify an unfamiliar source IP, dig +short -x on it usually shows the provider's hostname.
Example DMARC records for common situations
| Situation | Record |
|---|---|
| Just starting, collecting reports | v=DMARC1; p=none; rua=mailto:dmarc@example.com |
| Reports clean, first enforcement | v=DMARC1; p=quarantine; pct=25; rua=mailto:dmarc@example.com |
| Fully enforced | v=DMARC1; p=reject; rua=mailto:dmarc@example.com |
| Enforce on root, still testing subdomains | v=DMARC1; p=reject; sp=none; rua=mailto:dmarc@example.com |
| Domain that never sends mail | v=DMARC1; p=reject (with SPF v=spf1 -all) |
A subdomain without its own _dmarc record inherits the organizational domain's policy, using sp= if it is set and p= otherwise. A subdomain can also publish its own record, which overrides the inherited one.
Forwarding and mailing lists
Two kinds of legitimate mail can fail DMARC through no fault of yours:
- Forwarding. When someone forwards mail from an old address to a new one, the forwarder's IP is not in your SPF record. DKIM still passes if the message is unchanged, so DMARC still passes. This is why DKIM matters even when SPF is perfect. Forwarders that use SRS, as our own server does with postsrsd, stop SPF failing outright, but the SPF pass is then for the forwarder's domain, so for DMARC it is still DKIM that carries the message.
- Mailing lists. Lists that add a footer or a subject tag break the DKIM signature. Many list servers now rewrite the From address to their own domain for senders with strict policies, which avoids the failure.
You will see both in your reports as small numbers of failures from IPs you do not recognize. They are not a reason to stay at p=none forever, but they are a reason to check before assuming every failure is spoofing.
Do not forget parked domains
Domains you own but never send from are easy targets, because nobody watches them. Give each one v=spf1 -all at the root and v=DMARC1; p=reject at _dmarc. There is no rollout needed: if a domain sends nothing legitimate, nothing legitimate can break. Add an rua address if you want to see who tries.
Step 5: Tighten the policy
When reports show only your own senders, all passing, for a few weeks, move to p=quarantine, then p=reject. The detailed rollout, with pct, subdomain policy and what to watch at each stage, is in DMARC p=none to p=reject.
Common mistakes
- No
ruaaddress.p=nonewithout reports does nothing at all. The reports are the point. - Jumping straight to p=reject. A forgotten sender (invoices, the helpdesk) starts bouncing immediately, and you find out from customers.
- Publishing on the wrong host. The record goes at
_dmarc, not at the root. A DMARC record atexample.comitself is ignored. - Two DMARC records. Like SPF, only one is allowed. Two records means receivers act as if you have none.
- Assuming SPF pass means DMARC pass. Without alignment, it does not. Check
header.fromagainst the authenticated domains. - Reports to an outside domain without authorization. Add the
_report._dmarcrecord, or use a mailbox on your own domain.
How to verify it worked
dig +short TXT _dmarc.example.com
You should see exactly one string starting with v=DMARC1. Then send a test through each sender to port25's verifier (check-auth@verifier.port25.com) and to a Gmail address. In Gmail's "Show original", look for dmarc=pass. From your own server:
swaks --to check-auth@verifier.port25.com --from hello@example.com \
--server mail.example.com --port 587 --tls \
--auth LOGIN --auth-user hello@example.com
mail-tester.com also reports the DMARC record and result. Our email checker shows SPF, DKIM and DMARC for a domain in one place. Within a day or two the first aggregate reports should arrive at your rua mailbox.
DMARC is also one of the things the Gmail and Yahoo bulk sender requirements check. If you want someone to set it up, read the reports and walk you to enforcement, that is part of our Deliverability Fix; see the pricing page for what each plan includes.