Aller au contenu principal

RFC 7208 - Sender Policy Framework (SPF)

  • Statut: Proposed Standard
  • Publié: April 2014
  • Stream: IETF
  • Remplace: RFC4408
  • Errata: Pas d'errata

Informations de base

  • Numéro RFC: 7208
  • Titre: Sender Policy Framework (SPF) for Authorizing Use of Domains in Email
  • Titre français: Cadre de Politique de l'Expéditeur
  • Date de publication: Avril 2014
  • Statut: PROPOSED STANDARD (Norme proposée)
  • Auteur: S. Kitterman

Résumé (Abstract)

SPF permet aux propriétaires de domaines de spécifier via des enregistrements DNS quels serveurs de messagerie sont autorisés à envoyer des e-mails pour ce domaine. Les destinataires peuvent interroger les enregistrements SPF pour vérifier si les e-mails proviennent de serveurs autorisés, aidant ainsi à détecter et bloquer l'usurpation d'e-mails.

Contents

Appendices (Annexes)

Aperçu du SPF

Qu'est-ce que le SPF?

Définition:

SPF = Sender Policy Framework (Cadre de Politique de l'Expéditeur)
Fonction: Vérification des serveurs de messagerie autorisés
Méthode: Enregistrements DNS TXT
Objectif:
✓ Prévenir l'usurpation d'e-mail
✓ Réduire le spam
✓ Améliorer la délivrabilité des e-mails

Trio de sécurité e-mail:
1. SPF (ce RFC) - Vérifie le serveur d'envoi
2. DKIM (RFC 6376) - Vérifie le contenu de l'e-mail
3. DMARC (RFC 7489) - Politique et rapports unifiés

Principe de fonctionnement:

Expéditeur (example.com):
1. Publie un enregistrement SPF dans le DNS
example.com. IN TXT "v=spf1 ip4:203.0.113.1 -all"
→ Seul 203.0.113.1 est autorisé

2. Le serveur de messagerie envoie normalement des e-mails
MAIL FROM: `<[email protected]>`

Destinataire:
1. Extrait le domaine de l'expéditeur
MAIL FROM: [email protected] → Domaine: example.com

2. Interroge l'enregistrement SPF
Requête DNS: enregistrement TXT example.com

3. Vérifie l'IP du serveur d'envoi
IP du serveur d'envoi: 203.0.113.1
Enregistrement SPF autorise: ip4:203.0.113.1
→ Correspondance!

4. Résultat SPF:
Pass ✓ → Serveur autorisé
Fail ✗ → Serveur non autorisé

SPF vs DKIM vs DMARC

Comparaison des fonctionnalités:

SPF (RFC 7208):
- Vérifie: IP du serveur d'envoi
- Emplacement: SMTP MAIL FROM
- Enregistrement DNS: TXT
- Limitation: Les e-mails transférés échouent

DKIM (RFC 6376):
- Vérifie: Signature numérique de l'e-mail
- Emplacement: En-tête DKIM-Signature
- Enregistrement DNS: TXT (_domainkey)
- Limitation: Nécessite une configuration correcte des clés

DMARC (RFC 7489):
- Vérifie: Alignement SPF + DKIM
- Emplacement: En-tête From
- Enregistrement DNS: TXT (_dmarc)
- Fonction: Politique + Rapports

Utilisation combinée:
SPF + DKIM → DMARC réussi → Protection optimale

Format d'enregistrement SPF

Syntaxe de base

v=spf1 <mechanisms> <qualifiers> <modifiers>

Exemple:
v=spf1 ip4:192.0.2.0/24 include:_spf.example.com -all

Composants:
- v=spf1: Identifiant de version (requis, toujours spf1)
- mechanisms: Mécanismes de correspondance
- qualifiers: Qualificateurs de résultat
- modifiers: Modificateurs

Mécanismes (Mechanisms)

1. all:

Définition: Correspond à toutes les IPs
Utilisation: Généralement comme politique par défaut à la fin

Exemples:
v=spf1 -all Toutes les IPs non autorisées (le plus strict)
v=spf1 ~all Toutes les IPs en échec doux (recommandé)
v=spf1 +all Toutes les IPs autorisées (non recommandé!)

2. ip4/ip6:

Définition: Adresses IP ou plages explicites

Exemples:
v=spf1 ip4:203.0.113.1 -all
→ Seul 203.0.113.1 est autorisé

v=spf1 ip4:192.0.2.0/24 -all
→ Autorise 192.0.2.0-192.0.2.255

v=spf1 ip6:2001:db8::1 -all
→ Autorise l'adresse IPv6

v=spf1 ip4:203.0.113.0/24 ip4:198.51.100.0/24 -all
→ Autorise plusieurs plages

3. a:

Définition: Enregistrements A/AAAA du domaine actuel

Exemples:
v=spf1 a -all
→ Autorise les IPs de l'enregistrement A d'example.com

v=spf1 a:mail.example.com -all
→ Autorise les IPs de l'enregistrement A de mail.example.com

v=spf1 a/24 -all
→ Autorise le réseau /24 de l'IP de l'enregistrement A d'example.com

4. mx:

Définition: Enregistrements MX du domaine actuel

Exemples:
v=spf1 mx -all
→ Autorise les IPs des serveurs MX d'example.com

v=spf1 mx:example.com -all
→ Autorise les IPs des serveurs MX d'example.com

v=spf1 mx/24 -all
→ Autorise le réseau /24 des serveurs MX

5. include:

Définition: Inclut l'enregistrement SPF d'un autre domaine

Exemples:
v=spf1 include:_spf.google.com -all
→ Autorise les serveurs de messagerie Google (Gmail for Business)

v=spf1 include:spf.protection.outlook.com -all
→ Autorise les serveurs de messagerie Microsoft 365

Plusieurs includes:
v=spf1 include:_spf.google.com include:spf.protection.outlook.com -all

Attention: Limite de 10 recherches DNS maximum!

6. exists:

Définition: Correspond si le domaine spécifié a un enregistrement A

Exemple:
v=spf1 exists:%\{i}.spamhaus.example.com -all
→ Utilisation avancée, généralement pour les vérifications de liste noire anti-spam

Expansion de macro:
%\{i} = IP du serveur d'envoi (inversée)

7. ptr (non recommandé):

Définition: Requête DNS inverse

Exemple:
v=spf1 ptr:example.com -all

Problèmes:
❌ Mauvaises performances (nécessite une requête DNS inverse)
❌ Fiabilité faible
❌ RFC déconseille explicitement son utilisation

Alternative: Utilisez ip4/ip6 ou include

Qualificateurs (Qualifiers)

Symbole | Nom | Signification | Utilisation recommandée
--------|-----|---------------|------------------------
+ | Pass | Réussi (par défaut) | Serveurs autorisés
- | Fail | Échec | Rejeter l'e-mail
~ | SoftFail | Échec doux | Accepter mais marquer
? | Neutral | Neutre | Pas de politique claire

Exemples:
v=spf1 +ip4:203.0.113.1 -all
↑pass explicite ↑échec explicite

v=spf1 ip4:203.0.113.1 ~all
↑+ par défaut ↑échec doux

v=spf1 ?all
↑neutre (équivalent à pas de SPF)

Recommandations d'utilisation des qualificateurs:

+ (Pass): 
✓ Serveurs de messagerie autorisés
Exemple: +ip4:203.0.113.1

- (Fail):
✓ -all final (strict)
✓ Interdire explicitement certaines IPs
Exemple: -all

~ (SoftFail):
✓ ~all final (souple, recommandé au début)
✓ Utiliser pendant la période de transition
Exemple: ~all

? (Neutral):
✗ Rarement utilisé
✗ Équivalent à pas de politique

Modificateurs (Modifiers)

1. redirect:

Définition: Redirection vers l'enregistrement SPF d'un autre domaine

Exemple:
example.com: v=spf1 redirect=_spf.example.com
_spf.example.com: v=spf1 ip4:203.0.113.1 -all

Objectif:
✓ Gestion centralisée du SPF
✓ Plusieurs domaines partagent une politique

Attention:
- Aucun autre mécanisme ne peut suivre redirect
- Ne peut pas être utilisé avec all

2. exp:

Définition: Explication (texte d'explication en cas d'échec SPF)

Exemple:
v=spf1 -all exp=explain.example.com

Enregistrement TXT explain.example.com:
"This domain does not send email"

Objectif:
✓ Fournir des messages d'erreur conviviaux
- Rarement utilisé en pratique

Exemples d'enregistrements SPF

Configurations de base

1. Un seul serveur de messagerie:

v=spf1 ip4:203.0.113.1 -all

Explication:
- Seul 203.0.113.1 peut envoyer des e-mails
- Autres IPs sont rejetées

2. Utilisation des enregistrements MX:

v=spf1 mx -all

Explication:
- Autorise les serveurs MX du domaine à envoyer des e-mails
- S'adapte automatiquement aux changements MX

3. Plusieurs plages IP:

v=spf1 ip4:192.0.2.0/24 ip4:198.51.100.0/24 -all

Explication:
- Autorise deux réseaux de classe C
- Adapté au déploiement multi-datacenter

Services tiers

4. Google Workspace (Gmail for Business):

v=spf1 include:_spf.google.com -all

Explication:
- Utilise Google pour envoyer des e-mails
- Inclut l'enregistrement SPF de Google

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

Configurations mixtes

8. Serveur propre + Tiers:

v=spf1 ip4:203.0.113.1 include:_spf.google.com -all

Explication:
- Serveur propre: 203.0.113.1
- Google Workspace: include

9. Plusieurs services tiers:

v=spf1 include:_spf.google.com include:spf.protection.outlook.com include:sendgrid.net -all

Attention: Chaque include compte pour une recherche DNS

10. Domaine n'envoyant pas d'e-mails:

v=spf1 -all

Explication:
- Le domaine n'envoie pas d'e-mails
- Empêche l'usurpation
- Adapté aux domaines en réception seulement

Sous-domaines

11. Configuration séparée des sous-domaines:

example.com: v=spf1 ip4:203.0.113.1 -all
mail.example.com: v=spf1 include:_spf.google.com -all

Explication:
- Le domaine principal utilise son propre serveur
- Le sous-domaine mail utilise Google

12. Héritage de sous-domaine (sans enregistrement SPF):

Si mail.example.com n'a pas d'enregistrement SPF:
→ Utilise l'enregistrement SPF d'example.com

Si pas d'héritage souhaité:
mail.example.com: v=spf1 -all

Processus de vérification SPF

Étapes de vérification du destinataire

// Pseudo-code de vérification SPF
async function checkSPF(clientIP, sender, helo) {
// 1. Extraire le domaine
const domain = sender.split('@')[1]; // [email protected] → example.com

// 2. Interroger l'enregistrement SPF
const spfRecord = await queryDNS(domain, 'TXT', 'v=spf1');

if (!spfRecord) {
return 'none'; // Pas d'enregistrement SPF
}

// 3. Analyser l'enregistrement SPF
const mechanisms = parseSPF(spfRecord);

// 4. Vérifier les mécanismes un par un
for (const mechanism of mechanisms) {
const result = await evaluateMechanism(mechanism, clientIP, domain);

if (result !== null) {
return result; // Correspondance trouvée, retourner le résultat
}
}

return 'neutral'; // Pas de correspondance
}

// Évaluer un seul mécanisme
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; // Pas de correspondance
}

Résultats SPF

Valeur de retour   | Signification         | Traitement recommandé
-------------------|-----------------------|----------------------
none | Pas d'enregistrement | Accepter (confiance réduite)
| SPF |
neutral | Politique explicite | Accepter
| aucune |
pass | Réussi | Accepter
fail | Échec | Rejeter
softfail | Échec doux | Accepter mais marquer
temperror | Erreur temporaire | Réessayer plus tard
permerror | Erreur permanente | Rejeter

Exemples de réponses SMTP:
pass: 250 OK (SPF pass)
fail: 550 SPF check failed
softfail: 250 OK (ajouter en-tête X-SPF: softfail)

Limites de recherche DNS

Limite de 10 recherches

Problème:

La vérification SPF effectue au maximum 10 recherches DNS
Dépassement de la limite → permerror (Erreur permanente)

Compte pour les 10 recherches:
✓ include
✓ a
✓ mx
✓ exists
✓ redirect

Ne compte pas:
✗ ip4/ip6 (correspondance directe)
✗ all (correspondance directe)

Exemple - Limite dépassée:

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 ← Dépassé! permerror
-all

Si un include contient d'autres includes, ceux-ci comptent aussi!

Solutions:

1. Utiliser ip4/ip6 au lieu de a/mx
❌ v=spf1 a mx -all (2 recherches)
✓ v=spf1 ip4:203.0.113.1 ip4:198.51.100.1 -all (0 recherche)

2. Fusionner les includes
❌ include:service1.com include:service2.com
✓ Maintenir son propre enregistrement SPF contenant toutes les IPs

3. SPF Flattening (Aplatissement SPF)
Interroger régulièrement les IPs des includes, convertir en ip4/ip6

Outil SPF Flattening

// Exemple SPF Flattening
async function flattenSPF(domain) {
const spf = await querySPF(domain);
const ips = [];

// Analyser 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') {
// Interroger récursivement les includes
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}`));
}
}
}

// Générer SPF aplati
return `v=spf1 ${ips.join(' ')} -all`;
}

// Utilisation
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

Outils pratiques

Générateur d'enregistrement 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;
}
}

// Exemple d'utilisation
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());

// Sortie:
// 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

Outil de vérification SPF

const dns = require('dns').promises;

class SPFChecker {
async check(domain, ip) {
try {
// Interroger l'enregistrement 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);

// Analyser et vérifier
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;

// Extraire le qualificateur
let qualifier = '+';
let mechanism = part;

if (['+', '-', '~', '?'].includes(part[0])) {
qualifier = part[0];
mechanism = part.slice(1);
}

// Vérifier le mécanisme
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);
}
// D'autres mécanismes peuvent être ajoutés ici...
}

return { result: 'neutral', message: 'No match found' };
}

isIPInRange(ip, range) {
// Version simplifiée, l'environnement de production nécessite une implémentation complète
if (!range.includes('/')) {
return ip === range;
}
// Implémentation de correspondance CIDR omise...
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['+'];
}
}

// Utilisation
const checker = new SPFChecker();
const result = await checker.check('example.com', '203.0.113.1');
console.log('SPF Check Result:', result);

Meilleures pratiques de déploiement

1. Déploiement progressif

Phase 1: Mode surveillance
v=spf1 ?all
ou
v=spf1 ~all

Objectif: Collecter des données, observer quels serveurs envoient des e-mails
Durée: 2-4 semaines

Phase 2: Échec doux
v=spf1 ip4:x.x.x.x include:provider.com ~all

Objectif: Marquer mais ne pas rejeter les e-mails non autorisés
Durée: 4-8 semaines

Phase 3: Mode strict
v=spf1 ip4:x.x.x.x include:provider.com -all

Objectif: Rejeter les e-mails non autorisés

2. Erreurs courantes

❌ Erreur 1: Oublier -all
v=spf1 ip4:203.0.113.1
→ Équivalent à v=spf1 ip4:203.0.113.1 ?all
→ Toute IP est neutral

✓ Correct:
v=spf1 ip4:203.0.113.1 -all

❌ Erreur 2: Plusieurs enregistrements SPF
example.com TXT "v=spf1 ip4:203.0.113.1 -all"
example.com TXT "v=spf1 include:provider.com -all"
→ permerror

✓ Correct: Fusionner en un seul
v=spf1 ip4:203.0.113.1 include:provider.com -all

❌ Erreur 3: Plus de 10 recherches
v=spf1 include:a include:b include:c ... (trop)

✓ Correct: Utiliser ip4 directement ou flattening

❌ Erreur 4: Utilisation de ptr
v=spf1 ptr:example.com -all
→ Mauvaises performances, non recommandé

✓ Correct: Utiliser ip4 ou include

3. Tests et vérification

# Outils de test en ligne de commande

# 1. Interroger l'enregistrement SPF
dig example.com TXT | grep "v=spf1"
ou
nslookup -type=TXT example.com

# 2. Utiliser des outils en ligne
# - https://mxtoolbox.com/spf.aspx
# - https://www.kitterman.com/spf/validate.html

# 3. Envoyer un e-mail de test
# Envoyer à votre propre boîte e-mail, vérifier l'en-tête:
# Received-SPF: pass ...

Intégration avec DKIM/DMARC

Configuration complète de sécurité e-mail

1. Enregistrement SPF:
example.com. IN TXT "v=spf1 ip4:203.0.113.1 include:_spf.google.com -all"

2. Enregistrement DKIM:
default._domainkey.example.com. IN TXT "v=DKIM1; k=rsa; p=MIGfMA0..."

3. Enregistrement DMARC:
_dmarc.example.com. IN TXT "v=DMARC1; p=reject; rua=mailto:[email protected]"

Résultat:
- SPF vérifie le serveur d'envoi ✓
- DKIM vérifie le contenu de l'e-mail ✓
- DMARC unifie la politique ✓
→ Triple protection!

Références

RFCs liés au SPF:

  • [RFC 7208] SPF ← Ce document
  • [RFC 7489] DMARC
  • [RFC 6376] DKIM

Ressources connexes:


Résumé: SPF est la première ligne de défense pour la sécurité des e-mails. Par des enregistrements DNS, les serveurs d'envoi sont autorisés et l'usurpation d'e-mail est efficacement prévenue. Combiné avec DKIM et DMARC, un système complet de sécurité e-mail est créé. Rappelez-vous: Commencez avec l'échec doux et renforcez progressivement vers le mode strict, et respectez la limite de 10 recherches DNS!