メインコンテンツまでスキップ

RFC 7208 - Sender Policy Framework (SPF)

  • ステータス: Proposed Standard
  • 発行日: April 2014
  • ストリーム: IETF
  • 廃止: RFC4408
  • エラッタ: エラッタなし

基本情報

  • RFC番号: 7208
  • タイトル: Sender Policy Framework (SPF) for Authorizing Use of Domains in Email
  • 日本語タイトル: 送信者ポリシーフレームワーク
  • 公開日: 2014年4月
  • ステータス: PROPOSED STANDARD (提案標準)
  • 著者: S. Kitterman

概要 (Abstract)

SPFは、ドメイン所有者がDNSレコードを通じて、そのドメインのメール送信を許可されたメールサーバーを指定できるようにします。受信者はSPFレコードを照会して、メールが許可されたサーバーから送信されたかを検証でき、メールスプーフィングの検出とブロックに役立ちます。

Contents

Appendices (付録)

SPF概要

SPFとは?

定義:

SPF = Sender Policy Framework (送信者ポリシーフレームワーク)
機能: 認可されたメール送信サーバーの検証
方法: DNS TXTレコード
目的:
✓ メールスプーフィングの防止
✓ スパムの削減
✓ メール配信性の向上

メールセキュリティトリオ:
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
↑明示的pass ↑明示的fail

v=spf1 ip4:203.0.113.1 ~all
↑デフォルト+ ↑softfail

v=spf1 ?all
↑neutral (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

説明:
- 2つのクラス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. a/mxの代わりにip4/ip6を使用
❌ 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
✓ すべてのIPを含む独自のSPFレコードを維持

3. SPF Flattening (SPF平坦化)
定期的にincludeからIPを照会し、ip4/ip6に変換

SPF Flatteningツール

// SPF Flattening例
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

✓ 正しい: 1つにマージ
v=spf1 ip4:203.0.113.1 include:provider.com -all

❌ 間違い3: 10検索以上
v=spf1 include:a include:b include:c ... (多すぎる)

✓ 正しい: ip4を直接使用またはflattening

❌ 間違い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との統合

完全なメールセキュリティ設定

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はメールセキュリティの最前線です。DNSレコードを通じて送信サーバーが認可され、メールスプーフィングが効果的に防止されます。DKIMとDMARCと組み合わせることで、完全なメールセキュリティシステムが構築されます。忘れないでください: ソフト失敗から始めて、徐々に厳格モードに強化し、10 DNS検索制限に注意してください!