Set up and fix SPF, DKIM, and DMARC for a sending domain: inventory senders, build the three records, stay under the 10 lookup SPF limit, get alignment with the visible From domain, and roll DMARC out from p=none monitoring to quarantine and reject without blocking real mail. Use when a domain needs email authentication, mail is landing in spam or being rejected with errors like Gmail 5.7.26 or Outlook 550 5.7.515, a bulk sender requirement must be met, a new sending service is being added, or DMARC reports show unknown or misaligned sources.
./install.sh spf-dkim-dmarc-setupname: spf_dkim_dmarc_setup description: "Set up and fix SPF, DKIM, and DMARC for a sending domain: inventory senders, build the three records, stay under the 10 lookup SPF limit, get alignment with the visible From domain, and roll DMARC out from p=none monitoring to quarantine and reject without blocking real mail. Use when a domain needs email authentication, mail is landing in spam or being rejected with errors like Gmail 5.7.26 or Outlook 550 5.7.515, a bulk sender requirement must be met, a new sending service is being added, or DMARC reports show unknown or misaligned sources."
Publish three DNS records that work together: SPF says which hosts may send for the envelope domain, DKIM signs mail so receivers can verify it was not altered and came from a domain that holds a key, and DMARC ties both to the visible From domain and tells receivers what to do when neither aligns. The deliverable is records that pass, align, and are staged so enforcement does not block legitimate mail.
Helper: bin/check_auth.py validates record strings and alignment offline, with no network access. Tag tables, limits, and four worked setups are in references/record-reference.md. Read that file before building records for an unfamiliar domain shape.
List every source that sends with this domain: staff mail, transactional mail, marketing platform, CRM, helpdesk, invoicing, monitoring alerts, calendar invites, forwarded mail from websites. For each one record the From domain it uses, who operates it, and whether it supports a custom MAIL FROM subdomain and DKIM signing in your domain.
Also list subdomains that send, and domains owned by the same organization that send nothing. Non-sending domains need defensive records too, see step 6.
Do not skip this. Every failed rollout seen in practice is an inventory failure: enforcement turns on, then a forgotten invoicing tool gets blocked.
bash
dig +short TXT example.com # SPF lives at the apex, among other TXT records
dig +short TXT _dmarc.example.com # DMARC
dig +short TXT google._domainkey.example.com # DKIM, one query per known selector
dig +short MX example.com
Save the current values before changing anything, so a rollback is a paste, not a reconstruction. Watch for two TXT records starting v=spf1 at one name, which is a permerror, and for a DMARC record at the wrong name.
Selectors cannot be discovered by scanning DNS. Get them from each provider's setup screen, or from the s= field in the DKIM-Signature header of a real message that source sent.
One record, starting v=spf1, ending in -all for domains that send mail. Use ~all only as a short staging step while you confirm the inventory, and say so when you do.
Count DNS-causing terms: include, a, mx, ptr, exists, and redirect each cost one, and terms inside an included record cost too. The limit is 10 for the fully expanded record, and passing it returns permerror. RFC 7208 section 4.6.4 also caps void lookups at 2.
bash
python3 bin/check_auth.py --spf "v=spf1 include:_spf.google.com include:sendgrid.net -all"
The script reports the static count, which is a floor. To get the real number, expand each include by hand with dig +short TXT <included-domain> and count the DNS terms in what comes back. One include from a large provider can cost several lookups on its own.
When the count is the problem, fix it in this order:
a and mx with explicit ip4: or ip6: terms when the hosts are stable.ptr. It is deprecated and slow.Avoid automatic SPF flattening as a first move. Flattened records go stale when a provider changes its ranges, and then mail fails with no change on your side. If flattening is used, it must be a service that republishes on a schedule, and that dependency belongs in the report.
Generate or obtain a 2048 bit RSA key for each signing source. RFC 8301 sets 1024 bits as the floor and 2048 as what signers should use, and it forbids rsa-sha1, so confirm the signer uses rsa-sha256.
Publish the public key at <selector>._domainkey.<domain>. A 2048 bit key will not fit in one 255 character TXT string, so publish it as chunked strings in a single record. Then enable signing at the provider and confirm the selector in the header of a live message, not just in the provider's console.
bash
python3 bin/check_auth.py --dkim "v=DKIM1; k=rsa; p=<base64 key from the provider>"
The script flags an empty p= (revoked key), a key that decodes to a 1024 bit size, test mode t=y, and a corrupt base64 value.
Rotate with a new selector rather than by editing the existing record: publish the new selector, switch the signer, wait past the TTL and the longest retry window you care about, then revoke the old key by publishing it with an empty p=, or delete it. Add Ed25519 (RFC 8463) only as a second signature alongside RSA. Receiver support is uneven, so Ed25519 alone leaves some receivers with nothing to verify.
DMARC passes when SPF or DKIM passes and the passing identifier aligns with the domain in the visible From header. Both can pass and DMARC still fails, when a platform sends with its own envelope domain and signs with its own domain.
d= domain aligns with the From domain.send.example.com aligns with example.com. Strict requires an exact match. Set strict only when you control every subdomain sender.bash
python3 bin/check_auth.py --align --from-domain example.com \
--mailfrom-domain bounce.platform.example.net --dkim-domain example.com \
--spf-pass --dkim-pass
Aim for both mechanisms aligned. One aligned mechanism passes DMARC but leaves no margin: a key rotation mistake or an envelope change then takes delivery down.
The usual fix order is custom DKIM signing in your domain first, because it survives forwarding better than SPF, then a custom MAIL FROM subdomain under your domain so SPF aligns as well.
At _dmarc.<domain>:
v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com; adkim=r; aspf=r; fo=1
Current standard is RFC 9989 with aggregate reporting in RFC 9990 and failure reporting in RFC 9991, which replaced RFC 7489 in May 2026. Records still start v=DMARC1.
Three details that cost people weeks:
rua must point at a mailbox or report service that actually parses the XML. Unread reports make p=none permanent and useless.<your-domain>._report._dmarc.<report-domain> with the value v=DMARC1. Without it, receivers that check may discard your reports.For a domain that sends no mail, skip monitoring and publish enforcement plus a null SPF policy:
example.com TXT v=spf1 -all
_dmarc.example.com TXT v=DMARC1; p=reject; sp=reject; np=reject; rua=mailto:dmarc-reports@example.com
Move only when the gate is met. Typical dwell is a couple of weeks per stage because you need at least one full sending cycle, including monthly and quarterly mail, but the gate is the evidence, not the date.
| Stage | Record change | Gate to move on | |---|---|---| | Monitor | p=none with rua | Every source in the reports is identified, and every legitimate source passes aligned | | Quarantine | p=quarantine | No legitimate source has failed aligned in the last several report cycles | | Reject | p=reject | Same, and someone with authority has accepted that unknown senders will be refused | | Subdomains | add sp=reject and np=reject | Subdomain inventory is complete, including subdomains that send nothing |
Notes on staging controls:
pct was the old percentage lever, and older guides lean on it. RFC 9989 retired pct, so migrated receivers ignore it and unmigrated ones may honor it. Do not make pct your only safety control.rua in the record at every stage, including after p=reject. Reports are how you catch the next tool someone adds.Rollback is one DNS edit back to the previous policy. Keep the previous record text in the report so the rollback takes a minute.
DNS records alone prove nothing about production mail. Send from each source to a mailbox you control and read the Authentication-Results header. You want dmarc=pass with the From domain you expected, plus dkim=pass and spf=pass naming your domains. A sample header and how to read it are in the reference file.
bash
python3 bin/check_auth.py --dmarc "v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com"
Then check the requirement that triggered the work, if there was one:
p=none as the floor, From alignment, one-click unsubscribe on marketing mail, and a spam rate under 0.3 percent. Google describes 0.10 percent as the level to stay under.p=none or stricter with alignment. Non-compliant mail is rejected with 550 5.7.515.Report, in this order:
d= domain, aligned or not.+all. Never leave a sending domain on ~all without naming it as a staging step with an end condition.p=quarantine or p=reject from DNS inspection alone. Reports showing legitimate sources passing aligned are the precondition.pct, rf, or ri. RFC 9989 retired all three.p= is published, and it comes from the provider or the key tool, never typed by hand.publisher key 2e43c913833850e6d80f939677e3de76e806d47785c0f1caaca90f9d18c98e1e
No ratings yet — be the first to rate it.