RFC 7208 - Sender Policy Framework (SPF)
基本信息
- RFC编号: 7208
- 标题: Sender Policy Framework (SPF) for Authorizing Use of Domains in Email
- 中文标题: 发件人策略框架
- 发布日期: 2014年4月
- 状态: PROPOSED STANDARD (提案标准)
- 作者: S. Kitterman
摘要 (Abstract)
SPF允许域名所有者通过DNS记录指定哪些邮件服务器被授权发送该域名的邮件。接收方可以查询SPF记录来验证邮件是否来自授权的服务器,从而帮助检测和阻止邮件伪造。
Contents
- 1. Introduction (引言)
- 2. Operational Overview (操作概述)
- 3. SPF Records (SPF 记录)
- 4. The check_host() Function (check_host() 函数)
- 5. Mechanism Definitions (机制定义)
- 6. Modifier Definitions (修饰符定义)
- 7. Macros (宏)
- 8. Result Handling (结果处理)
- 9. Recording the Result (记录结果)
- 10. Effects on Infrastructure (对基础设施的影响)
- 11. Security Considerations (安全考虑)
- 12. Collected ABNF (汇总的 ABNF)
- 13-15. Additional Sections (附加章节)
Appendices (附录)
- Appendix A. Extended Examples (扩展示例)
- Appendix B. Changes from RFC 4408 (与 RFC 4408 的变更)
- Appendix C. Further Testing Advice (测试建议)
- Appendix D. SPF/Mediator Interactions (SPF/中介交互)
- Appendix E. Mail Services (邮件服务)
- Appendix F. MTA Relays (MTA 中继)
- Appendix G. Local Policy Considerations (本地策略考虑)
SPF概述
什么是SPF?
定义:
SPF = Sender Policy Framework (发件人策略框架)
作用: 验证邮件发送服务器的授权
方式: DNS TXT记录
目的:
✓ 防止邮件伪造
✓ 减少垃圾邮件
✓ 提高邮件送达率
Email安全三件套:
1. SPF (本RFC) - 验证发送服务器
2. DKIM (RFC 6376) - 验证邮件内容
3. DMARC (RFC 7489) - 统一策略和报告
工作原理:
发送方 (example.com):
1. 在DNS发布SPF记录
example.com. IN TXT "v=spf1 ip4:203.0.113.1 -all"
→ 只有203.0.113.1被授权发送
2. 邮件服务器正常发送邮件
MAIL FROM: <[email protected]>
接收方:
1. 提取发件人域名
MAIL FROM: [email protected] → 域名: example.com
2. 查询SPF记录
DNS查询: example.com TXT记录
3. 检查发送服务器IP
发送服务器IP: 203.0.113.1
SPF记录允许: ip4:203.0.113.1
→ 匹配!
4. SPF结果:
Pass ✓ → 授权服务器
Fail ✗ → 未授权服务器
SPF vs DKIM vs DMARC
特性对比:
SPF (RFC 7208):
- 验证: 发送服务器IP
- 位置: SMTP MAIL FROM
- DNS记录: TXT
- 局限: 转发邮件会失败
DKIM (RFC 6376):
- 验证: 邮件数字签名
- 位置: DKIM-Signature头部
- DNS记录: TXT (_domainkey)
- 局限: 需要正确配置密钥
DMARC (RFC 7489):
- 验证: SPF + DKIM对齐
- 位置: From头部
- DNS记录: TXT (_dmarc)
- 功能: 策略 + 报告
综合使用:
SPF + DKIM → DMARC通过 → 最佳保护
SPF记录格式
基本语法
v=spf1 <mechanisms> <qualifiers> <modifiers>
示例:
v=spf1 ip4:192.0.2.0/24 include:_spf.example.com -all
组成部分:
- v=spf1: 版本标识 (必需,始终是spf1)
- mechanisms: 匹配机制
- qualifiers: 结果限定符
- modifiers: 修饰符
机制 (Mechanisms)
1. all:
定义: 匹配所有IP
用途: 通常作为默认策略放在最后
示例:
v=spf1 -all 所有IP都不允许 (最严格)
v=spf1 ~all 所有IP软失败 (推荐)
v=spf1 +all 所有IP都允许 (不推荐!)
2. ip4/ip6:
定义: 明确指定IP地址或网段
示例:
v=spf1 ip4:203.0.113.1 -all
→ 只允许203.0.113.1
v=spf1 ip4:192.0.2.0/24 -all
→ 允许192.0.2.0-192.0.2.255
v=spf1 ip6:2001:db8::1 -all
→ 允许IPv6地址
v=spf1 ip4:203.0.113.0/24 ip4:198.51.100.0/24 -all
→ 允许多个网段
3. a:
定义: 当前域名的A/AAAA记录
示例:
v=spf1 a -all
→ 允许example.com的A记录IP
v=spf1 a:mail.example.com -all
→ 允许mail.example.com的A记录IP
v=spf1 a/24 -all
→ 允许example.com的A记录IP所在的/24网段
4. mx:
定义: 当前域名的MX记录
示例:
v=spf1 mx -all
→ 允许example.com的MX服务器IP
v=spf1 mx:example.com -all
→ 允许example.com的MX服务器IP
v=spf1 mx/24 -all
→ 允许MX服务器所在的/24网段
5. include:
定义: 包含另一个域名的SPF记录
示例:
v=spf1 include:_spf.google.com -all
→ 允许Google的邮件服务器 (Gmail for Business)
v=spf1 include:spf.protection.outlook.com -all
→ 允许Microsoft 365的邮件服务器
多个include:
v=spf1 include:_spf.google.com include:spf.protection.outlook.com -all
注意: 最多10次DNS查询限制!
6. exists:
定义: 如果指定的域名存在A记录则匹配
示例:
v=spf1 exists:%{i}.spamhaus.example.com -all
→ 高级用法,通常用于反垃圾邮件黑名单检查
宏展开:
%{i} = 发送服务器IP (倒序)
7. ptr (不推荐):
定义: 反向DNS查询
示例:
v=spf1 ptr:example.com -all
问题:
❌ 性能差 (需要反向DNS查询)
❌ 可靠性低
❌ RFC明确不推荐使用
替代: 使用ip4/ip6或include
限定符 (Qualifiers)
符号 | 名称 | 含义 | 建议使用
-----|------|------|----------
+ | Pass | 通过 (默认) | 授权服务器
- | Fail | 失败 | 拒绝邮件
~ | SoftFail | 软失败 | 接受但标记
? | Neutral | 中性 | 无明确策略
示例:
v=spf1 +ip4:203.0.113.1 -all
↑明确通过 ↑明确失败
v=spf1 ip4:203.0.113.1 ~all
↑默认+ ↑软失败
v=spf1 ?all
↑中性 (等同于没有SPF)
限定符使用建议:
+ (Pass):
✓ 授权的邮件服务器
示例: +ip4:203.0.113.1
- (Fail):
✓ 最终的-all (严格)
✓ 明确禁止某些IP
示例: -all
~ (SoftFail):
✓ 最终的~all (宽松,推荐初期)
✓ 过渡期使用
示例: ~all
? (Neutral):
✗ 很少使用
✗ 等同于没有策略
修饰符 (Modifiers)
1. redirect:
定义: 重定向到另一个域名的SPF记录
示例:
example.com: v=spf1 redirect=_spf.example.com
_spf.example.com: v=spf1 ip4:203.0.113.1 -all
用途:
✓ 集中管理SPF
✓ 多个域名共享策略
注意:
- redirect后不能有其他机制
- 不能与all同时使用
2. exp:
定义: 解释说明 (SPF失败时的说明文本)
示例:
v=spf1 -all exp=explain.example.com
explain.example.com TXT记录:
"This domain does not send email"
用途:
✓ 提供用户友好的错误信息
- 很少被实际使用
SPF记录示例
基础配置
1. 只有一个邮件服务器:
v=spf1 ip4:203.0.113.1 -all
说明:
- 只允许203.0.113.1发送邮件
- 其他IP都拒绝
2. 使用MX记录:
v=spf1 mx -all
说明:
- 允许域名的MX服务器发送邮件
- 自动适应MX记录变化
3. 多个IP段:
v=spf1 ip4:192.0.2.0/24 ip4:198.51.100.0/24 -all
说明:
- 允许两个C类网段
- 适合多机房部署
第三方服务
4. Google Workspace (Gmail for Business):
v=spf1 include:_spf.google.com -all
说明:
- 使用Google发送邮件
- 包含Google的SPF记录
5. Microsoft 365:
v=spf1 include:spf.protection.outlook.com -all
6. SendGrid:
v=spf1 include:sendgrid.net -all
7. Mailchimp:
v=spf1 include:servers.mcsv.net -all
混合配置
8. 自有服务器 + 第三方:
v=spf1 ip4:203.0.113.1 include:_spf.google.com -all
说明:
- 自己的服务器: 203.0.113.1
- Google Workspace: include
9. 多个第三方服务:
v=spf1 include:_spf.google.com include:spf.protection.outlook.com include:sendgrid.net -all
注意: 每个include算一次DNS查询
10. 不发送邮件的域名:
v=spf1 -all
说明:
- 域名不发送邮件
- 防止被伪造
- 适合只接收邮件的域名
子域名
11. 子域名单独配置:
example.com: v=spf1 ip4:203.0.113.1 -all
mail.example.com: v=spf1 include:_spf.google.com -all
说明:
- 主域使用自己的服务器
- mail子域使用Google
12. 子域名继承 (无SPF记录时):
如果mail.example.com没有SPF记录:
→ 使用example.com的SPF记录
如果不想继承:
mail.example.com: v=spf1 -all
SPF检查流程
接收方验证步骤
// SPF验证伪代码
async function checkSPF(clientIP, sender, helo) {
// 1. 提取域名
const domain = sender.split('@')[1]; // [email protected] → example.com
// 2. 查询SPF记录
const spfRecord = await queryDNS(domain, 'TXT', 'v=spf1');
if (!spfRecord) {
return 'none'; // 没有SPF记录
}
// 3. 解析SPF记录
const mechanisms = parseSPF(spfRecord);
// 4. 逐个检查机制
for (const mechanism of mechanisms) {
const result = await evaluateMechanism(mechanism, clientIP, domain);
if (result !== null) {
return result; // 找到匹配,返回结果
}
}
return 'neutral'; // 没有匹配
}
// 评估单个机制
async function evaluateMechanism(mechanism, clientIP, domain) {
const { type, value, qualifier } = mechanism;
switch (type) {
case 'ip4':
if (isInIPRange(clientIP, value)) {
return mapQualifier(qualifier); // +pass, -fail, ~softfail
}
break;
case 'a':
const aRecords = await queryDNS(value || domain, 'A');
if (aRecords.includes(clientIP)) {
return mapQualifier(qualifier);
}
break;
case 'mx':
const mxRecords = await queryDNS(value || domain, 'MX');
for (const mx of mxRecords) {
const mxIPs = await queryDNS(mx, 'A');
if (mxIPs.includes(clientIP)) {
return mapQualifier(qualifier);
}
}
break;
case 'include':
const includeResult = await checkSPF(clientIP, `user@${value}`, null);
if (includeResult === 'pass') {
return mapQualifier(qualifier);
}
break;
case 'all':
return mapQualifier(qualifier);
}
return null; // 不匹配
}
SPF结果
返回值 | 含义 | 建议处理
-----------------|----------------|------------------
none | 无SPF记录 | 接受 (降低信任度)
neutral | 明确无策略 | 接受
pass | 通过 | 接受
fail | 失败 | 拒绝
softfail | 软失败 | 接受但标记
temperror | 临时错误 | 稍后重试
permerror | 永久错误 | 拒绝
SMTP响应示例:
pass: 250 OK (SPF pass)
fail: 550 SPF check failed
softfail: 250 OK (添加 X-SPF: softfail 头部)
DNS查询限制
10次查询限制
问题:
SPF检查最多执行10次DNS查询
超过限制 → permerror (永久错误)
计入10次的操作:
✓ include
✓ a
✓ mx
✓ exists
✓ redirect
不计入的操作:
✗ ip4/ip6 (直接匹配)
✗ all (直接匹配)
示例 - 超过限制:
v=spf1
include:_spf1.example.com ← 1
include:_spf2.example.com ← 2
include:_spf3.example.com ← 3
include:_spf4.example.com ← 4
include:_spf5.example.com ← 5
include:_spf6.example.com ← 6
include:_spf7.example.com ← 7
include:_spf8.example.com ← 8
include:_spf9.example.com ← 9
include:_spf10.example.com ← 10
include:_spf11.example.com ← 超过!permerror
-all
如果任何include又包含其他include,也计入总数!
解决方案:
1. 使用ip4/ip6代替a/mx
❌ v=spf1 a mx -all (2次查询)
✓ v=spf1 ip4:203.0.113.1 ip4:198.51.100.1 -all (0次)
2. 合并include
❌ include:service1.com include:service2.com
✓ 自己维护一个SPF记录,包含所有IP
3. 扁平化SPF (SPF Flattening)
定期查询include的IP,转换为ip4/ip6
SPF扁平化工具
// SPF扁平化示例
async function flattenSPF(domain) {
const spf = await querySPF(domain);
const ips = [];
// 解析SPF
const mechanisms = parseSPF(spf);
for (const mech of mechanisms) {
if (mech.type === 'ip4' || mech.type === 'ip6') {
ips.push(mech.value);
} else if (mech.type === 'include') {
// 递归查询include
const includeIPs = await resolveInclude(mech.value);
ips.push(...includeIPs);
} else if (mech.type === 'a') {
const aRecords = await queryDNS(mech.value, 'A');
ips.push(...aRecords.map(ip => `ip4:${ip}`));
} else if (mech.type === 'mx') {
const mxRecords = await queryDNS(mech.value, 'MX');
for (const mx of mxRecords) {
const mxIPs = await queryDNS(mx, 'A');
ips.push(...mxIPs.map(ip => `ip4:${ip}`));
}
}
}
// 生成扁平化SPF
return `v=spf1 ${ips.join(' ')} -all`;
}
// 使用
const flatSPF = await flattenSPF('example.com');
console.log('Flattened SPF:', flatSPF);
// v=spf1 ip4:74.125.0.0/16 ip4:209.85.128.0/17 ip4:216.58.192.0/19 -all
实战工具
SPF记录生成器
class SPFBuilder {
constructor(domain) {
this.domain = domain;
this.mechanisms = [];
this.modifier = null;
}
addIP(ip) {
if (ip.includes(':')) {
this.mechanisms.push(`ip6:${ip}`);
} else {
this.mechanisms.push(`ip4:${ip}`);
}
return this;
}
addIPRange(cidr) {
if (cidr.includes(':')) {
this.mechanisms.push(`ip6:${cidr}`);
} else {
this.mechanisms.push(`ip4:${cidr}`);
}
return this;
}
useA() {
this.mechanisms.push('a');
return this;
}
useMX() {
this.mechanisms.push('mx');
return this;
}
include(domain) {
this.mechanisms.push(`include:${domain}`);
return this;
}
setDefault(qualifier) {
const qualifiers = { pass: '+all', fail: '-all', softfail: '~all', neutral: '?all' };
this.mechanisms.push(qualifiers[qualifier] || '-all');
return this;
}
redirect(domain) {
this.modifier = `redirect=${domain}`;
return this;
}
build() {
let spf = 'v=spf1';
if (this.mechanisms.length > 0) {
spf += ' ' + this.mechanisms.join(' ');
}
if (this.modifier) {
spf += ' ' + this.modifier;
}
return spf;
}
countLookups() {
let count = 0;
for (const mech of this.mechanisms) {
if (mech.startsWith('include:') || mech.startsWith('a') ||
mech.startsWith('mx') || mech.startsWith('exists:')) {
count++;
}
}
if (this.modifier && this.modifier.startsWith('redirect=')) {
count++;
}
return count;
}
}
// 使用示例
const spf = new SPFBuilder('example.com')
.addIP('203.0.113.1')
.addIPRange('192.0.2.0/24')
.include('_spf.google.com')
.include('spf.protection.outlook.com')
.setDefault('fail')
.build();
console.log('SPF Record:', spf);
console.log('DNS Lookups:', spf.countLookups());
// 输出:
// SPF Record: v=spf1 ip4:203.0.113.1 ip4:192.0.2.0/24 include:_spf.google.com include:spf.protection.outlook.com -all
// DNS Lookups: 2
SPF验证工具
const dns = require('dns').promises;
class SPFChecker {
async check(domain, ip) {
try {
// 查询SPF记录
const records = await dns.resolveTxt(domain);
const spfRecord = records.find(r =>
r.join('').startsWith('v=spf1')
);
if (!spfRecord) {
return { result: 'none', message: 'No SPF record found' };
}
const spf = spfRecord.join('');
console.log('SPF Record:', spf);
// 解析并验证
const result = await this.evaluate(spf, ip, domain);
return result;
} catch (err) {
return { result: 'temperror', message: err.message };
}
}
async evaluate(spf, ip, domain, depth = 0) {
if (depth > 10) {
return { result: 'permerror', message: 'Too many DNS lookups' };
}
const parts = spf.split(/\s+/);
for (const part of parts) {
if (part === 'v=spf1') continue;
// 提取限定符
let qualifier = '+';
let mechanism = part;
if (['+', '-', '~', '?'].includes(part[0])) {
qualifier = part[0];
mechanism = part.slice(1);
}
// 检查机制
if (mechanism.startsWith('ip4:')) {
const range = mechanism.slice(4);
if (this.isIPInRange(ip, range)) {
return this.mapResult(qualifier);
}
} else if (mechanism.startsWith('include:')) {
const includeDomain = mechanism.slice(8);
const includeRecords = await dns.resolveTxt(includeDomain);
const includeSPF = includeRecords.find(r =>
r.join('').startsWith('v=spf1')
);
if (includeSPF) {
const result = await this.evaluate(
includeSPF.join(''),
ip,
includeDomain,
depth + 1
);
if (result.result === 'pass') {
return this.mapResult(qualifier);
}
}
} else if (mechanism === 'all' || mechanism === '-all' ||
mechanism === '~all' || mechanism === '?all') {
return this.mapResult(qualifier);
}
// 可以添加更多机制...
}
return { result: 'neutral', message: 'No match found' };
}
isIPInRange(ip, range) {
// 简化版本,生产环境需要更完整的实现
if (!range.includes('/')) {
return ip === range;
}
// CIDR匹配实现省略...
return false;
}
mapResult(qualifier) {
const map = {
'+': { result: 'pass', message: 'SPF pass' },
'-': { result: 'fail', message: 'SPF fail' },
'~': { result: 'softfail', message: 'SPF softfail' },
'?': { result: 'neutral', message: 'SPF neutral' }
};
return map[qualifier] || map['+'];
}
}
// 使用
const checker = new SPFChecker();
const result = await checker.check('example.com', '203.0.113.1');
console.log('SPF Check Result:', result);
部署最佳实践
1. 分阶段部署
阶段1: 监控模式
v=spf1 ?all
或
v=spf1 ~all
作用: 收集数据,观察哪些服务器发送邮件
持续: 2-4周
阶段2: 软失败
v=spf1 ip4:x.x.x.x include:provider.com ~all
作用: 标记但不拒绝未授权邮件
持续: 4-8周
阶段3: 严格模式
v=spf1 ip4:x.x.x.x include:provider.com -all
作用: 拒绝未授权邮件
2. 常见错误
❌ 错误1: 忘记-all
v=spf1 ip4:203.0.113.1
→ 等同于 v=spf1 ip4:203.0.113.1 ?all
→ 任何IP都是neutral
✓ 正确:
v=spf1 ip4:203.0.113.1 -all
❌ 错误2: 多个SPF记录
example.com TXT "v=spf1 ip4:203.0.113.1 -all"
example.com TXT "v=spf1 include:provider.com -all"
→ permerror
✓ 正确: 合并为一条
v=spf1 ip4:203.0.113.1 include:provider.com -all
❌ 错误3: 超过10次查询
v=spf1 include:a include:b include:c ... (太多)
✓ 正确: 使用ip4直接指定或扁平化
❌ 错误4: 使用ptr
v=spf1 ptr:example.com -all
→ 性能差,不推荐
✓ 正确: 使用ip4或include
3. 测试和验证
# 命令行测试工具
# 1. 查询SPF记录
dig example.com TXT | grep "v=spf1"
或
nslookup -type=TXT example.com
# 2. 使用在线工具
# - https://mxtoolbox.com/spf.aspx
# - https://www.kitterman.com/spf/validate.html
# 3. 发送测试邮件
# 发送到自己的邮箱,检查邮件头部:
# Received-SPF: pass ...
与DKIM/DMARC集成
完整的Email安全配置
1. SPF记录:
example.com. IN TXT "v=spf1 ip4:203.0.113.1 include:_spf.google.com -all"
2. DKIM记录:
default._domainkey.example.com. IN TXT "v=DKIM1; k=rsa; p=MIGfMA0..."
3. DMARC记录:
_dmarc.example.com. IN TXT "v=DMARC1; p=reject; rua=mailto:[email protected]"
结果:
- SPF验证发送服务器 ✓
- DKIM验证邮件内容 ✓
- DMARC统一策略 ✓
→ 三重保护!
参考文献
SPF相关RFC:
- [RFC 7208] SPF ← 本文档
- [RFC 7489] DMARC
- [RFC 6376] DKIM
相关资源:
总结: SPF是Email安全的第一道防线,通过DNS记录授权发送服务器,有效防止邮件伪造。配合DKIM和DMARC,可以构建完整的邮件安全体系。记住:从软失败开始,逐步强化到严格模式,并注意10次DNS查询限制!