Passa al contenuto principale

RFC 7208 - Sender Policy Framework (SPF)

  • Stato: Proposed Standard
  • Pubblicato: April 2014
  • Stream: IETF
  • Sostituisce: RFC4408
  • Errata: Nessun errata

Informazioni di base

  • Numero RFC: 7208
  • Titolo: Sender Policy Framework (SPF) for Authorizing Use of Domains in Email
  • Titolo italiano: Framework delle Politiche del Mittente
  • Data di pubblicazione: Aprile 2014
  • Stato: PROPOSED STANDARD (Standard proposto)
  • Autore: S. Kitterman

Sommario (Abstract)

SPF consente ai proprietari di domini di specificare tramite record DNS quali server di posta sono autorizzati a inviare e-mail per quel dominio. I destinatari possono interrogare i record SPF per verificare se le e-mail provengono da server autorizzati, aiutando a rilevare e bloccare lo spoofing delle e-mail.

Contents

Appendices (Appendici)

Panoramica di SPF

Cos'è SPF?

Definizione:

SPF = Sender Policy Framework (Framework delle Politiche del Mittente)
Funzione: Verifica dei server di posta autorizzati
Metodo: Record DNS TXT
Scopo:
✓ Prevenire lo spoofing delle e-mail
✓ Ridurre lo spam
✓ Migliorare la consegnabilità delle e-mail

Trio di sicurezza e-mail:
1. SPF (questo RFC) - Verifica il server di invio
2. DKIM (RFC 6376) - Verifica il contenuto dell'e-mail
3. DMARC (RFC 7489) - Politica e rapporti unificati

Principio di funzionamento:

Mittente (example.com):
1. Pubblica record SPF nel DNS
example.com. IN TXT "v=spf1 ip4:203.0.113.1 -all"
→ Solo 203.0.113.1 è autorizzato

2. Il server di posta invia normalmente le e-mail
MAIL FROM: `<[email protected]>`

Destinatario:
1. Estrae il dominio del mittente
MAIL FROM: [email protected] → Dominio: example.com

2. Interroga il record SPF
Query DNS: record TXT example.com

3. Verifica l'IP del server di invio
IP del server di invio: 203.0.113.1
Record SPF consente: ip4:203.0.113.1
→ Corrispondenza!

4. Risultato SPF:
Pass ✓ → Server autorizzato
Fail ✗ → Server non autorizzato

SPF vs DKIM vs DMARC

Confronto delle funzionalità:

SPF (RFC 7208):
- Verifica: IP del server di invio
- Posizione: SMTP MAIL FROM
- Record DNS: TXT
- Limitazione: Le e-mail inoltrate falliscono

DKIM (RFC 6376):
- Verifica: Firma digitale dell'e-mail
- Posizione: Header DKIM-Signature
- Record DNS: TXT (_domainkey)
- Limitazione: Richiede configurazione corretta delle chiavi

DMARC (RFC 7489):
- Verifica: Allineamento SPF + DKIM
- Posizione: Header From
- Record DNS: TXT (_dmarc)
- Funzione: Politica + Rapporti

Uso combinato:
SPF + DKIM → DMARC superato → Protezione ottimale

Formato del record SPF

Sintassi di base

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

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

Componenti:
- v=spf1: Identificatore di versione (richiesto, sempre spf1)
- mechanisms: Meccanismi di corrispondenza
- qualifiers: Qualificatori di risultato
- modifiers: Modificatori

Meccanismi (Mechanisms)

1. all:

Definizione: Corrisponde a tutti gli IP
Utilizzo: Normalmente come politica predefinita alla fine

Esempi:
v=spf1 -all Tutti gli IP non consentiti (più rigoroso)
v=spf1 ~all Tutti gli IP softfail (raccomandato)
v=spf1 +all Tutti gli IP consentiti (non raccomandato!)

2. ip4/ip6:

Definizione: Indirizzi IP o intervalli espliciti

Esempi:
v=spf1 ip4:203.0.113.1 -all
→ Solo 203.0.113.1 è consentito

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

v=spf1 ip6:2001:db8::1 -all
→ Consente indirizzo IPv6

v=spf1 ip4:203.0.113.0/24 ip4:198.51.100.0/24 -all
→ Consente intervalli multipli

3. a:

Definizione: Record A/AAAA del dominio corrente

Esempi:
v=spf1 a -all
→ Consente IP dal record A di example.com

v=spf1 a:mail.example.com -all
→ Consente IP dal record A di mail.example.com

v=spf1 a/24 -all
→ Consente la rete /24 dell'IP del record A di example.com

4. mx:

Definizione: Record MX del dominio corrente

Esempi:
v=spf1 mx -all
→ Consente IP dei server MX di example.com

v=spf1 mx:example.com -all
→ Consente IP dei server MX di example.com

v=spf1 mx/24 -all
→ Consente la rete /24 dei server MX

5. include:

Definizione: Include il record SPF di un altro dominio

Esempi:
v=spf1 include:_spf.google.com -all
→ Consente server di posta Google (Gmail for Business)

v=spf1 include:spf.protection.outlook.com -all
→ Consente server di posta Microsoft 365

Include multipli:
v=spf1 include:_spf.google.com include:spf.protection.outlook.com -all

Attenzione: Limite massimo di 10 ricerche DNS!

6. exists:

Definizione: Corrisponde se il dominio specificato ha un record A

Esempio:
v=spf1 exists:%\{i}.spamhaus.example.com -all
→ Uso avanzato, normalmente per controlli di blacklist anti-spam

Espansione macro:
%\{i} = IP del server di invio (invertito)

7. ptr (non raccomandato):

Definizione: Query DNS inversa

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

Problemi:
❌ Prestazioni scadenti (richiede query DNS inversa)
❌ Bassa affidabilità
❌ RFC sconsiglia esplicitamente l'uso

Alternativa: Usare ip4/ip6 o include

Qualificatori (Qualifiers)

Simbolo | Nome | Significato | Uso raccomandato
--------|------|-------------|------------------
+ | Pass | Superato (predefinito) | Server autorizzati
- | Fail | Fallito | Rifiuta e-mail
~ | SoftFail | Fallimento morbido | Accetta ma marca
? | Neutral | Neutrale | Nessuna politica chiara

Esempi:
v=spf1 +ip4:203.0.113.1 -all
↑pass esplicito ↑fail esplicito

v=spf1 ip4:203.0.113.1 ~all
↑+ predefinito ↑softfail

v=spf1 ?all
↑neutrale (equivalente a nessun SPF)

Raccomandazioni per l'uso dei qualificatori:

+ (Pass): 
✓ Server di posta autorizzati
Esempio: +ip4:203.0.113.1

- (Fail):
✓ -all finale (rigoroso)
✓ Vietare esplicitamente certi IP
Esempio: -all

~ (SoftFail):
✓ ~all finale (permissivo, raccomandato inizialmente)
✓ Usare durante il periodo di transizione
Esempio: ~all

? (Neutral):
✗ Usato raramente
✗ Equivalente a nessuna politica

Modificatori (Modifiers)

1. redirect:

Definizione: Reindirizzamento al record SPF di un altro dominio

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

Scopo:
✓ Gestione centralizzata di SPF
✓ Più domini condividono una politica

Attenzione:
- Nessun altro meccanismo può seguire redirect
- Non può essere usato insieme ad all

2. exp:

Definizione: Spiegazione (testo di spiegazione in caso di fallimento SPF)

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

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

Scopo:
✓ Fornire messaggi di errore user-friendly
- Raramente usato nella pratica

Esempi di record SPF

Configurazioni di base

1. Un solo server di posta:

v=spf1 ip4:203.0.113.1 -all

Spiegazione:
- Solo 203.0.113.1 può inviare e-mail
- Altri IP vengono rifiutati

2. Uso dei record MX:

v=spf1 mx -all

Spiegazione:
- Consente ai server MX del dominio di inviare e-mail
- Si adatta automaticamente ai cambiamenti MX

3. Intervalli IP multipli:

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

Spiegazione:
- Consente due reti di classe C
- Adatto per distribuzione multi-datacenter

Servizi di terze parti

4. Google Workspace (Gmail for Business):

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

Spiegazione:
- Usa Google per inviare e-mail
- Include il record SPF di 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

Configurazioni miste

8. Server proprio + Terze parti:

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

Spiegazione:
- Server proprio: 203.0.113.1
- Google Workspace: include

9. Servizi di terze parti multipli:

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

Attenzione: Ogni include conta come una ricerca DNS

10. Dominio che non invia e-mail:

v=spf1 -all

Spiegazione:
- Il dominio non invia e-mail
- Previene lo spoofing
- Adatto per domini solo in ricezione

Sottodomini

11. Configurazione separata dei sottodomini:

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

Spiegazione:
- Il dominio principale usa il proprio server
- Il sottodominio mail usa Google

12. Ereditarietà del sottodominio (senza record SPF):

Se mail.example.com non ha un record SPF:
→ Usa il record SPF di example.com

Se non si desidera l'ereditarietà:
mail.example.com: v=spf1 -all

Processo di verifica SPF

Passaggi di verifica del destinatario

// Pseudo-codice di verifica SPF
async function checkSPF(clientIP, sender, helo) {
// 1. Estrarre il dominio
const domain = sender.split('@')[1]; // [email protected] → example.com

// 2. Interrogare il record SPF
const spfRecord = await queryDNS(domain, 'TXT', 'v=spf1');

if (!spfRecord) {
return 'none'; // Nessun record SPF
}

// 3. Analizzare il record SPF
const mechanisms = parseSPF(spfRecord);

// 4. Verificare i meccanismi uno per uno
for (const mechanism of mechanisms) {
const result = await evaluateMechanism(mechanism, clientIP, domain);

if (result !== null) {
return result; // Corrispondenza trovata, restituire il risultato
}
}

return 'neutral'; // Nessuna corrispondenza
}

// Valutare un singolo meccanismo
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; // Nessuna corrispondenza
}

Risultati SPF

Valore di ritorno | Significato          | Trattamento raccomandato
------------------|----------------------|-------------------------
none | Nessun record SPF | Accettare (fiducia ridotta)
neutral | Politica esplicita | Accettare
| nessuna |
pass | Superato | Accettare
fail | Fallito | Rifiutare
softfail | Fallimento morbido | Accettare ma marcare
temperror | Errore temporaneo | Riprovare più tardi
permerror | Errore permanente | Rifiutare

Esempi di risposte SMTP:
pass: 250 OK (SPF pass)
fail: 550 SPF check failed
softfail: 250 OK (aggiungere header X-SPF: softfail)

Limiti di ricerca DNS

Limite di 10 ricerche

Problema:

La verifica SPF esegue al massimo 10 ricerche DNS
Superamento del limite → permerror (Errore permanente)

Conta per le 10 ricerche:
✓ include
✓ a
✓ mx
✓ exists
✓ redirect

Non conta:
✗ ip4/ip6 (corrispondenza diretta)
✗ all (corrispondenza diretta)

Esempio - Limite superato:

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 ← Superato! permerror
-all

Se un include contiene altri include, anche questi contano!

Soluzioni:

1. Usare ip4/ip6 invece di a/mx
❌ v=spf1 a mx -all (2 ricerche)
✓ v=spf1 ip4:203.0.113.1 ip4:198.51.100.1 -all (0 ricerche)

2. Unire gli include
❌ include:service1.com include:service2.com
✓ Mantenere il proprio record SPF contenente tutti gli IP

3. SPF Flattening (Appiattimento SPF)
Interrogare regolarmente gli IP degli include, convertire in ip4/ip6

Strumento SPF Flattening

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

// Analizzare 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') {
// Interrogare ricorsivamente gli 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}`));
}
}
}

// Generare SPF appiattito
return `v=spf1 ${ips.join(' ')} -all`;
}

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

Strumenti pratici

Generatore di record 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;
}
}

// Esempio di utilizzo
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());

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

Strumento di verifica SPF

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

class SPFChecker {
async check(domain, ip) {
try {
// Interrogare il record 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);

// Analizzare e verificare
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;

// Estrarre il qualificatore
let qualifier = '+';
let mechanism = part;

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

// Verificare il meccanismo
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);
}
// Altri meccanismi possono essere aggiunti qui...
}

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

isIPInRange(ip, range) {
// Versione semplificata, l'ambiente di produzione richiede un'implementazione completa
if (!range.includes('/')) {
return ip === range;
}
// Implementazione di corrispondenza CIDR omessa...
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['+'];
}
}

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

Migliori pratiche per la distribuzione

1. Distribuzione graduale

Fase 1: Modalità monitoraggio
v=spf1 ?all
o
v=spf1 ~all

Scopo: Raccogliere dati, osservare quali server inviano e-mail
Durata: 2-4 settimane

Fase 2: Fallimento morbido
v=spf1 ip4:x.x.x.x include:provider.com ~all

Scopo: Marcare ma non rifiutare e-mail non autorizzate
Durata: 4-8 settimane

Fase 3: Modalità rigorosa
v=spf1 ip4:x.x.x.x include:provider.com -all

Scopo: Rifiutare e-mail non autorizzate

2. Errori comuni

❌ Errore 1: Dimenticare -all
v=spf1 ip4:203.0.113.1
→ Equivalente a v=spf1 ip4:203.0.113.1 ?all
→ Qualsiasi IP è neutrale

✓ Corretto:
v=spf1 ip4:203.0.113.1 -all

❌ Errore 2: Record SPF multipli
example.com TXT "v=spf1 ip4:203.0.113.1 -all"
example.com TXT "v=spf1 include:provider.com -all"
→ permerror

✓ Corretto: Unire in uno solo
v=spf1 ip4:203.0.113.1 include:provider.com -all

❌ Errore 3: Più di 10 ricerche
v=spf1 include:a include:b include:c ... (troppi)

✓ Corretto: Usare ip4 direttamente o flattening

❌ Errore 4: Uso di ptr
v=spf1 ptr:example.com -all
→ Prestazioni scadenti, non raccomandato

✓ Corretto: Usare ip4 o include

3. Test e verifica

# Strumenti di test da riga di comando

# 1. Interrogare il record SPF
dig example.com TXT | grep "v=spf1"
o
nslookup -type=TXT example.com

# 2. Usare strumenti online
# - https://mxtoolbox.com/spf.aspx
# - https://www.kitterman.com/spf/validate.html

# 3. Inviare un'e-mail di test
# Inviare alla propria casella di posta, verificare l'header:
# Received-SPF: pass ...

Integrazione con DKIM/DMARC

Configurazione completa della sicurezza e-mail

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

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

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

Risultato:
- SPF verifica il server di invio ✓
- DKIM verifica il contenuto dell'e-mail ✓
- DMARC unifica la politica ✓
→ Tripla protezione!

Riferimenti

RFC relativi a SPF:

  • [RFC 7208] SPF ← Questo documento
  • [RFC 7489] DMARC
  • [RFC 6376] DKIM

Risorse correlate:


Sommario: SPF è la prima linea di difesa per la sicurezza delle e-mail. Tramite record DNS, i server di invio sono autorizzati e lo spoofing delle e-mail è efficacemente prevenuto. Combinato con DKIM e DMARC, si crea un sistema completo di sicurezza e-mail. Ricorda: Inizia con il fallimento morbido e rafforza gradualmente verso la modalità rigorosa, e rispetta il limite di 10 ricerche DNS!