SPF, DMARC, and DKIM: the complete email authentication guide
Your domain passes every security scan. Your SSL cert is valid, your headers are tight, your CSP is locked down. Then a phishing email lands in a customer's inbox, from your domain, with your company name in the "From" field. The email is fake, but the damage is real.
This happens because email was designed in 1982 with no built-in sender verification. Anyone can set any "From" address on an email, and without SPF, DMARC, and DKIM records on your domain, receiving servers have no way to catch the forgery. Over 90% of phishing attacks use domain spoofing, and misconfigured email authentication is the open door.
This guide covers what each record does, how they work together, and how to audit all three on any domain with a single script using the botoi DNS security API.
How email authentication works
Three DNS records work as layers of defense. Each solves a different problem:
- SPF answers: "Is this server allowed to send email for this domain?"
- DKIM answers: "Was this message altered after it left the sender's server?"
- DMARC answers: "What should I do if SPF or DKIM fails, and where do I send reports?"
When someone sends an email claiming to be from you@example.com, the receiving
server checks these records in sequence. If SPF and DKIM both fail, and DMARC says
p=reject, the message gets dropped before it reaches the inbox. Without all
three, gaps exist that attackers exploit.
SPF: who can send on your behalf
SPF (Sender Policy Framework) is a DNS TXT record at your domain's root. It lists every
IP address and mail server authorized to send email for your domain. When a receiving server
gets an email from example.com, it checks the SPF record to see if the sending
server is on the approved list.
Check any domain's SPF record with a single API call:
curl -s -X POST https://api.botoi.com/v1/dns-security/spf-check \
-H "Content-Type: application/json" \
-d '{"domain": "example.com"}' Response:
{
"success": true,
"data": {
"domain": "example.com",
"has_spf": true,
"record": "v=spf1 include:_spf.google.com ~all",
"mechanisms": ["include:_spf.google.com", "~all"],
"all_policy": "~all",
"includes": ["_spf.google.com"],
"valid": true
}
} The key fields to check:
-
has_spf: Does a TXT record starting withv=spf1exist? If false, any server can forge email from your domain. -
valid: Does the record parse without errors? SPF records break silently when they exceed the 10 DNS lookup limit. -
all_policy: The trailing mechanism that defines what happens to unlisted senders.-all(hard fail) rejects them.~all(soft fail) marks them suspicious.+allallows everyone, which defeats the entire purpose.
The 10-lookup limit
SPF records allow a maximum of 10 DNS lookups. Each include:, a:,
mx:, and redirect: mechanism counts as one lookup. Nested includes
count too. Once you add Google Workspace, your marketing tool, your transactional email service,
and a CRM, you hit this limit fast.
When you exceed 10 lookups, the entire SPF record becomes invalid. The API's valid
field catches this. Fix it by flattening nested includes into IP ranges or consolidating
providers.
DMARC: what to do when checks fail
DMARC (Domain-based Message Authentication, Reporting, and Conformance) ties SPF and DKIM
together. It lives at _dmarc.example.com as a TXT record and tells receiving
servers two things: what to do with messages that fail authentication, and where to send
reports about those failures.
Check your DMARC record:
curl -s -X POST https://api.botoi.com/v1/dns-security/dmarc-check \
-H "Content-Type: application/json" \
-d '{"domain": "example.com"}' Response:
{
"success": true,
"data": {
"domain": "example.com",
"has_dmarc": true,
"record": "v=DMARC1; p=reject; rua=mailto:dmarc@example.com; pct=100",
"policy": "reject",
"subdomain_policy": null,
"reporting": {
"rua": ["mailto:dmarc@example.com"],
"ruf": []
},
"pct": 100,
"alignment": {
"dkim": "r",
"spf": "r"
}
}
} DMARC policies
The policy field determines what happens to failing messages:
-
none: Take no action. Monitor only. You get reports but spoofed emails still reach inboxes. -
quarantine: Move failing messages to spam. The recipient can still find them if they look. -
reject: Drop failing messages entirely. The strongest protection, but misconfiguration means lost legitimate email.
Safe DMARC rollout strategy
Going straight to p=reject is risky. Follow this path:
- Start with
p=none; rua=mailto:dmarc@example.comto collect reports for 2-4 weeks - Review reports. Identify any legitimate senders failing authentication. Fix their SPF includes and DKIM keys.
- Move to
p=quarantine; pct=10to quarantine 10% of failing messages - Increase
pctto 25, 50, then 100 over the next few weeks as you confirm no legitimate mail is affected - Switch to
p=reject; pct=100once you have confidence in your setup
The pct field in the API response shows you where you are in this rollout. A domain
at pct=100 with policy=reject has full protection.
DKIM: cryptographic signatures on every email
DKIM (DomainKeys Identified Mail) adds a cryptographic signature to the headers of every outgoing message. The sending server signs the message with a private key; the receiving server verifies it against a public key published in your DNS. If the message was altered in transit (headers changed, body modified, forwarded through a mailing list that rewrites content), the signature fails.
DKIM records live at [selector]._domainkey.example.com. The selector is a label
your email provider assigns, like google for Google Workspace or selector1
for Microsoft 365.
Check a DKIM record:
curl -s -X POST https://api.botoi.com/v1/dns-security/dkim-check \
-H "Content-Type: application/json" \
-d '{"domain": "example.com", "selector": "google"}' Response:
{
"success": true,
"data": {
"domain": "example.com",
"selector": "google",
"has_dkim": true,
"record": "v=DKIM1; k=rsa; p=MIIBIjANBgkq...",
"key_type": "rsa",
"public_key_length": 2048
}
} Key fields:
-
has_dkim: Is a public key published for this selector? If false, DKIM verification fails for all messages signed with this selector. -
public_key_length: NIST recommends a minimum of 2048 bits. Keys under 1024 bits are weak enough to factor. -
key_type: RSA is the standard. Ed25519 is faster and uses shorter keys but has limited mail provider support.
DKIM and email forwarding
This is why DKIM matters even when SPF is configured. When someone forwards your email, the forwarding server's IP is not in your SPF record, so SPF fails. But the DKIM signature survives forwarding (as long as the content is not modified). DMARC passes if either SPF or DKIM aligns, so DKIM is your safety net for forwarded messages.
How the three work together
| Threat | SPF | DKIM | DMARC |
|---|---|---|---|
| Spoofed email from unauthorized server | Detects | Detects (no valid signature) | Enforces policy |
| Message body tampered in transit | Does not detect | Detects | Enforces policy |
| Forwarded email from legitimate sender | Fails (different server IP) | Passes (signature intact) | Passes if DKIM aligns |
| Subdomain spoofing (user@fake.example.com) | No protection | No protection | Blocks via sp=reject |
| Visibility into authentication failures | None | None | Sends aggregate reports |
No single record provides full coverage. SPF without DMARC means failures are not enforced. DKIM without SPF means anyone with a valid key can send from your domain. DMARC without both SPF and DKIM has nothing to enforce against.
Audit your domain in 30 seconds
This shell script checks all three records and prints a pass/fail summary. It uses the botoi DNS security API; no API key needed for up to 5 requests per minute.
#!/bin/bash
# Audit SPF, DMARC, and DKIM for a domain in 30 seconds
DOMAIN=${1:-"example.com"}
DKIM_SELECTOR=${2:-"google"}
API="https://api.botoi.com/v1/dns-security"
PASS=0
FAIL=0
echo "Auditing email authentication for $DOMAIN"
echo "==========================================="
# SPF check
SPF=$(curl -s -X POST "$API/spf-check" \
-H "Content-Type: application/json" \
-d "{\"domain\": \"$DOMAIN\"}")
HAS_SPF=$(echo "$SPF" | jq -r '.data.has_spf')
SPF_VALID=$(echo "$SPF" | jq -r '.data.valid')
SPF_RECORD=$(echo "$SPF" | jq -r '.data.record // "not found"')
ALL_POLICY=$(echo "$SPF" | jq -r '.data.all_policy // "none"')
if [ "$HAS_SPF" = "true" ] && [ "$SPF_VALID" = "true" ]; then
echo "[PASS] SPF: $SPF_RECORD"
PASS=$((PASS + 1))
else
echo "[FAIL] SPF: $SPF_RECORD"
FAIL=$((FAIL + 1))
fi
if [ "$ALL_POLICY" = "+all" ]; then
echo " WARNING: +all allows any server to send as your domain"
fi
# DMARC check
DMARC=$(curl -s -X POST "$API/dmarc-check" \
-H "Content-Type: application/json" \
-d "{\"domain\": \"$DOMAIN\"}")
HAS_DMARC=$(echo "$DMARC" | jq -r '.data.has_dmarc')
POLICY=$(echo "$DMARC" | jq -r '.data.policy // "not set"')
PCT=$(echo "$DMARC" | jq -r '.data.pct // 0')
if [ "$HAS_DMARC" = "true" ]; then
echo "[PASS] DMARC: policy=$POLICY pct=$PCT%"
PASS=$((PASS + 1))
else
echo "[FAIL] DMARC: no record found"
FAIL=$((FAIL + 1))
fi
if [ "$POLICY" = "none" ]; then
echo " WARNING: policy=none only monitors, does not enforce"
fi
# DKIM check
DKIM=$(curl -s -X POST "$API/dkim-check" \
-H "Content-Type: application/json" \
-d "{\"domain\": \"$DOMAIN\", \"selector\": \"$DKIM_SELECTOR\"}")
HAS_DKIM=$(echo "$DKIM" | jq -r '.data.has_dkim')
KEY_TYPE=$(echo "$DKIM" | jq -r '.data.key_type // "unknown"')
KEY_LEN=$(echo "$DKIM" | jq -r '.data.public_key_length // 0')
if [ "$HAS_DKIM" = "true" ]; then
echo "[PASS] DKIM: selector=$DKIM_SELECTOR key_type=$KEY_TYPE key_length=$KEY_LEN"
PASS=$((PASS + 1))
else
echo "[FAIL] DKIM: no record for selector '$DKIM_SELECTOR'"
FAIL=$((FAIL + 1))
fi
if [ "$HAS_DKIM" = "true" ] && [ "$KEY_LEN" -lt 2048 ] 2>/dev/null; then
echo " WARNING: DKIM key is $KEY_LEN bits (2048 recommended)"
fi
# Summary
echo ""
echo "Results: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ] && echo "All checks passed." || exit 1 Run it:
bash audit.sh example.com google The first argument is your domain; the second is your DKIM selector. The script makes 3 API calls (within the free tier limit), checks each record, flags warnings for weak configurations, and exits with a non-zero code if any check fails. Drop it into a cron job or CI pipeline for ongoing monitoring.
If you prefer JavaScript, here is the same audit as an async function:
async function auditEmailAuth(domain, dkimSelector = "google") {
const api = "https://api.botoi.com/v1/dns-security";
const headers = { "Content-Type": "application/json" };
const [spf, dmarc, dkim] = await Promise.all([
fetch(`${api}/spf-check`, {
method: "POST",
headers,
body: JSON.stringify({ domain }),
}).then((r) => r.json()),
fetch(`${api}/dmarc-check`, {
method: "POST",
headers,
body: JSON.stringify({ domain }),
}).then((r) => r.json()),
fetch(`${api}/dkim-check`, {
method: "POST",
headers,
body: JSON.stringify({ domain, selector: dkimSelector }),
}).then((r) => r.json()),
]);
return {
domain,
spf: {
exists: spf.data.has_spf,
valid: spf.data.valid,
record: spf.data.record,
allPolicy: spf.data.all_policy,
},
dmarc: {
exists: dmarc.data.has_dmarc,
policy: dmarc.data.policy,
pct: dmarc.data.pct,
reporting: dmarc.data.reporting?.rua || [],
},
dkim: {
exists: dkim.data.has_dkim,
selector: dkimSelector,
keyType: dkim.data.key_type,
keyLength: dkim.data.public_key_length,
},
};
}
// Usage
const report = await auditEmailAuth("example.com", "google");
console.log(JSON.stringify(report, null, 2)); Common provider configurations
Each email provider has its own SPF include domain and DKIM selector. Here are the values for the most common providers:
| Provider | SPF include | DKIM selector(s) |
|---|---|---|
| Google Workspace | include:_spf.google.com | google |
| Microsoft 365 | include:spf.protection.outlook.com | selector1, selector2 |
| Amazon SES | include:amazonses.com | UUID-based (check SES console) |
| SendGrid | include:sendgrid.net | s1, s2 |
| Postmark | include:spf.mtasv.net | Per-domain (check Postmark DNS settings) |
| Mailchimp / Mandrill | include:servers.mcsv.net | k1 |
If you use multiple providers, your SPF record includes all of them in a single TXT entry.
For example: v=spf1 include:_spf.google.com include:sendgrid.net ~all. Watch
the 10-lookup limit as you add providers.
Key points
- SPF controls which servers can send email for your domain. Check it with
the
/v1/dns-security/spf-checkendpoint. Watch for the 10-lookup limit and avoid+all. - DMARC defines what happens when SPF or DKIM fails. Roll out gradually from
p=nonetop=rejectusing thepctfield. Always set aruaaddress to receive reports. - DKIM proves message integrity with cryptographic signatures. Use 2048-bit
keys minimum and verify your selector is published with
/v1/dns-security/dkim-check. - All three records work together. SPF alone does not stop forwarded email spoofing. DKIM alone does not tell receivers what to do on failure. DMARC without SPF and DKIM has nothing to enforce.
- Automate your checks. Run the audit script on a schedule or in CI to catch DNS drift, provider migrations, and accidental record deletions before they affect deliverability.
Frequently asked questions
- What is an SPF record and why do I need one?
- An SPF (Sender Policy Framework) record is a DNS TXT entry that lists every server authorized to send email on behalf of your domain. Without one, any server on the internet can send email that appears to come from your domain, and receiving mail servers have no way to tell the difference. Most domains need at least one SPF record that includes their email provider.
- How do I check if my domain has a DMARC record?
- Query the DNS TXT record at _dmarc.yourdomain.com. You can do this with dig, nslookup, or by sending a POST request to the botoi DMARC check API with your domain name. The API returns the full parsed record including policy, percentage, and reporting addresses.
- Do I need DKIM if I already have SPF?
- Yes. SPF and DKIM solve different problems. SPF verifies the sending server is authorized; DKIM verifies the message content was not altered in transit. Email forwarding breaks SPF alignment but preserves DKIM signatures. DMARC requires at least one of them to pass, so having both gives you resilience when forwarding or relaying occurs.
- What happens if I set my DMARC policy to reject immediately?
- Receiving servers will drop every message that fails both SPF and DKIM alignment. If you have misconfigured records, forgotten third-party senders, or forwarding rules you did not account for, legitimate email will be silently discarded. Start with p=none to collect reports, move to p=quarantine at 10%, and only set p=reject after you confirm all legitimate mail passes authentication.
- How often should I audit my email authentication records?
- At minimum, check after every DNS change, email provider migration, or when adding a new sending service (marketing tools, transactional email, CRM). A weekly automated check catches silent breakage like exceeding the SPF 10-lookup limit or an expired DKIM key. The botoi DNS security API endpoints make this easy to automate in CI or a cron job.
Try this API
Email Security Report API — interactive playground and code examples
More guide posts
Start building with botoi
150+ API endpoints for lookup, text processing, image generation, and developer utilities. Free tier, no credit card.