{"id":405,"date":"2026-10-22T06:24:00","date_gmt":"2026-10-22T13:24:00","guid":{"rendered":"https:\/\/blog.canutethegreat.com\/?p=405"},"modified":"2026-10-22T06:24:00","modified_gmt":"2026-10-22T13:24:00","slug":"spf-dkim-dmarc-why-p-none-isnt-enough","status":"publish","type":"post","link":"https:\/\/blog.canutethegreat.com\/index.php\/2026\/10\/22\/spf-dkim-dmarc-why-p-none-isnt-enough\/","title":{"rendered":"SPF, DKIM and DMARC, and why p=none isn&#8217;t enough"},"content":{"rendered":"<p>Email was designed in an era when every host on the network trusted every other host. SMTP lets any server claim to send mail from any address, and the <code>From:<\/code> header that a person actually reads is just text the sender typed. Three standards were bolted on afterwards to fix that: SPF, DKIM and DMARC. Most domains I look at have all three records published. A surprising number of them are still not protected, because the DMARC policy says <code>p=none<\/code>.<\/p>\n<h2>SPF: which servers may send<\/h2>\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/blog.canutethegreat.com\/wp-content\/uploads\/2026\/10\/dmarc-evaluation-flow.png\" alt=\"Flowchart of how a receiving mail server evaluates SPF, DKIM, alignment and the DMARC policy\" \/><\/figure>\n<p>Sender Policy Framework (RFC 7208) lets a domain publish, in DNS, the list of servers allowed to send mail for it. A receiving server takes the domain from the SMTP envelope sender (the <code>MAIL FROM<\/code>, also called the return path), looks up its SPF record, and checks whether the connecting IP is on the list.<\/p>\n<pre class=\"wp-block-code\"><code>example.com.  IN TXT  \"v=spf1 ip4:198.51.100.0\/24 include:_spf.google.com include:sendgrid.net -all\"<\/code><\/pre>\n<p>SPF has two well-known weaknesses. It checks the envelope sender, not the visible <code>From:<\/code> header, so on its own it does nothing to stop someone spoofing your domain in the address the recipient sees. And it breaks on forwarding, because a forwarding server isn&#8217;t in your list.<\/p>\n<p>There&#8217;s also a hard limit that bites growing companies: evaluating an SPF record may trigger at most 10 DNS lookups (<code>include<\/code>, <code>a<\/code>, <code>mx<\/code>, <code>ptr<\/code>, <code>exists<\/code> and <code>redirect<\/code> each count). Every SaaS tool you add with an <code>include:<\/code> eats into that budget, and when you exceed it the result is a <code>permerror<\/code>, which many receivers treat as a failure. I&#8217;ve seen marketing teams take down a company&#8217;s outbound mail by adding one more vendor.<\/p>\n<h2>DKIM: a signature on the message<\/h2>\n<p>DomainKeys Identified Mail (RFC 6376) has the sending server sign selected headers and the body with a private key. The signature goes in a <code>DKIM-Signature<\/code> header naming the signing domain (<code>d=<\/code>) and a selector (<code>s=<\/code>). The receiver fetches the public key from DNS at <code>selector._domainkey.domain<\/code> and verifies.<\/p>\n<pre class=\"wp-block-code\"><code>s1._domainkey.example.com.  IN TXT  \"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\"<\/code><\/pre>\n<p>DKIM survives plain forwarding because the signature travels with the message, though it breaks if a mailing list rewrites the subject or appends a footer. Use 2048-bit RSA keys, and rotate them. In 2012 a mathematician found that Google was signing with a 512-bit key that he could factor, which led to a quick round of key upgrades across large providers.<\/p>\n<p>The catch, again, is that DKIM proves a message was signed by <code>d=<\/code> some domain. It doesn&#8217;t, by itself, say that domain has anything to do with the <code>From:<\/code> address.<\/p>\n<h2>DMARC: tying it to the From header<\/h2>\n<p>DMARC (RFC 7489) is what makes the first two useful against spoofing. It adds the concept of alignment: a message passes DMARC only if SPF passes for a domain that matches the <code>From:<\/code> domain, or DKIM passes with a <code>d=<\/code> that matches the <code>From:<\/code> domain. Relaxed alignment (the default) accepts the same organisational domain, so <code>mail.example.com<\/code> aligns with <code>example.com<\/code>. Strict alignment requires an exact match.<\/p>\n<p>The DMARC record then tells receivers what to do with mail that fails, and where to send reports:<\/p>\n<pre class=\"wp-block-code\"><code>_dmarc.example.com.  IN TXT  \"v=DMARC1; p=reject; rua=mailto:dmarc-reports@example.com; adkim=r; aspf=r\"<\/code><\/pre>\n<p>The <code>p=<\/code> tag is the policy: <code>none<\/code>, <code>quarantine<\/code> or <code>reject<\/code>.<\/p>\n<h2>Why p=none isn&#8217;t protection<\/h2>\n<p>A <code>p=none<\/code> policy tells receivers: evaluate DMARC, send me reports, but deliver failing mail exactly as you would have anyway. It&#8217;s a monitoring mode. Anyone can still send mail with your domain in the <code>From:<\/code> header to your customers, your suppliers or your own finance team, and DMARC will do nothing to stop it.<\/p>\n<p>Monitoring is the right first step, and that&#8217;s why <code>p=none<\/code> exists. The problem is that a lot of organisations treat publishing it as finishing the job. Google and Yahoo&#8217;s bulk sender requirements, which took effect in February 2024, made a DMARC record mandatory for anyone sending more than 5,000 messages a day to their users, but the minimum they ask for is <code>p=none<\/code>. That pushed a lot of domains into publishing a record, and many of them stopped there.<\/p>\n<p>Business email compromise is where this hurts. A convincing invoice from <code>accounts@yourcompany.com<\/code> that passes every visual check is a lot easier to pull off when your domain doesn&#8217;t ask receivers to reject spoofs. With <code>p=reject<\/code>, major mailbox providers will drop exact-domain spoofs outright. Attackers then have to fall back on lookalike domains, which is still a problem, but a much more detectable one.<\/p>\n<h2>Getting from none to reject<\/h2>\n<p>The path is boring and it works.<\/p>\n<p>First, publish <code>p=none<\/code> with an <code>rua<\/code> address and actually read the aggregate reports. They&#8217;re XML, so use a parser or a reporting service rather than reading them by hand. Within a couple of weeks you&#8217;ll see every source sending mail as your domain: your mail platform, the CRM, the ticketing system, the HR tool somebody bought on a credit card, and the spoofers.<\/p>\n<p>Second, fix each legitimate source. Ideally each one signs with DKIM using your domain as <code>d=<\/code>, which most SaaS senders support through a CNAME to their key. Where you can only get SPF alignment, make sure the vendor uses a return path on your domain. Retire sources nobody can explain.<\/p>\n<p>Third, move to <code>p=quarantine<\/code>, then to <code>p=reject<\/code>. RFC 7489 provides the <code>pct=<\/code> tag to apply the policy to a percentage of failing mail, which lets you ramp up gradually. Be aware that the DMARCbis revision in the IETF drops <code>pct<\/code> in favour of a simpler testing flag, so don&#8217;t build long-term tooling around it.<\/p>\n<p>Don&#8217;t forget domains that send no mail at all. Parked and unused domains are attractive for spoofing precisely because nobody watches them. Lock them down completely:<\/p>\n<pre class=\"wp-block-code\"><code>parked-example.com.          IN TXT  \"v=spf1 -all\"\n_dmarc.parked-example.com.   IN TXT  \"v=DMARC1; p=reject;\"<\/code><\/pre>\n<p>Also check the <code>sp=<\/code> tag, which sets the policy for subdomains. If you leave it out, subdomains inherit <code>p=<\/code>, which is usually what you want.<\/p>\n<h2>What DMARC doesn&#8217;t do<\/h2>\n<p>DMARC protects the exact domain. It does nothing about <code>examp1e.com<\/code> or <code>example-billing.com<\/code>, nor display-name tricks where the friendly name says &#8220;Example Payroll&#8221; over an unrelated address. Those need receiver-side defences, user training and monitoring for newly registered lookalikes. MTA-STS (RFC 8461) and TLS reporting are separate pieces that protect mail in transit, and BIMI only becomes available once you&#8217;re at quarantine or reject.<\/p>\n<p>If you run a domain and its DMARC record still says <code>p=none<\/code> after a few months, you&#8217;ve built the monitoring and switched off the alarm. Read the reports, fix your senders, and finish the rollout.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Email was designed in an era when every host on the network trusted every other host. SMTP lets any server claim to send mail from any address, and the From: header that a person actually reads is just text the sender typed. Three standards were bolted on afterwards to fix that: SPF, DKIM and DMARC. [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":411,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[21],"tags":[250,251,252,12,61,253],"class_list":["post-405","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-security","tag-dkim","tag-dmarc","tag-dns","tag-email","tag-phishing","tag-spf"],"_links":{"self":[{"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/posts\/405","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/comments?post=405"}],"version-history":[{"count":1,"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/posts\/405\/revisions"}],"predecessor-version":[{"id":421,"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/posts\/405\/revisions\/421"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/media\/411"}],"wp:attachment":[{"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/media?parent=405"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/categories?post=405"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/tags?post=405"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}