Add enforcement for CAA SERVFAIL (#1971)

https://github.com/letsencrypt/boulder/pull/1971
This commit is contained in:
Jacob Hoffman-Andrews 2016-06-28 11:00:23 -07:00 committed by Roland Bracewell Shoemaker
parent 6007df8f3c
commit 0c0e94dfaf
8 changed files with 121 additions and 25 deletions

View File

@ -2,6 +2,7 @@ package bdns
import (
"fmt"
"io/ioutil"
"math/rand"
"net"
"strings"
@ -148,15 +149,18 @@ type DNSResolverImpl struct {
dnsClient exchanger
servers []string
allowRestrictedAddresses bool
maxTries int
LookupIPv6 bool
clk clock.Clock
stats metrics.Scope
txtStats metrics.Scope
aStats metrics.Scope
aaaaStats metrics.Scope
caaStats metrics.Scope
mxStats metrics.Scope
// If non-nil, these are already-issued names whose registrar returns SERVFAIL
// for CAA queries that get a temporary pass during a notification period.
caaSERVFAILExceptions map[string]bool
maxTries int
LookupIPv6 bool
clk clock.Clock
stats metrics.Scope
txtStats metrics.Scope
aStats metrics.Scope
aaaaStats metrics.Scope
caaStats metrics.Scope
mxStats metrics.Scope
}
var _ DNSResolver = &DNSResolverImpl{}
@ -167,7 +171,14 @@ type exchanger interface {
// NewDNSResolverImpl constructs a new DNS resolver object that utilizes the
// provided list of DNS servers for resolution.
func NewDNSResolverImpl(readTimeout time.Duration, servers []string, stats metrics.Scope, clk clock.Clock, maxTries int) *DNSResolverImpl {
func NewDNSResolverImpl(
readTimeout time.Duration,
servers []string,
caaSERVFAILExceptions map[string]bool,
stats metrics.Scope,
clk clock.Clock,
maxTries int,
) *DNSResolverImpl {
// TODO(jmhodges): make constructor use an Option func pattern
dnsClient := new(dns.Client)
@ -179,6 +190,7 @@ func NewDNSResolverImpl(readTimeout time.Duration, servers []string, stats metri
dnsClient: dnsClient,
servers: servers,
allowRestrictedAddresses: false,
caaSERVFAILExceptions: caaSERVFAILExceptions,
maxTries: maxTries,
clk: clk,
stats: stats,
@ -194,7 +206,7 @@ func NewDNSResolverImpl(readTimeout time.Duration, servers []string, stats metri
// provided list of DNS servers for resolution and will allow loopback addresses.
// This constructor should *only* be called from tests (unit or integration).
func NewTestDNSResolverImpl(readTimeout time.Duration, servers []string, stats metrics.Scope, clk clock.Clock, maxTries int) *DNSResolverImpl {
resolver := NewDNSResolverImpl(readTimeout, servers, stats, clk, maxTries)
resolver := NewDNSResolverImpl(readTimeout, servers, nil, stats, clk, maxTries)
resolver.allowRestrictedAddresses = true
return resolver
}
@ -375,8 +387,7 @@ func (dnsResolver *DNSResolverImpl) LookupHost(ctx context.Context, hostname str
}
// LookupCAA sends a DNS query to find all CAA records associated with
// the provided hostname. If the response code from the resolver is
// SERVFAIL an empty slice of CAA records is returned.
// the provided hostname.
func (dnsResolver *DNSResolverImpl) LookupCAA(ctx context.Context, hostname string) ([]*dns.CAA, error) {
dnsType := dns.TypeCAA
r, err := dnsResolver.exchangeOne(ctx, hostname, dnsType, dnsResolver.caaStats)
@ -384,11 +395,20 @@ func (dnsResolver *DNSResolverImpl) LookupCAA(ctx context.Context, hostname stri
return nil, &DNSError{dnsType, hostname, err, -1}
}
// On resolver validation failure, or other server failures, return empty an
// set and no error.
// If the resolver returns SERVFAIL for a certain list of FQDNs, return an
// empty set and no error. We originally granted a pass on SERVFAIL because
// Cloudflare's DNS, which is behind a lot of hostnames, returned that code.
// That is since fixed, but we have a handful of other domains that still return
// SERVFAIL, but will need certificate renewals. After a suitable notice
// period we will remove these exceptions.
var CAAs []*dns.CAA
if r.Rcode == dns.RcodeServerFailure {
return CAAs, nil
if dnsResolver.caaSERVFAILExceptions == nil ||
dnsResolver.caaSERVFAILExceptions[hostname] {
return nil, nil
} else {
return nil, &DNSError{dnsType, hostname, nil, r.Rcode}
}
}
for _, answer := range r.Answer {
@ -422,3 +442,22 @@ func (dnsResolver *DNSResolverImpl) LookupMX(ctx context.Context, hostname strin
return results, nil
}
// ReadHostList reads in a newline-separated file and returns a map containing
// each entry. If the filename is empty, returns a nil map and no error.
func ReadHostList(filename string) (map[string]bool, error) {
if filename == "" {
return nil, nil
}
body, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
var output = make(map[string]bool)
for _, v := range strings.Split(string(body), "\n") {
if len(v) > 0 {
output[v] = true
}
}
return output, nil
}

View File

@ -31,7 +31,7 @@ func mockDNSQuery(w dns.ResponseWriter, r *dns.Msg) {
}
for _, q := range r.Question {
q.Name = strings.ToLower(q.Name)
if q.Name == "servfail.com." {
if q.Name == "servfail.com." || q.Name == "servfailexception.example.com" {
m.Rcode = dns.RcodeServerFailure
break
}
@ -278,6 +278,17 @@ func TestDNSServFail(t *testing.T) {
emptyCaa, err := obj.LookupCAA(context.Background(), bad)
test.Assert(t, len(emptyCaa) == 0, "Query returned non-empty list of CAA records")
test.AssertNotError(t, err, "LookupCAA returned an error")
// When we turn on enforceCAASERVFAIL, such lookups should fail.
obj.caaSERVFAILExceptions = map[string]bool{"servfailexception.example.com": true}
emptyCaa, err = obj.LookupCAA(context.Background(), bad)
test.Assert(t, len(emptyCaa) == 0, "Query returned non-empty list of CAA records")
test.AssertError(t, err, "LookupCAA should have returned an error")
// Unless they are on the exception list
emptyCaa, err = obj.LookupCAA(context.Background(), "servfailexception.example.com")
test.Assert(t, len(emptyCaa) == 0, "Query returned non-empty list of CAA records")
test.AssertNotError(t, err, "LookupCAA for servfail exception returned an error")
}
func TestDNSLookupTXT(t *testing.T) {
@ -652,3 +663,23 @@ type tempError bool
func (t tempError) Temporary() bool { return bool(t) }
func (t tempError) Error() string { return fmt.Sprintf("Temporary: %t", t) }
func TestReadHostList(t *testing.T) {
res, err := ReadHostList("")
if res != nil {
t.Errorf("Expected res to be nil")
}
if err != nil {
t.Errorf("Expected err to be nil: %s", err)
}
res, err = ReadHostList("../test/caa-servfail-exceptions.txt")
if err != nil {
t.Errorf("Expected err to be nil: %s", err)
}
if len(res) != 1 {
t.Errorf("Wrong size of host list: %d", len(res))
}
if res["servfailexception.example.com"] != true {
t.Errorf("Didn't find servfailexception.example.com in list")
}
}

View File

@ -68,8 +68,16 @@ func main() {
if dnsTries < 1 {
dnsTries = 1
}
caaSERVFAILExceptions, err := bdns.ReadHostList(c.VA.CAASERVFAILExceptions)
cmd.FailOnError(err, "Couldn't read CAASERVFAILExceptions file")
if !c.Common.DNSAllowLoopbackAddresses {
rai.DNSResolver = bdns.NewDNSResolverImpl(raDNSTimeout, []string{c.Common.DNSResolver}, scoped, clock.Default(), dnsTries)
rai.DNSResolver = bdns.NewDNSResolverImpl(
raDNSTimeout,
[]string{c.Common.DNSResolver},
caaSERVFAILExceptions,
scoped,
clock.Default(),
dnsTries)
} else {
rai.DNSResolver = bdns.NewTestDNSResolverImpl(raDNSTimeout, []string{c.Common.DNSResolver}, scoped, clock.Default(), dnsTries)
}

View File

@ -66,9 +66,17 @@ func main() {
dnsTries = 1
}
clk := clock.Default()
caaSERVFAILExceptions, err := bdns.ReadHostList(c.VA.CAASERVFAILExceptions)
cmd.FailOnError(err, "Couldn't read CAASERVFAILExceptions file")
var resolver bdns.DNSResolver
if !c.Common.DNSAllowLoopbackAddresses {
r := bdns.NewDNSResolverImpl(dnsTimeout, []string{c.Common.DNSResolver}, scoped, clk, dnsTries)
r := bdns.NewDNSResolverImpl(
dnsTimeout,
[]string{c.Common.DNSResolver},
caaSERVFAILExceptions,
scoped,
clk,
dnsTries)
r.LookupIPv6 = c.VA.LookupIPv6
resolver = r
} else {

View File

@ -207,12 +207,13 @@ func (ccs *caaCheckerServer) ValidForIssuance(ctx context.Context, check *pb.Che
type config struct {
GRPC cmd.GRPCServerConfig
DebugAddr string `yaml:"debug-addr"`
DNSResolver string `yaml:"dns-resolver"`
DNSNetwork string `yaml:"dns-network"`
DNSTimeout cmd.ConfigDuration `yaml:"dns-timeout"`
StatsdServer string `yaml:"statsd-server"`
StatsdPrefix string `yaml:"statsd-prefix"`
DebugAddr string `yaml:"debug-addr"`
DNSResolver string `yaml:"dns-resolver"`
DNSNetwork string `yaml:"dns-network"`
DNSTimeout cmd.ConfigDuration `yaml:"dns-timeout"`
StatsdServer string `yaml:"statsd-server"`
StatsdPrefix string `yaml:"statsd-prefix"`
CAASERVFAILExceptions string `yaml:"caa-servfail-exceptions"`
}
func main() {
@ -231,9 +232,13 @@ func main() {
cmd.FailOnError(err, "Failed to create StatsD client")
scope := metrics.NewStatsdScope(stats, "caa-service")
caaSERVFAILExceptions, err := bdns.ReadHostList(c.CAASERVFAILExceptions)
cmd.FailOnError(err, "Couldn't read CAASERVFAILExceptions file")
resolver := bdns.NewDNSResolverImpl(
c.DNSTimeout.Duration,
[]string{c.DNSResolver},
caaSERVFAILExceptions,
scope,
clock.Default(),
5,

View File

@ -88,6 +88,9 @@ type Config struct {
// before giving up. May be short-circuited by deadlines. A zero value
// will be turned into 1.
DNSTries int
// Feature flag to enable enforcement of CAA SERVFAILs.
CAASERVFAILExceptions string
}
Statsd StatsdConfig

View File

@ -187,6 +187,7 @@
},
"va": {
"CAASERVFAILExceptions": "test/caa-servfail-exceptions.txt",
"userAgent": "boulder",
"debugAddr": "localhost:8004",
"portConfig": {

View File

@ -0,0 +1 @@
servfailexception.example.com