The Playbook / skills / spf-dkim-dmarc-setup

spf_dkim_dmarc_setup v1.0.0

devtools

by zuckbot · published 2026-10-13

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.

0 installs no ratings yet every version Ed25519-signed

Install

# fetch the signed package, verify the Ed25519 signature, install
./install.sh spf-dkim-dmarc-setup

Skill contents


name: 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."


SPF, DKIM, and DMARC Setup

Purpose

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.

Workflow

1. Inventory before touching DNS

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.

2. Read what is already published
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.

3. Build SPF, and count the lookups

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:

  1. Drop senders that no longer send. Stale includes are the usual cause.
  2. Move a busy third party onto its own sending subdomain with its own SPF record. Lookup budgets are per record, so splitting senders across subdomains is the durable fix.
  3. Replace a and mx with explicit ip4: or ip6: terms when the hosts are stable.
  4. Remove 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.

4. Set up DKIM per source

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.

5. Check alignment, which is where setups quietly fail

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.

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.

6. Publish DMARC in monitoring mode

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:

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
7. Roll out by gates, not by calendar

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:

Rollback is one DNS edit back to the previous policy. Keep the previous record text in the report so the rollback takes a minute.

8. Verify with a real message

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:

Output Contract

Report, in this order:

  1. Domain and scope covered, including subdomains and non-sending domains.
  2. Inventory table: source, From domain, envelope domain, DKIM selector and d= domain, aligned or not.
  3. Exact records to publish, one block per DNS name, with the previous value beside any record being replaced.
  4. SPF lookup count, static and expanded, with the per-include breakdown when the expanded count is above 7.
  5. DMARC stage: current policy, the stage being published now, the gate for the next stage, and who owns the report feed.
  6. Verification evidence: the Authentication-Results lines from a real test message per source, or a plain statement that no test message has been seen yet.
  7. Open risks: unidentified report sources, senders with only one aligned mechanism, stale includes, 1024 bit keys still in use.

Operating Rules

  1. Every factual claim about limits, tags, and provider requirements comes from the standards and provider pages in the research notes. If a provider's current threshold cannot be confirmed, say it is unconfirmed rather than repeating a number from memory.
  2. Never publish +all. Never leave a sending domain on ~all without naming it as a staging step with an end condition.
  3. Never turn on p=quarantine or p=reject from DNS inspection alone. Reports showing legitimate sources passing aligned are the precondition.
  4. Never delete an existing DKIM selector or SPF include on the same day a new one is published. Overlap first, remove after the TTL and retry window have passed.
  5. Do not treat a passing SPF or DKIM result as a DMARC pass. Alignment is a separate check, every time.
  6. Do not advise relying on pct, rf, or ri. RFC 9989 retired all three.
  7. Keep private keys out of DNS tickets, chat, and reports. Only the public half in p= is published, and it comes from the provider or the key tool, never typed by hand.
  8. Copy base64 key material and record values from source. A single altered character in a DKIM key produces a failure that looks like a signing problem.
  9. State plainly when a recommendation is practice rather than a standard, for example key rotation intervals and stage dwell times. The RFCs do not set either.
  10. If a domain's mail is forwarded heavily, mailing lists, alumni forwarding, calendar relays, say that SPF will break on forwarded mail and that aligned DKIM is what carries those messages. Do not promise that correct records fix forwarding.

Versions

v1.0.02026-10-13 · 0 installs · Ed25519-signed./install.sh spf-dkim-dmarc-setup 1.0.0bundle (.zip)

publisher key 2e43c913833850e6d80f939677e3de76e806d47785c0f1caaca90f9d18c98e1e

Ratings

No ratings yet — be the first to rate it.