dnscontrol/docs/spf.md
Tom Limoncelli d08d57d403 Revamp the website front page (#213)
* First iteration. Just the front page.
2017-09-29 15:44:13 -04:00

5.3 KiB

layout
default

The DNS Control SPF Optimizer

SPF records are hints to email systems that help them determine if an incoming email message might be spam. The SPF records are placed in DNS TXT records like so:

$ dig +short google.com txt
"v=spf1 include:_spf.google.com ~all"

SPF records are intentionally limited to 10 verbs that would cause DNS lookups. In the above example the include:_spf.google.com would cause a DNS lookup. The reason for the "10 lookup limit" is to make it difficult to leverage the SPF system to create a DDOS attack on a DNS server.

At StackOverflow, we use many SaaS services and we reached the "10 lookup limit" years ago. We would like to unroll or "inline" the includes but it would become a maintenance nightmare. What if we unrolled the SPF include required for Google GSuite and then Google changed the contents of the SPF records?

We figured that DNSControl could do a better job.

Better comments

Here's how we define our SPF record:

var SPF_SO_LIST = [
    'v=spf1',
    'ip4:198.252.206.0/24',           // ny-mail*
    'ip4:192.111.0.0/24',             // co-mail*
    'include:_spf.google.com',        // GSuite
    'include:mailgun.org',            // Greenhouse.io
    'include:spf-basic.fogcreek.com', // Fogbugz
    'include:mail.zendesk.com',       // Zenddesk
    'include:servers.mcsv.net',       // MailChimp (Ticket#12345)
    'include:sendgrid.net',           // SendGrid
    'include:spf.mtasv.net',          // Desk.com
    '~all'
].join(" ");

D('example.com', ...
  TXT("@", SPF_SO_LIST),
  ...
)

The first thing you'll notice is that by defining it this way each component can include a comment explaining what it is for. This is important because, and we're not kidding here, for a long time we didn't know what include:spf.mtasv.net was for and we were afraid to remove it. Finally someone remembered that it was for Desk.com and we breathed a sigh of relief. You'll also notice that the Mailchimp entry includes the ticket number of the request to add it. Now we can refer to that ticket to better understand the history.

In summary, listing your SPF record like this makes it easier to maintain a complex SPF record. Certainly you agree that this is better than var SPF_SO_LIST = 'v=spf1 'ip4:198.252.206.0/24 'ip4:192.111.0.0/24 'include:_spf.google.com 'include:mailgun.org 'include:spf-basic.fogcreek.com 'include:mail.zendesk.com 'include:servers.mcsv.net 'include:sendgrid.net include:spf.mtasv.net ~all'

However, we can do better.

Better macros

Because we don't want to have to remember the "@", and because we use the same SPF record for multiple domains (any domain that is attached to our GSuite account), we define a macro called SPF for use with many domains:

var SPF = [ TXT("@", SPF_SO_LIST) ]
D('example.com', ...
  SPF,
  ...
)
D('otherexample.com', ...
  SPF,
  ...
)

This is a lot less typing. It is also less error-prone: you don't have to remember the '@'.

However, we can do better.

SPF optimizer

As mentioned before, SPF records are intentionally limited to 10 verbs that would cause DNS lookups. This count includes recursive includes. For example, if you use an include: that includes 5 other domains, that's 6 lookups. That leaves you to only 4 more lookups.

We figured that DNSControl could do better. It could analyze an SPF record and flatten it to reduce the number of lookups.

However, we're very paranoid. If we break email, a lot of people notice. Therefore our "flattening" system has some safety rules:

  • The system is "opt in". You must specify exactly which includes will be flattened. We recommend you only flatten the minimum needed.
  • The flattening works off a cached copy of the DNS lookups. We are concerned that if someone else's DNS server is down, the optimizer will break and you won't be able to dnscontrol push, which would be very bad especially in an emergency. Therefore. the process runs off a file called FILLIN but will warn you if the file needs updating. The updates are easy to do (DNSControl generates the new file for you to use).

So what does it look like?

Add metadata to the TXT records:

  • flatten: "foo,bar" (flatten include:foo and include:bar)
  • split: "_spf%d" (if additional DNS records must be generated, make the labels _spf1, _spf2, _spf3, and so on.)

Here's an example:

var SPF = [
    TXT("@", SPF_SO_LIST, {flatten: "spf-basic.fogcreek.com,spf.mtasv.net", split: "_spf%d"}),
    TXT("_rawspf", SPF_SO_LIST) // keep unmodified availible for other tools
]
D('example.com', ...
  SPF,
  ...
)

As a result:

  • TXT record on example.com will be optimized.
  • TXT record on _rawspf.example.com is the unoptimized version, used purely for demonstration purposes.

You'll notice that we only flatten 2 of all the includes. These are sufficient to get us to only 10 lookups. They're also the 2 domains that SPF records are the least important. Thus, if their SPF records change and we don't notice, we won't be too greatly affected.

Operational Guide

FILL IN THE SEQUENCE OF COMMANDS TO MAINTAIN THE CACHE.

Interactive mode

FILL IN INFO ABOUT THE INTERACTIVE MDOE.

Future

We'd like to add other optimizations such as:

  • De-dup
  • Remove overlapping CIDR blocks