网站邮件总进垃圾箱排查实战:SPF/DKIM/DMARC 三件套配置与端到端验证

一、为什么"我发的邮件"会进垃圾箱,而这个问题往往和服务器运维强相关

个人站长迟早会遇到这件事:网站上的评论通知、密码重置邮件、订阅确认邮件,客户说"我没收到"。你去发件箱看,显示已发送成功;让对方翻垃圾邮件箱,确实在里面;再让 Gmail 用户收,直接进了促销标签甚至被拒收。这时候很多人第一反应是"换个邮件服务商",但真正的原因通常不在服务商,而在你自己域名的三个 DNS 记录:SPF、DKIM、DMARC。

这三个记录的名字看起来很像营销术语,实际上它们是纯粹的服务器与 DNS 运维问题——配置在哪台 DNS 服务器上、以什么格式写、TTL 怎么设、改了之后多久生效,全都是站长每天在打交道的东西。这篇文章按排查顺序把三者的关系讲清楚,并给出可复制的验证命令。所有命令都是我自己在 Debian 服务器上实际用的。这不是一封 "如何提高邮件营销打开率" 的文章,而是一份 DNS 与投递链路的技术排查手册。

二、先分清三者各管什么,以及它们为什么必须一起配

用一句话概括三者的分工:

SPF(Sender Policy Framework)管的是"哪些服务器有资格用我的域名发信"。它是一条 TXT 记录,列出了授权的发送 IP 或发送服务,接收方收到邮件后检查发信服务器的 IP 在不在这个名单里,不在就判定为伪造。

DKIM(DomainKeys Identified Mail)管的是"这封邮件在传输途中有没有被篡改"。发信服务器用私钥对邮件头和正文的一部分做签名,把签名写在邮件头里;接收方通过 DNS 里的公钥记录验证签名。签名对不上,说明内容被改过。

DMARC(Domain-based Message Authentication, Reporting and Conformance)管的是"前两者都没通过时怎么办,以及结果汇报给谁"。它本身不验证任何东西,只声明策略:是放进垃圾箱(quarantine)、直接拒收(reject),还是只观察不处理(none),同时指定一个接收报告的邮箱。

关键点在于:SPF 和 DKIM 单独存在时价值有限,只有配上 DMARC,接收方才会真正参考它们的结果对邮件做处置。很多站长只配了 SPF 就以为万事大吉,结果邮件依然进垃圾箱,原因就是缺少 DMARC 策略——Gmail 和 Outlook 从 2024 年起对批量发信方强制要求 DMARC 存在,缺失本身就是扣分项。

三、SPF 的五类常见错误,以及一条命令验证

SPF 是最容易配错的,因为它的语法有几条硬性限制。先看验证命令:

dig +short TXT example.com | grep spf
# 或者更详细
dig TXT example.com +noall +answer

你应该看到类似这样的内容:

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

接下来逐条对照这五类错误:

错误一:DNS 查询次数超过 10 次。SPF 规范规定,一次校验过程中触发的 DNS 查询(包括 include、a、mx、ptr、exists)不得超过 10 次。超过的话,接收方直接返回 permerror,等于 SPF 完全失效。你用了三个邮件服务商,每个 include 后面自己又 include 了若干个,很容易就超。查看实际消耗的查询次数有专门的工具,也可以手动逐层 dig 展开 include 的域名数一数。

错误二:语法里的 -all 与 ~all 混用理解错。-all 是硬失败,任何不在名单里的服务器发的信都判定为伪造,应该拒收——这是推荐配置。~all 是软失败,只标记不拒收。很多站长为了"怕漏邮件"写成 ~all,结果是攻击者用你的域名发钓鱼信也能通过大部分检查,你的域名信誉照样受损。

错误三:SPF 记录重复存在。一个域名只能有一条 SPF 的 TXT 记录。如果你在 DNS 面板里手工加了一条,同时邮件服务商又自动帮你加了一条,两条并存的结果是 permerror,比没有更糟。用 dig +short TXT 一看就能发现——如果输出里出现两行 v=spf1,立刻合并成一条。

错误四:把 SPF 放在 CNAME 后面。SPF 必须是 TXT 记录,不能指向一个 CNAME,接收方不会跟进解析。

错误五:TXT 记录超长被截断。有些 DNS 面板对单个 TXT 值的长度有限制,超过会静默截断,你看到的记录是半截的。SPF 太长时要拆成多个引号包裹的字符串,DNS 层面会自动拼接,但前提是你的面板支持这种写法。

四、DKIM:从生成密钥到写入 DNS 的完整流程

DKIM 的配置分两半:私钥留在服务器上给邮件发送程序用,公钥以 TXT 记录的形式发到 DNS 里。以最常用的 OpenDKIM 为例,流程如下:

# 1. 生成密钥对(selector 取名 default,月/日便于日后轮换)
opendkim-genkey -b 2048 -d example.com -s default -D /etc/opendkim/keys/example.com/

# 2. 目录与文件权限:opendkim 用户必须能读到私钥
chown -R opendkim:opendkim /etc/opendkim/keys/example.com
chmod 600 /etc/opendkim/keys/example.com/default.private

# 3. 看看要写进 DNS 的内容
cat /etc/opendkim/keys/example.com/default.txt

第三步的输出会是一行形如 default._domainkey IN TXT ( "v=DKIM1; k=rsa; p=MIIBIjANBg..." ) 的内容。把它原样写进 DNS —— 注意是写在 default._domainkey.example.com 这个主机名上,不是根域名。这里最常见的错误有两个:一是把 p= 后面的公钥复制时漏了字符或引入了换行,二是 DNS 面板自动把长字符串截断。

验证 DKIM 是否写对:

dig +short TXT default._domainkey.example.com
# 应该输出 v=DKIM1; k=rsa; p=... 一长串
# 只想要公钥长度做个粗略检查:
dig +short TXT default._domainkey.example.com | tr -d '"' | grep -o 'p=[A-Za-z0-9+/=]*' | wc -c

最后这条命令的输出应该在一千多字节量级(2048 位 RSA 密钥的 base64 表示)。如果只有几十字节,说明公钥被截断了。

配置完别忘了在 MTA 里启用签名。/etc/opendkim.conf 需要指定 Domain、Selector、KeyTable、SigningTable 四项,然后重启 opendkim。真正的验证方式不是看本地配置,而是给自己发一封邮件,在收到的原始邮件头里找 DKIM-Signature 和 Authentication-Results——后者会明确写出 dkim=pass 或 dkim=fail,这是唯一可信的判据。

五、DMARC:别一上来就用 reject

DMARC 记录的格式是 v=DMARC1; p=none; rua=mailto:dmarc@example.com,写在 _dmarc.example.com 上。这里最重要的建议是:务必先用 p=none 观察至少两周,再逐步收紧到 quarantine、最后才到 reject。

原因很直接。DMARC 只报告不拦截的阶段,你会收到各接收方发来的汇总报告(通常是一堆 XML 附件压缩包),里面列出了所有用你域名发信的来源 IP 和它们的 SPF/DKIM 结果。如果你的业务邮件除了主服务器之外,还有客服系统、发票系统、第三方通知服务在发信,它们很可能没有被 SPF 覆盖。此时如果你直接上 p=reject,这些邮件会全部被拒收,而你可能几天后才发现——客户投诉"收不到验证码"的时候。

逐步收紧的实操节奏:

# 第 1-2 周:只观察
v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com; fo=1

# 第 3-4 周:可疑邮件进垃圾箱(先给 pct 留后路)
v=DMARC1; p=quarantine; pct=50; rua=mailto:dmarc-reports@example.com

# 稳定后:全量执行
v=DMARC1; p=reject; pct=100; rua=mailto:dmarc-reports@example.com; adkim=s; aspf=s

最后的 adkim=s; aspf=s 是严格对齐模式,要求邮件头里的 From 域名与 SPF/DKIM 校验的域名完全一致,比默认的宽松模式(子域名也算对齐)更严格。对自有域名发信的场景,建议用严格模式。

关于报告邮箱:rua 收到的 XML 报告人类基本读不了,必须用解析工具。如果你不想自己搭,可以直接把 rua 指向第三方聚合服务的地址,它们会把报告渲染成可读的每日摘要。关键是一定要收,不看报告的 DMARC 等于白配。

六、一条链路验证法:不要分段猜,要端到端看

配完之后,最高效的验证方式不是逐个 dig 记录,而是发一封真实邮件然后读完整邮件头。Authentication-Results 这一行会同时告诉你三项检查的结果:

Authentication-Results: mx.google.com;
       spf=pass (google.com: domain of bounce@example.com designates 203.0.113.10 as permitted sender) smtp.mailfrom=example.com;
       dkim=pass header.i=@example.com header.s=default header.b=XXXX;
       dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=example.com

三项全 pass,才算真的配好了。任何一项 fail 或 softfail,都在这行里能找到原因关键词:spf=softfail 通常是 ~all 或者 IP 没在名单里;dkim=fail (bad signature) 是签名与 DNS 公钥不匹配,多半是复制公钥时出错;dmarc=fail 则是前两者至少一个没通过,且域名没有对齐。

还有一个容易被忽略的运维细节:密钥轮换。DKIM 私钥泄露意味着任何人都能用你的域名签名发信。稳妥的做法是准备两个 selector(比如 default 和 2026q1),轮换时先发布新 selector 的公钥,等 DNS 生效并观察一段时间后再切换签名用的 selector,旧的那条记录再留一两个月才删。这样整个过程零中断,也不会因为 TTL 没到期而出现"签名用的是新密钥、DNS 还是旧公钥"的错配窗口。

七、小结

SPF、DKIM、DMARC 三者的关系可以记成一句话:SPF 管"谁能发",DKIM 管"有没有被改",DMARC 管"不合规时怎么办并告诉我"。它们全是 DNS 记录层面的配置,出问题的排查路径也高度一致——先 dig 记录本体确认语法和长度,再发真实邮件读 Authentication-Results 看端到端结果,最后用 DMARC 报告持续观察真实发信来源。对个人站长来说,这套配置一次做对,之后半年都不用再碰;而如果跳过它,邮件送达率会长期处在一个说不清原因的糟糕状态里,这个成本远比配置本身高。

Last modification:September 25th, 2026 at 09:24 pm

Leave a Comment