// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package healthcheck import ( "bytes" "crypto/x509" "fmt" "github.com/hashicorp/go-secure-stdlib/parseutil" ) type RootIssuedLeaves struct { Enabled bool UnsupportedVersion bool CertsToFetch int FetchIssues map[string]*PathFetch RootCertMap map[string]*x509.Certificate LeafCertMap map[string]*x509.Certificate } func NewRootIssuedLeavesCheck() Check { return &RootIssuedLeaves{ FetchIssues: make(map[string]*PathFetch), RootCertMap: make(map[string]*x509.Certificate), LeafCertMap: make(map[string]*x509.Certificate), } } func (h *RootIssuedLeaves) Name() string { return "root_issued_leaves" } func (h *RootIssuedLeaves) IsEnabled() bool { return h.Enabled } func (h *RootIssuedLeaves) DefaultConfig() map[string]interface{} { return map[string]interface{}{ "certs_to_fetch": 100, } } func (h *RootIssuedLeaves) LoadConfig(config map[string]interface{}) error { count, err := parseutil.SafeParseIntRange(config["certs_to_fetch"], 1, 100000) if err != nil { return fmt.Errorf("error parsing %v.certs_to_fetch: %w", h.Name(), err) } h.CertsToFetch = int(count) 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 *RootIssuedLeaves) FetchResources(e *Executor) error { exit, _, issuers, err := pkiFetchIssuersList(e, func() { h.UnsupportedVersion = true }) if exit || err != nil { return err } for _, issuer := range issuers { skip, pathFetch, cert, err := pkiFetchIssuer(e, issuer, func() { h.UnsupportedVersion = true }) h.FetchIssues[issuer] = pathFetch if skip || err != nil { if err != nil { return err } continue } // Ensure we only check Root CAs. if !bytes.Equal(cert.RawSubject, cert.RawIssuer) { continue } if err := cert.CheckSignatureFrom(cert); err != nil { continue } h.RootCertMap[issuer] = cert } exit, f, leaves, err := pkiFetchLeavesList(e, func() { h.UnsupportedVersion = true }) if exit || err != nil { if f != nil && f.IsSecretPermissionsError() { for _, issuer := range issuers { h.FetchIssues[issuer] = f } } return err } var leafCount int for _, serial := range leaves { if leafCount >= h.CertsToFetch { break } skip, _, cert, err := pkiFetchLeaf(e, serial, func() { h.UnsupportedVersion = true }) if skip || err != nil { if err != nil { return err } continue } // Ignore other CAs. if cert.BasicConstraintsValid && cert.IsCA { continue } leafCount += 1 h.LeafCertMap[serial] = cert } return nil } func (h *RootIssuedLeaves) 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 } for issuer, fetchPath := range h.FetchIssues { if fetchPath != nil && fetchPath.IsSecretPermissionsError() { delete(h.RootCertMap, issuer) ret := Result{ Status: ResultInsufficientPermissions, Endpoint: fetchPath.Path, Message: "Without this information, this health check is unable to function.", } if e.Client.Token() == "" { ret.Message = "No token available so unable for the endpoint for this mount. " + ret.Message } else { ret.Message = "This token lacks permission for the endpoint for this mount. " + ret.Message } results = append(results, &ret) } } issuerHasLeaf := make(map[string]bool) for serial, leaf := range h.LeafCertMap { if len(issuerHasLeaf) == len(h.RootCertMap) { break } for issuer, root := range h.RootCertMap { if issuerHasLeaf[issuer] { continue } if !bytes.Equal(leaf.RawIssuer, root.RawSubject) { continue } if err := leaf.CheckSignatureFrom(root); err != nil { continue } ret := Result{ Status: ResultWarning, Endpoint: "/{{mount}}/issuer/" + issuer, Message: fmt.Sprintf("Root issuer has directly issued non-CA leaf certificates (%v) instead of via an intermediate CA. This can make rotating the root CA harder as direct cross-signing of the roots must be used, rather than cross-signing of the intermediates. It is encouraged to set up and use an intermediate CA and tidy the mount when all directly issued leaves have expired.", serial), } issuerHasLeaf[issuer] = true results = append(results, &ret) } } if len(results) == 0 && len(h.RootCertMap) > 0 { ret := Result{ Status: ResultOK, Endpoint: "/{{mount}}/certs", Message: "Root certificate(s) in this mount have not directly issued non-CA leaf certificates.", } results = append(results, &ret) } return }