| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package healthcheck |
| |
| import ( |
| "bytes" |
| "crypto/x509" |
| "fmt" |
| "strings" |
| "time" |
| |
| "github.com/hashicorp/go-secure-stdlib/parseutil" |
| ) |
| |
| type CAValidityPeriod struct { |
| Enabled bool |
| |
| RootExpiries map[ResultStatus]time.Duration |
| IntermediateExpieries map[ResultStatus]time.Duration |
| |
| UnsupportedVersion bool |
| |
| Issuers map[string]*x509.Certificate |
| } |
| |
| func NewCAValidityPeriodCheck() Check { |
| return &CAValidityPeriod{ |
| RootExpiries: make(map[ResultStatus]time.Duration, 3), |
| IntermediateExpieries: make(map[ResultStatus]time.Duration, 3), |
| Issuers: make(map[string]*x509.Certificate), |
| } |
| } |
| |
| func (h *CAValidityPeriod) Name() string { |
| return "ca_validity_period" |
| } |
| |
| func (h *CAValidityPeriod) IsEnabled() bool { |
| return h.Enabled |
| } |
| |
| func (h *CAValidityPeriod) DefaultConfig() map[string]interface{} { |
| return map[string]interface{}{ |
| "root_expiry_critical": "180d", |
| "intermediate_expiry_critical": "30d", |
| "root_expiry_warning": "365d", |
| "intermediate_expiry_warning": "60d", |
| "root_expiry_informational": "730d", |
| "intermediate_expiry_informational": "180d", |
| } |
| } |
| |
| func (h *CAValidityPeriod) LoadConfig(config map[string]interface{}) error { |
| parameters := []string{ |
| "root_expiry_critical", |
| "intermediate_expiry_critical", |
| "root_expiry_warning", |
| "intermediate_expiry_warning", |
| "root_expiry_informational", |
| "intermediate_expiry_informational", |
| } |
| for _, parameter := range parameters { |
| name_split := strings.Split(parameter, "_") |
| if len(name_split) != 3 || name_split[1] != "expiry" { |
| return fmt.Errorf("bad parameter: %v / %v / %v", parameter, len(name_split), name_split[1]) |
| } |
| |
| status, present := NameResultStatusMap[name_split[2]] |
| if !present { |
| return fmt.Errorf("bad parameter: %v's type %v isn't in name map", parameter, name_split[2]) |
| } |
| |
| value_raw, present := config[parameter] |
| if !present { |
| return fmt.Errorf("parameter not present in config; Executor should've handled this for us: %v", parameter) |
| } |
| |
| value, err := parseutil.ParseDurationSecond(value_raw) |
| if err != nil { |
| return fmt.Errorf("failed to parse parameter (%v=%v): %w", parameter, value_raw, err) |
| } |
| |
| if name_split[0] == "root" { |
| h.RootExpiries[status] = value |
| } else if name_split[0] == "intermediate" { |
| h.IntermediateExpieries[status] = value |
| } else { |
| return fmt.Errorf("bad parameter: %v's CA type isn't root/intermediate: %v", parameters, name_split[0]) |
| } |
| } |
| |
| enabled, err := parseutil.ParseBool(config["enabled"]) |
| if err != nil { |
| return fmt.Errorf("error parsing %v.enabled: %w", h.Name(), err) |
| } |
| h.Enabled = enabled |
| |
| return nil |
| } |
| |
| func (h *CAValidityPeriod) FetchResources(e *Executor) error { |
| exit, _, issuers, err := pkiFetchIssuersList(e, func() { |
| h.UnsupportedVersion = true |
| }) |
| if exit || err != nil { |
| return err |
| } |
| |
| for _, issuer := range issuers { |
| skip, _, cert, err := pkiFetchIssuer(e, issuer, func() { |
| h.UnsupportedVersion = true |
| }) |
| if skip || err != nil { |
| if err != nil { |
| return err |
| } |
| continue |
| } |
| |
| h.Issuers[issuer] = cert |
| } |
| |
| return nil |
| } |
| |
| func (h *CAValidityPeriod) Evaluate(e *Executor) (results []*Result, err error) { |
| if h.UnsupportedVersion { |
| ret := Result{ |
| Status: ResultInvalidVersion, |
| Endpoint: "/{{mount}}/issuers", |
| Message: "This health check requires Vault 1.11+ but an earlier version of Vault Server was contacted, preventing this health check from running.", |
| } |
| return []*Result{&ret}, nil |
| } |
| |
| now := time.Now() |
| |
| for name, cert := range h.Issuers { |
| var ret Result |
| ret.Status = ResultOK |
| ret.Endpoint = "/{{mount}}/issuer/" + name |
| ret.Message = fmt.Sprintf("Issuer's validity (%v) is OK", cert.NotAfter.Format("2006-01-02")) |
| |
| hasSelfReference := bytes.Equal(cert.RawSubject, cert.RawIssuer) |
| isSelfSigned := cert.CheckSignatureFrom(cert) == nil |
| isRoot := hasSelfReference && isSelfSigned |
| |
| for _, criticality := range []ResultStatus{ResultCritical, ResultWarning, ResultInformational} { |
| var d time.Duration |
| if isRoot { |
| d = h.RootExpiries[criticality] |
| } else { |
| d = h.IntermediateExpieries[criticality] |
| } |
| |
| windowExpiry := now.Add(d) |
| if cert.NotAfter.Before(windowExpiry) { |
| ret.Status = criticality |
| ret.Message = fmt.Sprintf("Issuer's validity is outside of the suggested rotation window: issuer is valid until %v but expires within %v (ending on %v). It is suggested to start rotating this issuer to new key material to avoid future downtime caused by this current issuer expiring.", cert.NotAfter.Format("2006-01-02"), FormatDuration(d), windowExpiry.Format("2006-01-02")) |
| break |
| } |
| } |
| |
| results = append(results, &ret) |
| } |
| |
| return |
| } |