Productionize the SPF optimizer (#279)

* Productionize the SPF optimizer
* SPF_BUILDER(): New helper function
* docs/spf-optimizer.md: Document SPF_BUILDER()
This commit is contained in:
Tom Limoncelli 2017-12-06 15:50:21 -05:00 committed by GitHub
parent 95fb79dfcc
commit c6e244d8da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 353 additions and 70 deletions

View file

@ -111,7 +111,7 @@ title: DnsControl
<a href="{{site.github.url}}/alias">Aliases</a>: ALIAS/ANAME records
</li>
<li>
<a href="{{site.github.url}}/spf">SPF Optimizer</a>: Optimize your SPF records
<a href="{{site.github.url}}/spf-optimizer">SPF Optimizer</a>: Optimize your SPF records
</li>
</ul>
</div>

236
docs/spf-optimizer.md Normal file
View file

@ -0,0 +1,236 @@
---
layout: default
title: SPF Optimizer
---
# SPF Optimizer
dnscontrol can optimize the SPF settings on a domain by flattening
(inlining) includes and removing duplicates. dnscontrol also makes
it easier to document your SPF configuration.
**Warning:** Flattening SPF includes is risky. Only flatten an SPF
setting if it is absolutely needed to bring the number of "lookups"
to be less than 10. In fact, it is debatable whether or not ISPs
enforce the "10 lookup rule".
## The old way
Here is an example of how SPF settings are normally done:
```
D("example.tld", REG, DNS, ...
TXT("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:450622.spf05.hubspotemail.net ~all")
)
```
This has a few problems:
* No comments. It is difficult to add a comment. In particular, we want to be able to list which ticket requested each item in the SPF setting so that history is retained.
* Ugly diffs. If you add an element to the SPF setting, the diff will show the entire line changed, which is difficult to read.
* Too many lookups. The SPF RFC says that SPF settings should not require more than 10 DNS lookups. If we manually flatten (i.e. "inline") an include, we have to remember to check back to see if the settings have changed. Humans are not good at that kind of thing.
## The dnscontrol way
```
D("example.tld", REG, DSP, ...
A("@", "10.2.2.2"),
MX("@", "example.tld."),
SPF_BUILDER({
label: "@",
overflow: "_spf%d",
raw: "_rawspf",
parts: [
"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
"include:sendgrid.net", // SendGrid
"include:450622.spf05.hubspotemail.net", // Hubspot (Ticket# SREREQ-107)
"~all"
],
flatten: [
"spf-basic.fogcreek.com", // Rational: Being deprecated. Low risk if it breaks.
"450622.spf05.hubspotemail.net" // Rational: Unlikely to change without warning.
]
}),
);
```
By using the `SPF_BUILDER` we gain many benefits:
* Comments can appear next to the element they refer to.
* Diffs will be shorter and more specific; therefore easier to read.
* Automatic flattening. We can specify which includes should be flattened and dnscontrol will do the work. It will even warn us if the includes change.
## Syntax
When you want to specify SPF settings for a domain, use the
`SPF_BUILD()` function.
```
D("example.tld", REG, DSP, ...
...
...
...
SPF_BUILDER({
label: "@",
overflow: "_spf%d", // Delete this line if you don't want big strings split.
raw: "_rawspf", // Delete this line if the default is sufficient.
parts: [
"v=spf1",
// fill in your SPF items here
"~all"
],
flatten: [
// fill in any domains to inline.
]
}),
...
...
);
```
The parameters are:
* `label:` The label of the first TXT record. (Optional. Default: `"@"`)
* `overflow:` If set, SPF strings longer than 255 chars will be split into multiple TXT records. The value of this setting determines the template for what the additional labels will be named. If not set, no splitting will occur and dnscontrol may generate TXT strings that are too long.
* `raw:` The label of the unaltered SPF settings. (Optional. Default: `"_rawspf"`)
* `parts:` The individual parts of the SPF settings.
* `flatten:` Which includes should be inlined. For safety purposes the flattening is done on an opt-in basis. If `"*"` is listed, all includes will be flattened... this might create more problems than is solves due to length limitations.
`SPR_BUILDER()` returns multiple `TXT()` records:
* `TXT("@", "v=spf1 .... ~all")`
* This is the optimized configuration.
* `TXT("_spf1", "...")`
* If the optimizer needs to split a long string across multiple TXT records, the additional TXT records will have labels `_spf1`, `_spf2`, `_spf3`, etc.
* `TXT("_rawspf", "v=spf1 .... ~all")`
* This is the unaltered SPF configuration. This is purely for debugging purposes and is not used by any email or anti-spam system. It is only generated if flattening is requested.
We recommend first using this without any flattening. Make sure
`dnscontrol preview` works as expected. Once that is done, add the
flattening required to reduce the number of lookups to 10 or less.
To count the number of lookups, you can use our interactive SPF
debugger at [https://stackexchange.github.io/dnscontrol/flattener/index.html](https://stackexchange.github.io/dnscontrol/flattener/index.html)
## Notes about the DNS Cache
dnscontrol keeps a cache of the DNS lookups performed during
optimization. The cache is maintained so that the optimizer does
not produce different results depending on the ups and downs of
other people's DNS servers. This makes it possible to do `dnscontrol
push` even if your or third-party DNS servers are down.
The DNS cache is kept in a file called `spfcache.json`. If it needs
to be updated, the proper data will be written to a file called
`spfcache.updated.json` and instructions such as the ones below
will be output telling you exactly what to do:
```
$ dnscontrol preview
1 Validation errors:
WARNING: 2 spf record lookups are out of date with cache (_spf.google.com,_netblocks3.google.com).
Wrote changes to spfcache.updated.json. Please rename and commit:
$ mv spfcache.updated.json spfcache.json
$ git commit spfcache.json
```
In this case, you are being asked to replace `spfcache.json` with
the newly generated data in `spfcache.updated.json`.
Needing to do this kind of update is considered a validation error
and will block `dnscontrol push` from running.
Note: The instructions are hardcoded strings. The filenames will
not change.
Note: The instructions assume you use git. If you use something
else, please do the appropriate equivalent command.
## Caveats:
1. Dnscontrol 'gives up' if it sees SPF records it can't understand.
This includes: syntax errors, features that our spflib doesn't know
about, overly complex SPF settings, and anything else that we we
didn't feel like implementing.
2. The TXT record that is generated may exceed DNS limits. dnscontrol
will not generate a single TXT record that exceeds DNS limits, but
it ignores the fact that there may be other TXT records on the same
label. For example, suppose it generates a TXT record on the bare
domain (stackoverflow.com) that is 250 bytes long. That's fine and
doesn't require a continuation record. However if there is another
TXT record (not an SPF record, perhaps a TXT record used to verify
domain ownership), the total packet size of all the TXT records
could exceed 512 bytes, and will require EDNS or a TCP request.
3. Dnscontrol does not warn if the number of lookups exceeds 10.
We hope to implement this some day.
## Advanced Technique: Interactive SPF Debugger
dnscontrol includes an experimental system for viewing
SPF settings:
[https://stackexchange.github.io/dnscontrol/flattener/index.html](https://stackexchange.github.io/dnscontrol/flattener/index.html)
You can also run this locally (it is self-contained) by opening
`dnscontrol/docs/flattener/index.html` in your browser.
You can use this to determine the minimal number of domains you
need to flatten to have fewer than 10 lookups.
The output is as follows:
1. The top part lists the domain as it current is configured, how
many lookups it requires, and includes a checkbox for each item
that could be flattened.
2. Fully flattened: This section shows the SPF configuration if you
fully flatten it. i.e. This is what it would look like if all the
checkboxes were checked. Note that this result is likely to be
longer than 255 bytes, the limit for a single TXT string.
3. Fully flattened split: This takes the "fully flattened" result
and splits it into multiple DNS records. To continue to the next
record an include is added.
## Advanced Technique: Define once, use many
In some situations we define an SPF setting once and want to re-use
it on many domains. Here's how to do this:
```
var SPF_MYSETTINGS = SPF_BUILDER({
label: "@",
overflow: "_spf%d",
raw: "_rawspf",
parts: [
"v=spf1",
...
"~all"
],
flatten: [
...
]
});
D("example.tld", REG, DSP, ...
SPF_MYSETTINGS
);
D("example2.tld", REG, DSP, ...
SPF_MYSETTINGS
);
```

View file

@ -538,3 +538,43 @@ var CF_TEMP_REDIRECT = recordBuilder('CF_TEMP_REDIRECT', {
var URL = recordBuilder('URL')
var URL301 = recordBuilder('URL301')
var FRAME = recordBuilder('FRAME')
// SPF_BUILDER takes an object:
// parts: The parts of the SPF record (to be joined with ' ').
// label: The DNS label for the primary SPF record. (default: '@')
// raw: If defined, also
// split: The template for additional records to be created (default: '_spf%d')
// flatten: A list of domains to be flattened.
function SPF_BUILDER(value) {
if (!value.parts || value.parts.length < 2) {
throw "SPF_BUILDER requires at least 2 elements";
}
if (!value.label) {
value.label = "@";
}
if (!value.raw) {
value.raw = "_rawspf";
}
r = [] // The list of records to return.
p = {} // The metaparameters to set on the main TXT record.
rawspf = value.parts.join(" "); // The unaltered SPF settings.
// If flattening is requested, generate a TXT record with the raw SPF settings.
if (value.flatten && value.flatten.length > 0) {
p.flatten = value.flatten.join(",");
r.push(TXT(value.raw, rawspf))
}
// If overflow is specified, enable splitting.
if (value.overflow) {
p.split = value.overflow
}
// Generate a TXT record with the metaparameters.
r.push(TXT(value.label, rawspf, p))
return r
}

View file

@ -192,76 +192,83 @@ var _escData = map[string]*_escFile{
"/helpers.js": {
local: "pkg/js/helpers.js",
size: 14948,
size: 16101,
modtime: 0,
compressed: `
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
NdDlyNv9KIeDsPV/AQAA//+AFZySZDoAAA==
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
`,
},

View file

@ -71,7 +71,7 @@ func flattenSPFs(cfg *models.DNSConfig) []error {
if err := cache.Save("spfcache.updated.json"); err != nil {
errs = append(errs, err)
} else {
errs = append(errs, Warning{fmt.Errorf("%d spf record lookups are out of date with cache (%s). Wrote changes to spfcache.updated.json. Please rename and commit:\nmv spfcache.updated.json spfcache.json", len(changed), strings.Join(changed, ","))})
errs = append(errs, Warning{fmt.Errorf("%d spf record lookups are out of date with cache (%s).\nWrote changes to spfcache.updated.json. Please rename and commit:\n $ mv spfcache.updated.json spfcache.json\n $ git commit spfcache.json\n", len(changed), strings.Join(changed, ","))})
}
}
}