Notes on email Sender Policy Framework (SPF)

bike9876@エボ猫.コム

When an email server sends an email, it tells the receiving server its hostname (its HELO Identity [1],Sec 1.1.4). It also states the email address of the email sender (the MAIL FROM value [1],Sec 1.1.3).

Eg if server mail.example.com was sending an email from foo@example.com, then HELO=mail.example.com and MAIL FROM=foo@example.com.

(If the server is bouncing back a message, MAIL FROM will be null.)

SPF provides a way for the HELO and MAIL FROM values to be validated by the receiving server.

[1] recommends that the HELO identity be checked and states that MAIL FROM must be checked if the HELO value either was not checked or the check did not reach a definitive conclusion.

To validate the HELO identity, <domain> is set to the HELO identity (would be mail.example.com in the above example). Any DNS TXT records for <domain> of the form

v=spf1 <directive> <directive> ...

are fetched. TODO >1 or 0? (It is an error if more than 1 such record exists.) Each <directive> has the form <prefix><mech>, with <prefix> an optional prefix (+,-,~,? - see later) and <mech> a “mechanism” (all, ip4, ip6, a etc - see later).

The ip address of the incoming connection is then matched against the SPF mechanisms, left to right, listed in the record. What counts as a match is determined by the individual mechanism, see below.

If there is a match, then the action determined by <prefix> is the result of the SPF test. For <prefix>=+, it is a pass - the message should be accepted. “-”: it is a fail - the message should be rejected. “~”: it is a “soft fail” - the message should be accepted but marked as suspicious (eg may be saved in a spam folder). “?”: “neutral” - the message should be accepted (use of this prefix is not recommended). If no prefix is specified, “+” is assumed.

If no <mech> matches, then the result is “neutral”.

Validating the MAIL FROM address is the same as for HELO, except now <domain> is set to the domain part of MAIL FROM, eg <domain>=example.com for MAIL FROM=foo@example.com.

If MAIL FROM is null, then it should be taken as postmaster@<HELO>.

Some Mechanisms

See [1], [2] for more mechanisms/details, also no mention is made here of errors.

“all” mechanism:

The special mechanism “all” always matches. It would normally be the final mechanism. It is recommended to use “-all” (or “~all” when testing SPF). With “-all” if no previous mechanism matched, then the message is marked as a fail.

“ip4/”ip6” mechanisms:

eg for an SPF record for <domain>:

v=spf1 ip4:1.2.3.4 ip6:2a00:1234:5678:9abc::1 -all

The ip address of the incoming connection is checked against the ip addresses listed. In this example, if the ip address matches one of these, the SPF test returns “pass”. Else the SPF returns “fail” (because of the “-all”).

Ip addresses can refer to networks in CIDR notation, eg 1.2.3.0/24 or 2a00:1234:5678:9abc::/64.

“a” mechanism:

eg

v=spf1 a -all

The ip address of the incoming connection is checked against the ip addresses for <domain> (ie as obtained from its DNS A records, or AAAA records if the connection is over ipv6). In this example, SPF will return a pass if there is a match, else fail.

eg

v=spf1 a:mail.example.com -all

As the previous example, but the ip address of the incoming connection is checked against the ip addresses for mail.example.com (not <domain>).

“mx” mechanism:

eg

v=spf1 mx -all

The MX DNS record for <domain> is looked up to find the hostnames of mx servers for that domain. The A (or AAAA) records of those hostnames are looked up to give ip addresses. The ip address of the incoming connection is checked against those ip addresses.

Just as for the “a” mechanism, an explicit domain can be provided, eg mx:example.com .

“include” mechanism:

eg

v=spf1 include:spf.example.com -include:bad.example.com -all

The SPF record is obtained for spf.example.com and the matching mechanism described above is done using that SPF record. The include mechanism is then judged to have matched if and only if the included SPF mechanisms returned a “pass” [1],Sec 5.2.

eg if spf.example.com gave an SPF record:

v=spf1 ip4:1.2.3.4 -all

Then if the incoming IP address equalled 1.2.3.4, the included SPF record would generate a pass, and so the “include” mechanism would be deemed to have matched. Therefore, since “include:spf.example.com” is equivalent to “+include:spf.example.com”, the overall SPF record would return a pass.

If the incoming IP address was any other value, then the included SPF record would generate a fail (due to its “-all”). Therefore the “include” mechanism would be deemed not to have matched. Therefore the matching process would move onto the next directive “-include:bad.example.com”.

Since a “neutral” result from an included record is also treated as not-a-match, it would be ok to have

v=spf1 ip4:1.2.3.4

as the SPF record for spf.example.com (ie miss out the final -all). Some online SPF checkers complain about this though. So I generally have “-all” at the end of included SPF records. (But “~all” and “?all” would also have the same effect.)

Note the second include directive “-include:bad.example.com”. Let bad.example.com’s SPF record be eg

v=spf1 ip4:4.3.2.1 -all

If the incoming ip address was 4.3.2.1, the included SPF would generate a pass. So the “include” mechanism would be deemed to have matched. Therefore “-include:bad.example.com” would generate a fail (and so the overall SPF record would return a fail).

Working example

Single mail server sending and receiving

Suppose mail.example.com is the mail server (sending and receiving) for the example.com domain (ie this is its HELO identity). Note it is recommended that a reverse lookup of the server’s ip address give mail.example.com (this is particularly relevant if the server is known by more than 1 name).

There should be an SPF record for mail.example.com, eg:

mail.example.com. TXT "v=spf1 a -all"

(to enable the server’s HELO identity to be verified).

and one for the email domain, eg

example.com. TXT "v=spf1 mx -all"

(this works because the MX server (ie the receiving mail server) for this domain is also the sending mail server).

More than 1 service handling email sending and receiving

Suppose the receiving mail server for example.com is no longer mail.example.com (it may be an encrypted mail service like tuta mail). Also, some mail clients (eg the human ones) use the encrypted mail service to send email, others (eg the non-human ones) use mail.example.com as a smarthost to send their email.

There should still be an SPF record for mail.example.com, eg as before:

mail.example.com. TXT "v=spf1 a -all"

Now the email domain record would be eg:

example.com. TXT "v=spf1 a:mail.example.com include:spf.encryptedmail.com -all"

assuming the encrypted mail service provides an SPF DNS record (spf.encryptedmail.com) that can be included, listing their sending mail servers. Since the MX (receiving) server is no longer mail.example.com, the SPF record has to explicitly give mail.example.com as an allowed sending email server.

Testing

https://www.kitterman.com/spf/validate.html is useful for testing both SPF records already published and not yet published in DNS, and for trying example incoming ip addresses.

Refs

[1]
“RFC 7208: Sender policy framework (SPF) for authorizing use of domains in email, version 1.” [Online]. Available: https://www.rfc-editor.org/info/rfc7208/
[2]
“Sender policy framework: SPF record syntax.” [Online]. Available: http://www.open-spf.org/SPF_Record_Syntax/