// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package healthcheck import ( "fmt" "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/go-secure-stdlib/parseutil" ) type RoleNoStoreFalse struct { Enabled bool UnsupportedVersion bool AllowedRoles map[string]bool RoleListFetchIssue *PathFetch RoleFetchIssues map[string]*PathFetch RoleEntryMap map[string]map[string]interface{} CRLConfig *PathFetch } func NewRoleNoStoreFalseCheck() Check { return &RoleNoStoreFalse{ RoleFetchIssues: make(map[string]*PathFetch), AllowedRoles: make(map[string]bool), RoleEntryMap: make(map[string]map[string]interface{}), } } func (h *RoleNoStoreFalse) Name() string { return "role_no_store_false" } func (h *RoleNoStoreFalse) IsEnabled() bool { return h.Enabled } func (h *RoleNoStoreFalse) DefaultConfig() map[string]interface{} { return map[string]interface{}{ "allowed_roles": []string{}, } } func (h *RoleNoStoreFalse) LoadConfig(config map[string]interface{}) error { value, present := config["allowed_roles"].([]interface{}) if present { for _, rawValue := range value { h.AllowedRoles[rawValue.(string)] = true } } 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 *RoleNoStoreFalse) FetchResources(e *Executor) error { exit, f, roles, err := pkiFetchRolesList(e, func() { h.UnsupportedVersion = true }) if exit || err != nil { if f != nil && f.IsSecretPermissionsError() { h.RoleListFetchIssue = f } return err } for _, role := range roles { skip, f, entry, err := pkiFetchRole(e, role, func() { h.UnsupportedVersion = true }) if skip || err != nil || entry == nil { if f != nil && f.IsSecretPermissionsError() { h.RoleFetchIssues[role] = f } if err != nil { return err } continue } h.RoleEntryMap[role] = entry } // Check if the issuer is fetched yet. configRet, err := e.FetchIfNotFetched(logical.ReadOperation, "/{{mount}}/config/crl") if err != nil { return err } h.CRLConfig = configRet return nil } func (h *RoleNoStoreFalse) Evaluate(e *Executor) (results []*Result, err error) { if h.UnsupportedVersion { // Shouldn't happen; roles have been around forever. ret := Result{ Status: ResultInvalidVersion, Endpoint: "/{{mount}}/roles", 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 } if h.RoleListFetchIssue != nil && h.RoleListFetchIssue.IsSecretPermissionsError() { ret := Result{ Status: ResultInsufficientPermissions, Endpoint: h.RoleListFetchIssue.Path, Message: "lacks permission either to list the roles. This restricts the ability to fully execute this health check.", } if e.Client.Token() == "" { ret.Message = "No token available and so this health check " + ret.Message } else { ret.Message = "This token " + ret.Message } return []*Result{&ret}, nil } for role, fetchPath := range h.RoleFetchIssues { if fetchPath != nil && fetchPath.IsSecretPermissionsError() { delete(h.RoleEntryMap, role) 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 the endpoint for this mount. " + ret.Message } results = append(results, &ret) } } crlAutoRebuild := false if h.CRLConfig != nil { if h.CRLConfig.IsSecretPermissionsError() { ret := Result{ Status: ResultInsufficientPermissions, Endpoint: "/{{mount}}/config/crl", Message: "This prevents the health check from seeing if the CRL is set to auto_rebuild=true and lowering the severity of check results appropriately.", } if e.Client.Token() == "" { ret.Message = "No token available so unable read authenticated CRL configuration for this mount. " + ret.Message } else { ret.Message = "This token lacks so permission to read the CRL configuration for this mount. " + ret.Message } results = append(results, &ret) } else if h.CRLConfig.Secret != nil && h.CRLConfig.Secret.Data["auto_rebuild"] != nil { crlAutoRebuild = h.CRLConfig.Secret.Data["auto_rebuild"].(bool) } } for role, entry := range h.RoleEntryMap { noStore := entry["no_store"].(bool) if noStore { continue } ret := Result{ Status: ResultWarning, Endpoint: "/{{mount}}/roles/" + role, Message: "Role currently stores every issued certificate (no_store=false). Too many issued and/or revoked certificates can exceed Vault's storage limits and make operations slow. It is encouraged to enable auto-rebuild of CRLs to prevent every revocation from creating a new CRL, and to limit the number of certificates issued under roles with no_store=false: use shorter lifetimes and/or BYOC revocation instead.", } if crlAutoRebuild { ret.Status = ResultInformational ret.Message = "Role currently stores every issued certificate (no_store=false). With auto-rebuild CRL enabled, less performance impact occur on CRL rebuilding, but note that too many issued and/or revoked certificates can exceed Vault's storage limits and make operations slow. It is suggested to limit the number of certificates issued under roles with no_store=false: use shorter lifetimes to avoid revocation and/or BYOC revocation instead." } results = append(results, &ret) } if len(results) == 0 && len(h.RoleEntryMap) > 0 { ret := Result{ Status: ResultOK, Endpoint: "/{{mount}}/roles", Message: "Roles follow best practices regarding certificate storage.", } results = append(results, &ret) } return }