27bb03bbc0
* adding copyright header * fix fmt and a test
206 lines
4.8 KiB
Go
206 lines
4.8 KiB
Go
// 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
|
|
}
|