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  }