open-vault/command/healthcheck/pki_root_issued_leaves.go
Alexander Scheel 5ee7cc5e6d
Various health check improvements + tests (#18096)
* Rename common.go->healthcheck.go

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Push handling of no resources to the health checks

This allows us to better run on empty mounts.

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Exit when no issuers are found

This makes health checks less useful.

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add additional test criteria, refactor tests

This will allow us to setup more tests.

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add more OK statuses when checks are good

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add test cases for all bad results

The test for too-many-certs was elided for now due to being too hard to
setup in CI.

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add test for missing mount

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add expected failure test on empty mount

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add test for only having an issuer in the mount

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* More consistently perform permission checks

Also return them to the caller when they're relevant.

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add test without token

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Run health check tests in parallel

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Update command/healthcheck/healthcheck.go

Co-authored-by: Steven Clark <steven.clark@hashicorp.com>

* Update command/healthcheck/healthcheck.go

Co-authored-by: Steven Clark <steven.clark@hashicorp.com>

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
Co-authored-by: Steven Clark <steven.clark@hashicorp.com>
2022-11-23 14:42:19 +00:00

176 lines
3.9 KiB
Go

package healthcheck
import (
"bytes"
"crypto/x509"
"fmt"
"github.com/hashicorp/go-secure-stdlib/parseutil"
)
type RootIssuedLeaves struct {
Enabled bool
UnsupportedVersion bool
CertsToFetch int
RootCertMap map[string]*x509.Certificate
LeafCertMap map[string]*x509.Certificate
}
func NewRootIssuedLeavesCheck() Check {
return &RootIssuedLeaves{
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, _, cert, err := pkiFetchIssuer(e, issuer, func() {
h.UnsupportedVersion = true
})
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, _, leaves, err := pkiFetchLeavesList(e, func() {
h.UnsupportedVersion = true
})
if exit || err != nil {
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
}
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
}