is it dns? / wiki ← back to the live monitor

MX, SPF, DKIM, DMARC: the four records every mail-receiving domain needs

Four DNS records, ordered from "where does mail go" to "what should the recipient do if it doesn't look right." Each adds forge-resistance on top of the previous one. None of them are optional once you start receiving mail at a public domain.

The four records, at a glance#

RecordLives atWhat it answers
MXexample.com (apex, or any name)Where does inbound mail for @example.com go?
SPF (a TXT)example.com (apex)Which IPs are allowed to send as @example.com?
DKIM (a TXT)<selector>._domainkey.example.comThe public key that verifies signatures on outbound mail.
DMARC (a TXT)_dmarc.example.comWhat should the recipient do when SPF or DKIM doesn't align with the visible From:?

Each one without the others is brittle. All four together is the modern minimum.

MX: where does mail go?#

A receiving mailserver looking to deliver you@example.com queries MX, gets a list of hostnames each carrying a preference value (the sender tries the lowest number first), then resolves each to an A/AAAA, then makes an SMTP connection.

example.com.   MX   10   mx1.mailprovider.com.
example.com.   MX   10   mx2.mailprovider.com.
example.com.   MX   20   backup-mx.mailprovider.com.

The single biggest mistake: pointing an MX at a CNAME. MX targets must be A/AAAA, not CNAMEs (RFC 2181 §10.3).

Having no MX record at all does not stop mail: senders fall back to an implicit MX with preference 0 pointing at the domain's A/AAAA record (RFC 5321 §5.1). So if your domain never receives mail, publish a null MX (RFC 7505), a single MX 0 ., so senders bounce immediately instead of retrying for days or delivering to whatever the A record points at.

example.com.   MX   0   .

Query a live domain's MX records:

dig @1.1.1.1 cloudflare.com MX +short
5 mxa-canary.global.inbound.cf-emailsecurity.net.
5 mxb-canary.global.inbound.cf-emailsecurity.net.
10 mxa.global.inbound.cf-emailsecurity.net.
10 mxb.global.inbound.cf-emailsecurity.net.
curl -s "https://cloudflare-dns.com/dns-query?name=cloudflare.com&type=MX" \
  -H "Accept: application/dns-json" \
  | jq '{Status: .Status, AD: .AD, Question: .Question, Answer: .Answer}'
{
  "Status": 0,
  "AD": true,
  "Question": [
    {
      "name": "cloudflare.com",
      "type": 15
    }
  ],
  "Answer": [
    {
      "name": "cloudflare.com",
      "type": 15,
      "TTL": 222,
      "data": "5 mxa-canary.global.inbound.cf-emailsecurity.net."
    },
    {
      "name": "cloudflare.com",
      "type": 15,
      "TTL": 222,
      "data": "5 mxb-canary.global.inbound.cf-emailsecurity.net."
    },
    {
      "name": "cloudflare.com",
      "type": 15,
      "TTL": 222,
      "data": "10 mxa.global.inbound.cf-emailsecurity.net."
    },
    {
      "name": "cloudflare.com",
      "type": 15,
      "TTL": 222,
      "data": "10 mxb.global.inbound.cf-emailsecurity.net."
    }
  ]
}

AD: true means the resolver validated the DNSSEC chain for this answer (RFC 4035 §3.2.3); you are trusting 1.1.1.1's validation rather than doing your own. type: 15 is the IANA wire type for MX. TTL is in seconds and counts down in the resolver's cache, so the number changes between queries. (The raw API response is a single line of JSON with a few more flags; the jq filter pretty-prints the fields shown.)

nslookup -type=MX cloudflare.com 1.1.1.1
Server:		1.1.1.1
Address:	1.1.1.1#53

Non-authoritative answer:
cloudflare.com	mail exchanger = 5 mxa-canary.global.inbound.cf-emailsecurity.net.
cloudflare.com	mail exchanger = 5 mxb-canary.global.inbound.cf-emailsecurity.net.
cloudflare.com	mail exchanger = 10 mxa.global.inbound.cf-emailsecurity.net.
cloudflare.com	mail exchanger = 10 mxb.global.inbound.cf-emailsecurity.net.

nslookup can't show DNSSEC AD-bit status, EDNS details, or follow MX-to-A in one shot. Use dig for that.

SPF: who's allowed to send from this domain?#

SPF (RFC 7208) is a TXT record at your domain apex. A receiving server checks whether the connecting IP is authorized to send as your domain.

example.com.   TXT   "v=spf1 include:_spf.mailprovider.com ~all"
TokenMeaning
v=spf1SPF version
include:_spf.mailprovider.comAuthorize the IPs your mail provider publishes (most hosted mail has a TXT under their own zone that you include)
~allAnyone else: soft-fail (mark, don't reject)

A recipient checks SPF by looking at the envelope MAIL FROM domain (not the visible From: header) and verifying the sending IP is in the allowed set. Bounces arrive with an empty MAIL FROM; the check then uses postmaster@<HELO hostname> instead (RFC 7208 §2.4).

Final qualifierMeaning
-allHard fail. Reject.
~allSoft fail. Mark suspicious.
?allNeutral. Make no decision.
+allPass everything. Don't do this.

Conservative opening posture is ~all because SPF alignment through hosted email is fragile: the envelope-from often resolves to a domain that doesn't visibly match your From:. DKIM picks up that slack via the alignment rules in DMARC.

One SPF record per domain, max. Multiple SPF TXT records at the same name is invalid; receiving servers will (correctly) return permerror (RFC 7208 §4.5). If you need multiple include rules, combine them: v=spf1 include:a include:b ~all.

Query a live SPF record:

dig @1.1.1.1 cloudflare.com TXT +short | grep -i spf
"v=spf1 ip4:199.15.212.0/22 ip4:173.245.48.0/20 include:_spf.google.com
include:spf1.mcsv.net include:spf.mandrillapp.com include:mail.zendesk.com
include:stspg-customer.com include:_spf.salesforce.com -all"

(the grep isolates the SPF record from the 26 TXT records cloudflare.com publishes at the time of writing; the single long string is wrapped here for readability)

nslookup -type=TXT cloudflare.com 1.1.1.1
Server:		1.1.1.1
Address:	1.1.1.1#53

Non-authoritative answer:
cloudflare.com	text = "v=spf1 ip4:199.15.212.0/22 ip4:173.245.48.0/20
include:_spf.google.com include:spf1.mcsv.net include:spf.mandrillapp.com
include:mail.zendesk.com include:stspg-customer.com include:_spf.salesforce.com -all"

nslookup cannot filter its output, so running this literally returns every TXT record for cloudflare.com (26 today); the block above is trimmed to the one that matters. Scan the output for the line beginning v=spf1: that is the SPF record. On Windows, findstr "v=spf1" pipes the result if you want to isolate it.

This SPF record illustrates two common patterns at once: explicit IP ranges for known sending infrastructure (ip4: blocks), and include: delegation to third-party services (Google Workspace, Mailchimp, Mandrill, Zendesk, Salesforce). The -all hard-fail tells receivers to reject anything not on the list. A domain that sends through many services like this often accumulates include: entries over time; watch the DNS lookup count: the include, a, mx, ptr, and exists mechanisms plus the redirect modifier are capped at 10 per evaluation, and going over is a permerror (RFC 7208 §4.6.4).

DKIM: sign every outbound message#

DKIM (RFC 6376) adds a cryptographic signature in a header (DKIM-Signature: v=1; d=example.com; s=selector1; ...). The recipient:

  1. Reads the d= (domain) and s= (selector) from the header.
  2. Queries <selector>._domainkey.<domain> for the public key.
  3. Verifies the signature against the body and signed headers.

Most hosted mail providers publish DKIM via a CNAME so they can rotate the underlying key without touching your zone:

selector1._domainkey.example.com.   CNAME   selector1.mailprovider.example.

Self-managed mail publishes the key as a TXT directly:

selector1._domainkey.example.com.   TXT   "v=DKIM1; k=rsa; p=ABC...XYZ"

DKIM TXT records often exceed the 255-byte per-string limit and need to be split into multiple quoted strings; the verifier concatenates them before use (RFC 6376 §3.6.2.2).

Query a real DKIM public key (Proton Mail's protonmail selector on proton.me):

dig @1.1.1.1 protonmail._domainkey.proton.me TXT +short
protonmail.domainkey.drfeyjwh4gwlal4e2rhajsytrp6auv2nhenecpzigu7muak6lw6ya.domains.proton.ch.
"v=DKIM1;k=rsa;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn46zCm3zHBmS
1zePKxA+RIXw41Nu6l91NpLLBnWnrcZ35H/843XNWPZEQ0OgGwx/yqTXETMLXIDjGEWlK1E1
mpdguqu+3s7SuIHoo5+i6mgyxJguljkwc3dk8ojnJ6VVUPnDh5GJArkAhXxEb1aOK1BVGM0y
DlmYdmaOfd48qcx5iODP/MFc8pivfxEXTIL+aUz"
"7+X69lMiwUSHpWYL3/a5X3nLD0zEntxv08xs8J/rpuRg4v+OXEOhcNvhkeiRZqJBdpJTkoEZ
fGvdTct+U0YYC69NW0ClUcKio2uDPmxU1xvfvHbSTW2gHYk8RpYZaxLACULdMo+Vt4Na/oIR
+swIDAQAB;"

The first output line is the CNAME delegation described above, live: protonmail._domainkey.proton.me points into Proton's own domains.proton.ch zone, so Proton can rotate the key without touching the proton.me zone file. The TXT at the end of the chain carries the key.

nslookup -type=TXT protonmail._domainkey.proton.me 1.1.1.1
Server:		1.1.1.1
Address:	1.1.1.1#53

Non-authoritative answer:
protonmail._domainkey.proton.me	canonical name =
protonmail.domainkey.drfeyjwh4gwlal4e2rhajsytrp6auv2nhenecpzigu7muak6lw6ya.domains.proton.ch.
protonmail.domainkey.drfeyjwh4gwlal4e2rhajsytrp6auv2nhenecpzigu7muak6lw6ya.domains.proton.ch
text = "v=DKIM1;k=rsa;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn46zCm3zHBmS
1zePKxA+RIXw41Nu6l91NpLLBnWnrcZ35H/843XNWPZEQ0OgGwx/yqTXETMLXIDjGEWlK1E1
mpdguqu+3s7SuIHoo5+i6mgyxJguljkwc3dk8ojnJ6VVUPnDh5GJArkAhXxEb1aOK1BVGM0y
DlmYdmaOfd48qcx5iODP/MFc8pivfxEXTIL+aUz"
"7+X69lMiwUSHpWYL3/a5X3nLD0zEntxv08xs8J/rpuRg4v+OXEOhcNvhkeiRZqJBdpJTkoEZ
fGvdTct+U0YYC69NW0ClUcKio2uDPmxU1xvfvHbSTW2gHYk8RpYZaxLACULdMo+Vt4Na/oIR
+swIDAQAB;"

The key is split into two quoted strings because the RSA public key data exceeds the 255-byte per-string wire limit (the first string here is exactly 255 bytes). The DKIM verifier concatenates them before decoding the key; the resolver hands them over as-is, which is why dig shows two separate strings.

Retired DKIM keys do not just disappear: providers publish the selector with an empty key (p=), which tells verifiers the key is revoked (RFC 6376 section 3.6.1). Gmail's old 20230601 selector returns exactly that today, so an empty p= in the wild is deliberate, not broken.

nslookup cannot verify the DKIM signature itself, show the selector, or display EDNS details. Use dig for troubleshooting.

DMARC: what do I do if SPF or DKIM fails?#

DMARC (originally RFC 7489, 2015, Informational; obsoleted by RFC 9989, published May 2026 as a Proposed Standard, with reporting split out into RFC 9990 for aggregate and RFC 9991 for failure reports) ties SPF + DKIM to the visible From: header and gives the recipient a policy.

_dmarc.example.com.   TXT   "v=DMARC1; p=none; adkim=r; aspf=r"

Receivers look the record up at _dmarc. prefixed to the From: domain; if nothing is there, they fall back to the organizational domain's record (RFC 7489 derived that from the Public Suffix List, RFC 9989 §4.10.1 replaces it with a bounded DNS tree walk).

TagMeaning
v=DMARC1DMARC version
p=nonePolicy: don't take action. Monitor only.
p=quarantineSend failing mail to spam
p=rejectHard-reject failing mail at SMTP time
sp=Policy for subdomains; if absent, subdomains inherit p=
np=Policy for non-existent subdomains (new in RFC 9989)
adkim=rDKIM alignment: relaxed (subdomain matches parent OK)
adkim=sDKIM alignment: strict (must match exactly)
aspf=r/sSame, for SPF alignment
rua=mailto:Where to send aggregate reports (XML, daily)
ruf=mailto:Where to send failure reports (detailed, per-message). Note: few large receivers send ruf= reports in practice; most stop at rua= aggregates.
pct=NApply policy to N% of failing mail (staged rollout). RFC 7489 only: RFC 9989 removed pct, though records in the wild still carry it

A typical opening posture: p=none, announce DMARC is in play, ask the receiver to check, but don't ask for action. Once you confirm DKIM signatures align consistently (a few days of rua= reports proves it), promote to p=quarantine. Skipping straight to p=reject is a common way to discover a forgotten legitimate sender by losing its mail in production.

Query a live DMARC record at a p=reject domain:

dig @1.1.1.1 _dmarc.cloudflare.com TXT +short
"v=DMARC1; p=reject; sp=reject; adkim=r; aspf=r; pct=100;
rua=mailto:<token>@dmarc-reports.cloudflare.net,
mailto:rua@cloudflare.com"
nslookup -type=TXT _dmarc.cloudflare.com 1.1.1.1
Server:		1.1.1.1
Address:	1.1.1.1#53

Non-authoritative answer:
_dmarc.cloudflare.com	text = "v=DMARC1; p=reject; sp=reject; adkim=r;
aspf=r; pct=100;
rua=mailto:<token>@dmarc-reports.cloudflare.net,
mailto:rua@cloudflare.com"

This is what a fully-enforced DMARC policy looks like: p=reject with pct=100 means every unauthenticated message that claims From: @cloudflare.com is rejected at SMTP time, and sp=reject applies the same policy to subdomains. The adkim=r; aspf=r pair spells out relaxed alignment, which is also the default when the tags are absent.

Forge resistance: cheapest to strongest#

StepWhat it costsWhat it stops
Just MXnothingNothing, anyone can claim to send "from" you
MX + SPF ~allone TXT recordA casual spammer with a different sending IP than yours
MX + SPF + DKIMDKIM setup, key rotationTampering in transit: any change to the signed headers or body breaks the signature
MX + SPF + DKIM + DMARC p=noneone TXT recordNothing yet, but you start getting reports if you publish rua=
Same + DMARC p=quarantinenothingMost forgeries, they land in spam at major receivers
Same + DMARC p=rejectnothingExact-domain forgeries: receivers that honor the policy reject at SMTP time

The jump from p=none to p=quarantine is usually where domains stall, because legitimate mail-forwarding, mailing lists, and old infrastructure often break alignment. Watch reports first. And note the scope: DMARC protects your exact domain and its subdomains; lookalike domains (examp1e.com) are outside its reach entirely.

BIMI: the optional cosmetic on top#

Once you're at p=quarantine or p=reject, you can publish a BIMI record that lets supporting clients (Gmail, Apple Mail, others) show your logo next to messages. It takes an SVG logo (SVG Tiny PS profile) plus, in practice, a certificate proving you own the mark: Gmail displays logos only with a VMC or CMC and only at full enforcement, and Apple Mail likewise requires certificate-backed evidence such as a VMC. The BIMI spec calls the certificate optional; the two clients that matter do not.

Reading the headers in a received message#

When debugging:

  1. Open a message in your mail client and use "show raw headers" or "show original."
  2. Find Authentication-Results:: it lists spf=pass/fail, dkim=pass/fail, dmarc=pass/fail.
  3. If DMARC says policy=none and result is fail, the message would have been quarantined under stricter policy.
Authentication-Results: mx.recipient.example;
  spf=pass smtp.mailfrom=example.com;
  dkim=pass header.d=example.com header.s=selector1;
  dmarc=pass action=none header.from=example.com

nslookup can show you the SPF, DKIM, and DMARC TXT records, but it cannot parse or validate them. The Authentication-Results header is written by the receiving MTA after it does that work.


See also: MX record reference · TXT record reference · PTR: why reverse DNS matters for outbound mail · CAA: locking down cert issuance for the mail server · CNAME: why DKIM often uses one · nslookup and dig: the two tools you will actually use