github.com/letsencrypt/boulder@v0.20251208.0/observer/probers/tls/tls_conf.go (about) 1 package probers 2 3 import ( 4 "fmt" 5 "net" 6 "net/url" 7 "slices" 8 "strconv" 9 "strings" 10 11 "github.com/prometheus/client_golang/prometheus" 12 13 "github.com/letsencrypt/boulder/observer/probers" 14 "github.com/letsencrypt/boulder/strictyaml" 15 ) 16 17 const ( 18 notAfterName = "obs_tls_not_after" 19 notBeforeName = "obs_tls_not_before" 20 reasonName = "obs_tls_reason" 21 ) 22 23 // TLSConf is exported to receive YAML configuration. 24 type TLSConf struct { 25 Hostname string `yaml:"hostname"` 26 RootOrg string `yaml:"rootOrg"` 27 RootCN string `yaml:"rootCN"` 28 Response string `yaml:"response"` 29 } 30 31 // Kind returns a name that uniquely identifies the `Kind` of `Configurer`. 32 func (c TLSConf) Kind() string { 33 return "TLS" 34 } 35 36 // UnmarshalSettings takes YAML as bytes and unmarshals it to the to an TLSConf 37 // object. 38 func (c TLSConf) UnmarshalSettings(settings []byte) (probers.Configurer, error) { 39 var conf TLSConf 40 err := strictyaml.Unmarshal(settings, &conf) 41 if err != nil { 42 return nil, err 43 } 44 45 return conf, nil 46 } 47 48 func (c TLSConf) validateHostname() error { 49 hostname := c.Hostname 50 51 if strings.Contains(c.Hostname, ":") { 52 host, port, err := net.SplitHostPort(c.Hostname) 53 if err != nil { 54 return fmt.Errorf("invalid 'hostname', got %q, expected a valid hostport: %s", c.Hostname, err) 55 } 56 57 _, err = strconv.Atoi(port) 58 if err != nil { 59 return fmt.Errorf("invalid 'hostname', got %q, expected a valid hostport: %s", c.Hostname, err) 60 } 61 hostname = host 62 } 63 64 url, err := url.Parse(hostname) 65 if err != nil { 66 return fmt.Errorf("invalid 'hostname', got %q, expected a valid hostname: %s", c.Hostname, err) 67 } 68 69 if url.Scheme != "" { 70 return fmt.Errorf("invalid 'hostname', got: %q, should not include scheme", c.Hostname) 71 } 72 73 return nil 74 } 75 76 func (c TLSConf) validateResponse() error { 77 acceptable := []string{"valid", "expired", "revoked"} 78 if slices.Contains(acceptable, strings.ToLower(c.Response)) { 79 return nil 80 } 81 82 return fmt.Errorf( 83 "invalid `response`, got %q. Must be one of %s", c.Response, acceptable) 84 } 85 86 // MakeProber constructs a `TLSProbe` object from the contents of the bound 87 // `TLSConf` object. If the `TLSConf` cannot be validated, an error appropriate 88 // for end-user consumption is returned instead. 89 func (c TLSConf) MakeProber(collectors map[string]prometheus.Collector) (probers.Prober, error) { 90 // Validate `hostname` 91 err := c.validateHostname() 92 if err != nil { 93 return nil, err 94 } 95 96 // Valid `response` 97 err = c.validateResponse() 98 if err != nil { 99 return nil, err 100 } 101 102 // Validate the Prometheus collectors that were passed in 103 coll, ok := collectors[notAfterName] 104 if !ok { 105 return nil, fmt.Errorf("tls prober did not receive collector %q", notAfterName) 106 } 107 108 notAfterColl, ok := coll.(*prometheus.GaugeVec) 109 if !ok { 110 return nil, fmt.Errorf("tls prober received collector %q of wrong type, got: %T, expected *prometheus.GaugeVec", notAfterName, coll) 111 } 112 113 coll, ok = collectors[notBeforeName] 114 if !ok { 115 return nil, fmt.Errorf("tls prober did not receive collector %q", notBeforeName) 116 } 117 118 notBeforeColl, ok := coll.(*prometheus.GaugeVec) 119 if !ok { 120 return nil, fmt.Errorf("tls prober received collector %q of wrong type, got: %T, expected *prometheus.GaugeVec", notBeforeName, coll) 121 } 122 123 coll, ok = collectors[reasonName] 124 if !ok { 125 return nil, fmt.Errorf("tls prober did not receive collector %q", reasonName) 126 } 127 128 reasonColl, ok := coll.(*prometheus.CounterVec) 129 if !ok { 130 return nil, fmt.Errorf("tls prober received collector %q of wrong type, got: %T, expected *prometheus.CounterVec", reasonName, coll) 131 } 132 133 return TLSProbe{c.Hostname, c.RootOrg, c.RootCN, strings.ToLower(c.Response), notAfterColl, notBeforeColl, reasonColl}, nil 134 } 135 136 // Instrument constructs any `prometheus.Collector` objects the `TLSProbe` will 137 // need to report its own metrics. A map is returned containing the constructed 138 // objects, indexed by the name of the Promtheus metric. If no objects were 139 // constructed, nil is returned. 140 func (c TLSConf) Instrument() map[string]prometheus.Collector { 141 notBefore := prometheus.Collector(prometheus.NewGaugeVec( 142 prometheus.GaugeOpts{ 143 Name: notBeforeName, 144 Help: "Certificate notBefore value as a Unix timestamp in seconds", 145 }, []string{"hostname"}, 146 )) 147 notAfter := prometheus.Collector(prometheus.NewGaugeVec( 148 prometheus.GaugeOpts{ 149 Name: notAfterName, 150 Help: "Certificate notAfter value as a Unix timestamp in seconds", 151 }, []string{"hostname"}, 152 )) 153 reason := prometheus.Collector(prometheus.NewCounterVec( 154 prometheus.CounterOpts{ 155 Name: reasonName, 156 Help: fmt.Sprintf("Reason for TLS Prober check failure. Can be one of %s", getReasons()), 157 }, []string{"hostname", "reason"}, 158 )) 159 return map[string]prometheus.Collector{ 160 notAfterName: notAfter, 161 notBeforeName: notBefore, 162 reasonName: reason, 163 } 164 } 165 166 // init is called at runtime and registers `TLSConf`, a `Prober` `Configurer` 167 // type, as "TLS". 168 func init() { 169 probers.Register(TLSConf{}) 170 }