DevKitLab Logo DevKitLab
邮件 / SPF / DKIM / DMARC / DNS

SPF、DKIM、DMARC 到底各管什么

三条记录,三件不同的事。SPF 和 DKIM 各自验的是什么,为什么对齐才是 DMARC 真正的机关,以及一封信怎么会 SPF 通过却 DMARC 失败。

有人转给你一张截图:一封催款邮件,发件人写着你公司的名字,付款链接却不是你们的。你翻发信服务器日志,什么都没发出去过。这封信根本没碰过你的基础设施——对方只是在发件人栏里敲了你的域名,就跟在信封上随手写个回信地址一样。

SPF、DKIM、DMARC 要堵的就是这个洞。它们总被并排念出来,像是同一件事的三个版本,而这个印象恰恰是混乱的源头。三者既不重复也不能互相替代:每一个回答的问题都不一样,而且只有第三个管的才是收件人真正看到的那个地址。

先说没人告诉你的那两个「发件人」

记录本身不难懂,难的是它们底下那层收发机制——几乎所有让人费解的 DMARC 结果都能追到这里:一封邮件有两个发件人地址,而且它们不必一致。

  • 信封发件人,也叫 MAIL FROM,投递之后落成 Return-Path 头。这是 SMTP 层的东西:发信服务器在握手时报出的地址,退信往这儿回。收件人根本看不到它。
  • From 头,邮件客户端里显示的那个。这是人读到、并且据此产生信任的地址。

两者是各自独立的字段。你通过营销平台正常群发的一封信,信封发件人通常在 bounces.mailer-provider.com,From 头写的却是 yourcompany.com——完全正常,不是伪造。但这个缝隙也正是攻击者的全部机会所在:在自己控制的域名上通过认证,显示一个不属于自己的域名。

把这个区别记住。SPF 单独拦不住伪造是因为它,DMARC 不得不发明「对齐」也是因为它。

SPF:哪些服务器可以替这个域名发信

SPF(发件人策略框架,RFC 7208)在 DNS 里公布一份允许替某域名发信的 IP 清单,形式是一条 TXT 记录:

v=spf1 include:_spf.google.com include:sendgrid.net ip4:198.51.100.25 -all

从左往右读:来自 Google Workspace 地址段的信通过,来自 SendGrid 地址段的通过,来自那台指定服务器的通过,最后的 -all 说其余一律失败。收信方拿连接过来的 IP 逐项比对,给出一个结果。

结尾那个机制的分量比它的长度大得多:

结尾名称含义
-allfail未授权。严格设置,也是本来该用的那个
~allsoftfail未授权,但仍接收并做标记。过渡期设置
?allneutral不表态,跟没发布记录差别不大
+allpass谁都算授权。不要发布这个

实际用起来有三条限制要留神。第一,SPF 有十次 DNS 查询的预算上限(RFC 7208 §4.6.4)。includeamxptrexistsredirect 各消耗一次,而 include 是递归消耗的——四五家 SaaS 服务商就能悄悄把你顶出上限。超过十次,求值直接返回 PermError,在 DMARC 眼里等同 SPF 失败。记录本身看上去一点毛病没有,只是对某些发信路径不再工作了。

第二条更根本:SPF 验的是信封发件人,不是 From 头。攻击者注册一个一次性域名,给它配一条完全合法的 SPF 记录,从自己服务器发出去,SPF 照样通过——而 From 头写的是你公司。SPF 没做错任何事,它的职责本来就不包括保护读者看到的那个地址。

第三条是个实践中的别扭事:SPF 遇到转发就会断。收件人设了自动转发,转发服务器用的是它自己的 IP,这个 IP 不在你的记录里,SPF 于是失败——不怪你。

想看自己的预算被展开成什么样,包括 include 套 include 消耗掉的那些,把域名丢进 SPF/DMARC 检测工具,它会把整棵树走一遍,把每一个消耗查询的项都数出来。这个总数要当上限读,不是判决:求值在第一个命中的机制处就停了,所以超过十次意味着「路径走到第十次之后的那些发信方会挂」,而不是「你发的每一封都挂」。

DKIM:一个跟着信走的签名

DKIM(域名密钥识别邮件,RFC 6376)把密码学签名附在邮件本身上。发信服务器用私钥签一组头部加正文,加上一个 DKIM-Signature: 头;公钥放在 DNS 里,收信方取回来验证。

签名头自己就写明了去哪儿找钥匙:

DKIM-Signature: v=1; a=rsa-sha256; d=example.com; s=selector1;
  h=from:to:subject:date; bh=...; b=...

d= 是签名域名,s=selector。两者拼出 DNS 名 selector1._domainkey.example.com,公钥就在那儿。把钥匙位置拆出一个 selector,好处是同一个域名可以同时挂多把钥匙——每家服务商一把,或者轮换期间新旧各一把。

因为证据是装在信里走的,DKIM 扛得住中转:一封签好的信转发出去,只要内容没动,签名照样验得过。反过来说就是——DKIM 怕改动。邮件列表往正文尾巴上贴一段声明、或者改写主题,就把经手的那个签名弄失效了。

读签名时有两处细节值得知道。h= 列的是到底签了哪些头,一个没覆盖 From 的签名基本等于没有——攻击者可以在不惊动它的前提下把可见发件人换掉。还有 l=,它给正文设一个长度上限,只签前 N 个字节,超出部分追加的内容既没签也没人察觉。

审一个域名时还有一件事必须先知道,否则容易得出反的结论:DNS 没有任何办法列出一个域名发布了哪些 selector。你可以查一个已知的 selector,但你没法把它们枚举出来。任何能报告 DKIM 状况的工具,要么在探测常见名字,要么在问你要——所以「没找到密钥」的意思永远是「我试的这些名字都没命中」,而不是「这个域名没配 DKIM」。

DMARC:把前两者拴到可见的发件人上

到这儿前两者才真正变得有用。SPF 验的是信封,DKIM 验的是一个签名域名,这两个都不要求跟 From 头有任何关系——而 From 头是收件人唯一会读的那部分。

DMARC(RFC 9989)用一条叫「对齐」的规则把这个缺口补上:SPF 或 DKIM 的通过只有在它验的那个域名与 From 头里的域名相符时才算数。

一封信通过 DMARC 的条件是下面任意一条成立:

  • SPF 通过,并且信封发件人域名与 From 域名对齐;或者
  • DKIM 通过,并且签名的 d= 域名与 From 域名对齐。

有一条就够——这也正是 SPF 因转发而断掉之后,光靠 DKIM 还能让信继续通过的原因。

现在回头看开头那次攻击,它散架了。攻击者那个一次性域名 SPF 仍然通过,但那个域名跟 From 头不对齐,所以对 DMARC 毫无贡献。一条对齐的通过都没有,DMARC 判失败,接下来由你公布的策略决定收信方怎么处置。

对齐分两档,用 aspfadkim 分别设置:

  • 宽松r,默认):组织域相符即可,mail.example.comexample.com 算对齐。
  • 严格s):必须完全一致,mail.example.comexample.com 算对齐。

绝大多数情况宽松是对的默认值。严格值得有意识地选:它排掉了一整类基于子域的滥用,但凡是「被认证的域名是 From 头域名的子域」这种形态的信件都会挂——而多数 SaaS 发信恰好就长这个样子。

组织域怎么算,这一版改了。 RFC 9989 改用 DNS tree walk——逐级查询更短的名字看有没有策略记录——来确定组织域,而不再像 RFC 7489 那样查 Public Suffix List。对一个普通的 company.com 两种算法结果一样,所以多数域名什么都感觉不到。它影响的是边缘情况:多段后缀,以及在命名空间的好几层上都发布了策略的组织。

策略本身是 _dmarc.yourdomain.com 上的一条 TXT 记录:

v=DMARC1; p=reject; sp=reject; np=reject; adkim=r; aspf=r;
  rua=mailto:dmarc-reports@yourdomain.com
标签作用
p未对齐的信怎么处理:nonequarantinereject
sp已存在子域的策略,不写则回落到 p
npDNS 里不存在的子域的策略,不写则回落到 sp,再回落到 p
adkim / aspf对齐档位,rs,默认 r
t测试模式。t=y 请求收信方不要直接套用已公布的策略;预期会低一档
fo哪些失败要生成报告:01ds
rua聚合报告发到哪儿
ruf单封失败报告发到哪儿

np 是最值得专门加上的一个。攻击者很爱编造你从没建过的子域——billing.yourcompany.com 在 DNS 里没有任何记录,于是你真实的邮件配置管不到它。np=reject 一句话堵死这条路,同时完全不影响你在用的那些子域。

t 则是最容易被读错的一个。t=y 不是「忽略我的策略」,它请求收信方不要直接套用已公布的策略。RFC 9989 预期会低一档:reject 变成 quarantinequarantine 变成 none;但最终怎样处置仍由收信方决定。这是一个上线用的刹车,因此带着 t=yp=reject 在标签被移除前,实际请求的是隔离。

如果你是 2026 年之前学的 DMARC,回头对一下现在的规范。 DMARC 在 2026 年 5 月完成修订,发布为 RFC 9989(协议本体)、RFC 9990(聚合报告)、RFC 9991(失败报告)三份文档,一并废止 RFC 7489,并首次把 DMARC 提升为标准轨协议。这次修订移除了 pctrfri 三个标签——pct 原本承担的事情交给了 t——并把 p 从必需降为推荐。p 缺失,或 pspnp 有无效值时,只有记录中带有效 rua 才会按 p=none 继续;否则收信方不做 DMARC 处理。已有的 v=DMARC1 记录不受影响:收信方是忽略这些被移除的标签,而不是拒绝整条记录。下次改 DNS 时顺手清掉即可。

串起来看:一封信是怎么被判的

对一封进来的信,收信方大致跑这么一遍:

  1. 读 From 头的域名,后面所有判断都以它为基准。
  2. 对信封发件人的域名跑 SPF,用连接过来的 IP 比对。记下通过与否,以及验的是哪个域名
  3. 验所有 DKIM 签名,记下通过与否和每个签名的 d=
  4. 判对齐:SPF 验的那个域名跟 From 域名相符吗?某个通过的 d= 跟它相符吗?
  5. 有任意一条对齐的通过 → DMARC 通过。域名使用已通过认证,但正常投递和过滤规则仍照收信方的规则走。
  6. 一条都没有 → DMARC 失败。查发信方请求的 p(或 sp/np)策略;若 t=y,预期会低一档,但最终处置仍由收信方决定。

意外都出在第 4 步,它也解释了那个让人以为撞见 bug 的结果:spf=passdmarc=fail 同时出现并不矛盾。这恰恰是「信从某家服务商发出去、但从没配过对齐」的典型特征——信封发件人是服务商的退信域名,SPF 在那个域名上通过,而没有任何东西把它和你的 From 头拴在一起。修法是在那家服务商上把 d=yourdomain.com 的 DKIM 签名配起来,那正是对齐这半边检查在找的东西。

怎么看自己的配置

有两个方向要查,需要的证据不一样。

对外——你公布了什么。 三条记录都是公开 DNS,谁都能读,你自己当然也能。把域名过一遍 SPF/DMARC 检测工具:它按十次预算展开 SPF 树,逐个标签读 DMARC 记录,验证外部报告地址是否已经授权你往那儿发,并探测常见的 DKIM selector。想看不加解释的原始记录,用 DNS 查询工具 直接查 yourdomain.com_dmarc.yourdomain.com 的 TXT。

对内——某一封信实际经历了什么。 公布的记录告诉你规则,不告诉你对某封具体邮件的判决。判决在邮件头里,收信服务器把它查到的结果写在那儿:

Authentication-Results: mx.example.net;
  spf=pass smtp.mailfrom=bounces.provider.com;
  dkim=pass header.d=yourcompany.com;
  dmarc=pass header.from=yourcompany.com

把这几行读透,整个模型就在一处摊开了:SPF 在服务商的域名上通过,DKIM 在你的域名上通过,而因为 DKIM 的 d=header.from 对齐,DMARC 通过。把任意一封信存成原始邮件源码,把头部丢进邮件头分析工具就能得到这样一份拆解——它完全在你浏览器里跑,邮件不会离开你的机器。完整的读法,包括 Received 链和伪造发件人的那几处破绽,见怎么读邮件头

一句话各自记住

  • SPF 回答「这个 IP 能替这个域名发信吗」——验的是信封发件人,收件人看不见,十次 DNS 查询封顶,转发即断。
  • DKIM 回答「这封信是被该域名公布过的密钥签过的吗」——跟着信走,扛转发,怕改动,而且 selector 无法枚举。
  • DMARC 回答「SPF 或 DKIM 的那次通过,属不属于读者真正看到的那个域名」——这就是对齐,三者里只有它保护 From 头。它同时还告诉收信方失败了该怎么办,并且给你寄报告。

三个都需要,而且需要它们对齐。没有 DMARC 的 SPF 和 DKIM,认证的是没人看的地址;而没有一条通过且对齐的 SPF 或 DKIM 撑着,DMARC 拒的就是你自己的信。

各块拼图归位之后,下一个问题就变成操作层面的了:怎么从一条什么都不做的策略走到 p=reject,中间还不至于把自己的账单邮件一起送进黑洞。那是上线节奏的问题而不是协议的问题,从 p=none 走到 p=reject 讲的就是那套分阶段的做法。