Add enforcement for CAA SERVFAIL (#1971)
https://github.com/letsencrypt/boulder/pull/1971
This commit is contained in:
parent
6007df8f3c
commit
0c0e94dfaf
71
bdns/dns.go
71
bdns/dns.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -187,6 +187,7 @@
|
|||
},
|
||||
|
||||
"va": {
|
||||
"CAASERVFAILExceptions": "test/caa-servfail-exceptions.txt",
|
||||
"userAgent": "boulder",
|
||||
"debugAddr": "localhost:8004",
|
||||
"portConfig": {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
servfailexception.example.com
|
Loading…
Reference in New Issue