Observer: detect CRL IDP mismatch (#8067)
Give boulder-observer the ability to detect if the CRL it fetches is the CRL it expects, by comparing that CRLs issuingDistributionPoint extension to the prober's configured URL. Only do this if instructed to (by configuring the CRL prober as "partitioned") because non-partitioned CRLs do not necessarily contain an IDP. Fixes https://github.com/letsencrypt/boulder/issues/7527
This commit is contained in:
parent
ebf232cccb
commit
d045b387ef
|
@ -4,15 +4,19 @@ import (
|
|||
"crypto/x509"
|
||||
"io"
|
||||
"net/http"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/letsencrypt/boulder/crl/idp"
|
||||
)
|
||||
|
||||
// CRLProbe is the exported 'Prober' object for monitors configured to
|
||||
// monitor CRL availability & characteristics.
|
||||
type CRLProbe struct {
|
||||
url string
|
||||
partitioned bool
|
||||
cNextUpdate *prometheus.GaugeVec
|
||||
cThisUpdate *prometheus.GaugeVec
|
||||
cCertCount *prometheus.GaugeVec
|
||||
|
@ -47,6 +51,19 @@ func (p CRLProbe) Probe(timeout time.Duration) (bool, time.Duration) {
|
|||
return false, dur
|
||||
}
|
||||
|
||||
// Partitioned CRLs MUST contain an issuingDistributionPoint extension, which
|
||||
// MUST contain the URL from which they were fetched, to prevent substitution
|
||||
// attacks.
|
||||
if p.partitioned {
|
||||
idps, err := idp.GetIDPURIs(crl.Extensions)
|
||||
if err != nil {
|
||||
return false, dur
|
||||
}
|
||||
if len(idps) != 0 && !slices.Contains(idps, p.url) {
|
||||
return false, dur
|
||||
}
|
||||
}
|
||||
|
||||
// Report metrics for this CRL
|
||||
p.cThisUpdate.WithLabelValues(p.url).Set(float64(crl.ThisUpdate.Unix()))
|
||||
p.cNextUpdate.WithLabelValues(p.url).Set(float64(crl.NextUpdate.Unix()))
|
||||
|
|
|
@ -4,9 +4,10 @@ import (
|
|||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/letsencrypt/boulder/observer/probers"
|
||||
"github.com/letsencrypt/boulder/strictyaml"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -17,7 +18,8 @@ const (
|
|||
|
||||
// CRLConf is exported to receive YAML configuration
|
||||
type CRLConf struct {
|
||||
URL string `yaml:"url"`
|
||||
URL string `yaml:"url"`
|
||||
Partitioned bool `yaml:"partitioned"`
|
||||
}
|
||||
|
||||
// Kind returns a name that uniquely identifies the `Kind` of `Configurer`.
|
||||
|
@ -87,7 +89,7 @@ func (c CRLConf) MakeProber(collectors map[string]prometheus.Collector) (probers
|
|||
return nil, fmt.Errorf("crl prober received collector %q of wrong type, got: %T, expected *prometheus.GaugeVec", certCountName, coll)
|
||||
}
|
||||
|
||||
return CRLProbe{c.URL, nextUpdateColl, thisUpdateColl, certCountColl}, nil
|
||||
return CRLProbe{c.URL, c.Partitioned, nextUpdateColl, thisUpdateColl, certCountColl}, nil
|
||||
}
|
||||
|
||||
// Instrument constructs any `prometheus.Collector` objects the `CRLProbe` will
|
||||
|
|
|
@ -3,10 +3,11 @@ package probers
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/letsencrypt/boulder/observer/probers"
|
||||
"github.com/letsencrypt/boulder/test"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/letsencrypt/boulder/observer/probers"
|
||||
"github.com/letsencrypt/boulder/test"
|
||||
)
|
||||
|
||||
func TestCRLConf_MakeProber(t *testing.T) {
|
||||
|
@ -70,25 +71,20 @@ func TestCRLConf_MakeProber(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCRLConf_UnmarshalSettings(t *testing.T) {
|
||||
type fields struct {
|
||||
url interface{}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
fields probers.Settings
|
||||
want probers.Configurer
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid", fields{"google.com"}, CRLConf{"google.com"}, false},
|
||||
{"invalid (map)", fields{make(map[string]interface{})}, nil, true},
|
||||
{"invalid (list)", fields{make([]string, 0)}, nil, true},
|
||||
{"valid", probers.Settings{"url": "google.com"}, CRLConf{"google.com", false}, false},
|
||||
{"valid with partitioned", probers.Settings{"url": "google.com", "partitioned": true}, CRLConf{"google.com", true}, false},
|
||||
{"invalid (map)", probers.Settings{"url": make(map[string]interface{})}, nil, true},
|
||||
{"invalid (list)", probers.Settings{"url": make([]string, 0)}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
settings := probers.Settings{
|
||||
"url": tt.fields.url,
|
||||
}
|
||||
settingsBytes, _ := yaml.Marshal(settings)
|
||||
settingsBytes, _ := yaml.Marshal(tt.fields)
|
||||
t.Log(string(settingsBytes))
|
||||
c := CRLConf{}
|
||||
got, err := c.UnmarshalSettings(settingsBytes)
|
||||
|
|
Loading…
Reference in New Issue