All posts

Move DMARC from p=none to p=reject

A step-by-step path from a DMARC record that only reports to one that rejects spoofed mail, with the RFC 9989 changes and the checks for each step.

Three DMARC TXT records for _dmarc.example.com, stepping from p=none to p=quarantine to p=reject

A DMARC record with p=none meets the DMARC requirement that Gmail and Yahoo set for bulk senders. But p=none does not ask receivers to quarantine or reject mail that fails DMARC. Moving to p=reject asks them to refuse mail that uses your domain without authentication. The move is safe when you do it in steps and let the reports tell you when to take the next one.

This guide follows RFC 9989, the DMARC standard published in May 2026. It replaces RFC 7489, and it changes how you test a policy.

How a receiver decides

DMARC passes when a message passes SPF or DKIM and the passing domain aligns with the domain in the visible From: header.

  • SPF checks the envelope sender (the return path). It aligns when that domain matches the From: domain.
  • DKIM checks a signature. It aligns when the d= domain of the signature matches the From: domain.

Alignment is relaxed by default: mail.example.com aligns with example.com because both share the organizational domain. Strict alignment (aspf=s, adkim=s) requires an exact match.

A message from a newsletter tool can pass SPF for the tool's own bounce domain and still fail DMARC, because that domain does not align with yours. Look for this pattern in the reports.

Step 1: publish p=none with a report address

Start with a record that asks for aggregate reports and requests no enforcement:

_dmarc.example.com.  TXT  "v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com"

Check what is published:

dig +short TXT _dmarc.example.com

The rua tag matters most. RFC 9989 §4.7 says receivers must not send aggregate reports for a domain without it. Use a mailbox you read, or a report-processing service. If the report address is in another domain, that domain must confirm that it accepts your reports (RFC 9990).

RFC 9989 explains that domain owners usually start with p=none to make sure nothing was missed in the SPF and DKIM setup (§5.1.4 and §5.1.5).

Step 2: read the aggregate reports

Receivers send aggregate reports as XML, usually once a day. Each record says which IP address sent mail with your domain in From:, how many messages, and how SPF, DKIM and DMARC evaluated them. The format is in RFC 9990. One record looks like this (the addresses are examples):

<record>  <row>    <source_ip>203.0.113.25</source_ip>    <count>42</count>    <policy_evaluated>      <disposition>none</disposition>      <dkim>fail</dkim>      <spf>fail</spf>    </policy_evaluated>  </row>  <identifiers>    <header_from>example.com</header_from>  </identifiers>  <auth_results>    <spf>      <domain>bounces.example.net</domain>      <result>pass</result>    </spf>  </auth_results></record>

SPF passed for bounces.example.net, but that domain does not align with example.com, so DMARC evaluated SPF as fail. Without an aligned DKIM signature, these 42 messages fail DMARC.

A short script lists the sources that fail. It reads one report file and prints the failing IP addresses with their message counts. Reports in the RFC 9990 format put every element in the namespace urn:ietf:params:xml:ns:dmarc-2.0, and older reports use no namespace, so the script compares element names without the namespace:

import sysimport xml.etree.ElementTree as ET

def name(element):    return element.tag.rsplit("}", 1)[-1]

def child(element, wanted):    return next(item for item in element if name(item) == wanted)

root = ET.parse(sys.argv[1]).getroot()for record in (item for item in root.iter() if name(item) == "record"):    row = child(record, "row")    evaluated = child(row, "policy_evaluated")    if child(evaluated, "dkim").text != "pass" and child(evaluated, "spf").text != "pass":        print(child(row, "source_ip").text, child(row, "count").text)

Run it with python3 failing_sources.py report.xml. Reports often arrive compressed; unpack them first. An empty output means that no record in that file failed both checks.

For every source in that list, find out what it is. Your mail platform, your helpdesk and your billing tool are legitimate. Fix each one with aligned DKIM, aligned SPF, or both. If a tool cannot sign with your domain, move it to its own subdomain, for example billing.example.com, and give that subdomain its own authentication and DMARC policy. A subdomain alone does not fix alignment.

A source you do not recognise is not always a forger. Mail that is forwarded, or sent through a mailing list that changes the message, can fail DMARC even when the original sender is legitimate. RFC 9989 discusses these indirect mail flows and what enforcement does to them (§7.3 and §7.4). Investigate unknown sources before you enforce. RFC 9989 also warns that it can take many months of reports before a domain owner is sure that all legitimate mail is authenticated (§5.1.7). Give this step the time it needs.

Step 3: quarantine, tested first

RFC 7489 used pct to apply a policy to part of the mail. RFC 9989 removes pct, because receivers applied it reliably only at 0 and 100. It adds the t tag (testing) instead (Appendix A.6):

  • p=quarantine; t=y asks receivers to treat failing mail as if the policy were none, while you test.
  • p=reject; t=y asks receivers to treat failing mail as if the policy were quarantine.

Not every receiver implements t yet, so treat it as a request. The real quarantine step is the record without t:

_dmarc.example.com.  TXT  "v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@example.com"

Receivers are now asked to treat failing mail as suspicious, for example by putting it in the spam folder. Each receiver decides how to apply the request. Watch the reports for a legitimate source that starts to fail. If one appears, fix it, or go back to p=none while you do.

Step 4: reject

When the reports show only sources you know and they all pass, publish the reject policy. It asks receivers to refuse failing mail, and each receiver still decides how to apply it:

_dmarc.example.com.  TXT  "v=DMARC1; p=reject; rua=mailto:dmarc-reports@example.com"

Two more tags control subdomains. sp= sets the policy for existing subdomains, and np= sets it for subdomains that do not exist in DNS (§4.7). Without them, p= applies to both. Keep rua after you reach p=reject: a new tool that someone adds next year shows up in the reports first.

If something breaks, step back to p=quarantine or p=none. DNS changes take effect within the record's TTL, so a short TTL during the move makes a rollback faster.

What Sendtier does

When you add a domain, Sendtier gives you three DNS records:

RecordNameValue
DKIM TXTst1._domainkey.<domain>v=DKIM1; k=rsa; p=<public key>
Return-path CNAMEsend.<domain>rp.<sending zone>
DMARC TXT_dmarc.<domain>v=DMARC1; p=none;

Sendtier signs each message with a 2048-bit RSA DKIM key for your domain, so the DKIM domain aligns with your From: domain. The return-path CNAME points the envelope sender at Sendtier's bounce host, whose SPF record lists Sendtier's sending IPs. That return-path domain is a subdomain of yours, so SPF also aligns under relaxed alignment. Mail that you send through Sendtier passes DMARC on both checks once the records are published.

The suggested DMARC record is only a starting point. Verification checks that a record exists at _dmarc.<domain> and that it starts with v=DMARC1. It does not require a policy value, so p=quarantine and p=reject verify too. If you already have a DMARC record, keep it.

What Sendtier does not do yet

Sendtier does not show DMARC aggregate reports to customers. There is no report view in the API or the dashboard, and the suggested record has no rua tag, so you choose where reports go. Use your own mailbox or a report service until that changes.

Sources were checked on 15 October 2026: RFC 9989, RFC 9990, Google's email sender guidelines and Yahoo's sender best practices, which both accept p=none for bulk senders.

Sendtier