170 lines
4.7 KiB
Go
170 lines
4.7 KiB
Go
// 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
|
|
}
|