| // 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 |
| } |