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"
|
"crypto/x509"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
|
"github.com/letsencrypt/boulder/crl/idp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CRLProbe is the exported 'Prober' object for monitors configured to
|
// CRLProbe is the exported 'Prober' object for monitors configured to
|
||||||
// monitor CRL availability & characteristics.
|
// monitor CRL availability & characteristics.
|
||||||
type CRLProbe struct {
|
type CRLProbe struct {
|
||||||
url string
|
url string
|
||||||
|
partitioned bool
|
||||||
cNextUpdate *prometheus.GaugeVec
|
cNextUpdate *prometheus.GaugeVec
|
||||||
cThisUpdate *prometheus.GaugeVec
|
cThisUpdate *prometheus.GaugeVec
|
||||||
cCertCount *prometheus.GaugeVec
|
cCertCount *prometheus.GaugeVec
|
||||||
|
@ -47,6 +51,19 @@ func (p CRLProbe) Probe(timeout time.Duration) (bool, time.Duration) {
|
||||||
return false, dur
|
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
|
// Report metrics for this CRL
|
||||||
p.cThisUpdate.WithLabelValues(p.url).Set(float64(crl.ThisUpdate.Unix()))
|
p.cThisUpdate.WithLabelValues(p.url).Set(float64(crl.ThisUpdate.Unix()))
|
||||||
p.cNextUpdate.WithLabelValues(p.url).Set(float64(crl.NextUpdate.Unix()))
|
p.cNextUpdate.WithLabelValues(p.url).Set(float64(crl.NextUpdate.Unix()))
|
||||||
|
|
|
@ -4,9 +4,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
"github.com/letsencrypt/boulder/observer/probers"
|
"github.com/letsencrypt/boulder/observer/probers"
|
||||||
"github.com/letsencrypt/boulder/strictyaml"
|
"github.com/letsencrypt/boulder/strictyaml"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -17,7 +18,8 @@ const (
|
||||||
|
|
||||||
// CRLConf is exported to receive YAML configuration
|
// CRLConf is exported to receive YAML configuration
|
||||||
type CRLConf struct {
|
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`.
|
// 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 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
|
// Instrument constructs any `prometheus.Collector` objects the `CRLProbe` will
|
||||||
|
|
|
@ -3,10 +3,11 @@ package probers
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/letsencrypt/boulder/observer/probers"
|
|
||||||
"github.com/letsencrypt/boulder/test"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/letsencrypt/boulder/observer/probers"
|
||||||
|
"github.com/letsencrypt/boulder/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCRLConf_MakeProber(t *testing.T) {
|
func TestCRLConf_MakeProber(t *testing.T) {
|
||||||
|
@ -70,25 +71,20 @@ func TestCRLConf_MakeProber(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCRLConf_UnmarshalSettings(t *testing.T) {
|
func TestCRLConf_UnmarshalSettings(t *testing.T) {
|
||||||
type fields struct {
|
|
||||||
url interface{}
|
|
||||||
}
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
fields fields
|
fields probers.Settings
|
||||||
want probers.Configurer
|
want probers.Configurer
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{"valid", fields{"google.com"}, CRLConf{"google.com"}, false},
|
{"valid", probers.Settings{"url": "google.com"}, CRLConf{"google.com", false}, false},
|
||||||
{"invalid (map)", fields{make(map[string]interface{})}, nil, true},
|
{"valid with partitioned", probers.Settings{"url": "google.com", "partitioned": true}, CRLConf{"google.com", true}, false},
|
||||||
{"invalid (list)", fields{make([]string, 0)}, nil, true},
|
{"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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
settings := probers.Settings{
|
settingsBytes, _ := yaml.Marshal(tt.fields)
|
||||||
"url": tt.fields.url,
|
|
||||||
}
|
|
||||||
settingsBytes, _ := yaml.Marshal(settings)
|
|
||||||
t.Log(string(settingsBytes))
|
t.Log(string(settingsBytes))
|
||||||
c := CRLConf{}
|
c := CRLConf{}
|
||||||
got, err := c.UnmarshalSettings(settingsBytes)
|
got, err := c.UnmarshalSettings(settingsBytes)
|
||||||
|
|
Loading…
Reference in New Issue