Add role-based health checks to PKI Health Check (#17877)
* Add more PKI related common utilities Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add role_allows_localhost health check Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add role_allows_glob_wildcards health checks Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add role_no_store_false health check Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add new checks to the CLI Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Switch to new guard style Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Provision role for test Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Address review feedback Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Fix invalid version check Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Fix message with auto-rebuild enabled Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
This commit is contained in:
parent
610a4ede82
commit
0217f7de65
|
@ -227,3 +227,49 @@ func pkiFetchLeaf(e *Executor, serial string, versionError func()) (bool, *PathF
|
|||
|
||||
return false, leafRet, leafRet.ParsedCache["certificate"].(*x509.Certificate), nil
|
||||
}
|
||||
|
||||
func pkiFetchRoles(e *Executor, versionError func()) (bool, *PathFetch, []string, error) {
|
||||
rolesRet, err := e.FetchIfNotFetched(logical.ListOperation, "/{{mount}}/roles")
|
||||
if err != nil {
|
||||
return true, nil, nil, err
|
||||
}
|
||||
|
||||
if !rolesRet.IsSecretOK() {
|
||||
if rolesRet.IsUnsupportedPathError() {
|
||||
versionError()
|
||||
}
|
||||
|
||||
return true, nil, nil, nil
|
||||
}
|
||||
|
||||
if len(rolesRet.ParsedCache) == 0 {
|
||||
var roles []string
|
||||
for _, roleName := range rolesRet.Secret.Data["keys"].([]interface{}) {
|
||||
roles = append(roles, roleName.(string))
|
||||
}
|
||||
rolesRet.ParsedCache["roles"] = roles
|
||||
}
|
||||
|
||||
return false, rolesRet, rolesRet.ParsedCache["roles"].([]string), nil
|
||||
}
|
||||
|
||||
func pkiFetchRole(e *Executor, name string, versionError func()) (bool, *PathFetch, map[string]interface{}, error) {
|
||||
roleRet, err := e.FetchIfNotFetched(logical.ReadOperation, "/{{mount}}/roles/"+name)
|
||||
if err != nil {
|
||||
return true, nil, nil, err
|
||||
}
|
||||
|
||||
if !roleRet.IsSecretOK() {
|
||||
if roleRet.IsUnsupportedPathError() {
|
||||
versionError()
|
||||
}
|
||||
return true, nil, nil, nil
|
||||
}
|
||||
|
||||
var data map[string]interface{} = nil
|
||||
if roleRet.Secret != nil && len(roleRet.Secret.Data) > 0 {
|
||||
data = roleRet.Secret.Data
|
||||
}
|
||||
|
||||
return false, roleRet, data, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
package healthcheck
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-secure-stdlib/parseutil"
|
||||
)
|
||||
|
||||
type RoleAllowsGlobWildcards struct {
|
||||
Enabled bool
|
||||
UnsupportedVersion bool
|
||||
|
||||
RoleEntryMap map[string]map[string]interface{}
|
||||
}
|
||||
|
||||
func NewRoleAllowsGlobWildcardsCheck() Check {
|
||||
return &RoleAllowsGlobWildcards{
|
||||
RoleEntryMap: make(map[string]map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RoleAllowsGlobWildcards) Name() string {
|
||||
return "role_allows_glob_wildcards"
|
||||
}
|
||||
|
||||
func (h *RoleAllowsGlobWildcards) IsEnabled() bool {
|
||||
return h.Enabled
|
||||
}
|
||||
|
||||
func (h *RoleAllowsGlobWildcards) DefaultConfig() map[string]interface{} {
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
|
||||
func (h *RoleAllowsGlobWildcards) LoadConfig(config map[string]interface{}) error {
|
||||
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 *RoleAllowsGlobWildcards) FetchResources(e *Executor) error {
|
||||
exit, _, roles, err := pkiFetchRoles(e, func() {
|
||||
h.UnsupportedVersion = true
|
||||
})
|
||||
if exit || err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, role := range roles {
|
||||
skip, _, entry, err := pkiFetchRole(e, role, func() {
|
||||
h.UnsupportedVersion = true
|
||||
})
|
||||
if skip || err != nil || entry == nil {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
h.RoleEntryMap[role] = entry
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *RoleAllowsGlobWildcards) 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
|
||||
}
|
||||
|
||||
for role, entry := range h.RoleEntryMap {
|
||||
allowsWildcards, present := entry["allow_wildcard_certificates"]
|
||||
if !present {
|
||||
ret := Result{
|
||||
Status: ResultInvalidVersion,
|
||||
Endpoint: "/{{mount}}/roles",
|
||||
Message: "This health check requires a version of Vault with allow_wildcard_certificates (Vault 1.8.9+, 1.9.4+, or 1.10.0+), but an earlier version of Vault Server was contacted, preventing this health check from running.",
|
||||
}
|
||||
return []*Result{&ret}, nil
|
||||
}
|
||||
if !allowsWildcards.(bool) {
|
||||
continue
|
||||
}
|
||||
|
||||
allowsGlobs := entry["allow_glob_domains"].(bool)
|
||||
if !allowsGlobs {
|
||||
continue
|
||||
}
|
||||
|
||||
rawAllowedDomains := entry["allowed_domains"].([]interface{})
|
||||
var allowedDomains []string
|
||||
for _, rawDomain := range rawAllowedDomains {
|
||||
allowedDomains = append(allowedDomains, rawDomain.(string))
|
||||
}
|
||||
|
||||
if len(allowedDomains) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
hasGlobbedDomain := false
|
||||
for _, domain := range allowedDomains {
|
||||
if strings.Contains(domain, "*") {
|
||||
hasGlobbedDomain = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasGlobbedDomain {
|
||||
continue
|
||||
}
|
||||
|
||||
ret := Result{
|
||||
Status: ResultWarning,
|
||||
Endpoint: "/{{mount}}/role/" + role,
|
||||
Message: fmt.Sprintf("Role currently allows wildcard issuance while allowing globs in allowed_domains (%v). Because globs can expand to one or more wildcard character, including wildcards under additional subdomains, these options are dangerous to enable together. If glob domains are required to be enabled, it is suggested to either disable wildcard issuance if not desired, or create two separate roles -- one with wildcard issuanced for specified domains, and one with glob matching enabled for concrete domain identifiers.", allowedDomains),
|
||||
}
|
||||
|
||||
results = append(results, &ret)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package healthcheck
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/go-secure-stdlib/parseutil"
|
||||
)
|
||||
|
||||
type RoleAllowsLocalhost struct {
|
||||
Enabled bool
|
||||
UnsupportedVersion bool
|
||||
|
||||
RoleEntryMap map[string]map[string]interface{}
|
||||
}
|
||||
|
||||
func NewRoleAllowsLocalhostCheck() Check {
|
||||
return &RoleAllowsLocalhost{
|
||||
RoleEntryMap: make(map[string]map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RoleAllowsLocalhost) Name() string {
|
||||
return "role_allows_localhost"
|
||||
}
|
||||
|
||||
func (h *RoleAllowsLocalhost) IsEnabled() bool {
|
||||
return h.Enabled
|
||||
}
|
||||
|
||||
func (h *RoleAllowsLocalhost) DefaultConfig() map[string]interface{} {
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
|
||||
func (h *RoleAllowsLocalhost) LoadConfig(config map[string]interface{}) error {
|
||||
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 *RoleAllowsLocalhost) FetchResources(e *Executor) error {
|
||||
exit, _, roles, err := pkiFetchRoles(e, func() {
|
||||
h.UnsupportedVersion = true
|
||||
})
|
||||
if exit || err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, role := range roles {
|
||||
skip, _, entry, err := pkiFetchRole(e, role, func() {
|
||||
h.UnsupportedVersion = true
|
||||
})
|
||||
if skip || err != nil || entry == nil {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
h.RoleEntryMap[role] = entry
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *RoleAllowsLocalhost) 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
|
||||
}
|
||||
for role, entry := range h.RoleEntryMap {
|
||||
allowsLocalhost := entry["allow_localhost"].(bool)
|
||||
if !allowsLocalhost {
|
||||
continue
|
||||
}
|
||||
|
||||
rawAllowedDomains := entry["allowed_domains"].([]interface{})
|
||||
var allowedDomains []string
|
||||
for _, rawDomain := range rawAllowedDomains {
|
||||
allowedDomains = append(allowedDomains, rawDomain.(string))
|
||||
}
|
||||
|
||||
if len(allowedDomains) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
ret := Result{
|
||||
Status: ResultWarning,
|
||||
Endpoint: "/{{mount}}/role/" + role,
|
||||
Message: fmt.Sprintf("Role currently allows localhost issuance with a non-empty allowed_domains (%v): this role is intended for issuing other hostnames and the allow_localhost=true option may be overlooked by operators. If this role is intended to issue certificates valid for localhost, consider setting allow_localhost=false and explicitly adding localhost to the list of allowed domains.", allowedDomains),
|
||||
}
|
||||
|
||||
results = append(results, &ret)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
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
|
||||
|
||||
CertCounts int
|
||||
RoleEntryMap map[string]map[string]interface{}
|
||||
CRLConfig *PathFetch
|
||||
}
|
||||
|
||||
func NewRoleNoStoreFalseCheck() Check {
|
||||
return &RoleNoStoreFalse{
|
||||
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, _, roles, err := pkiFetchRoles(e, func() {
|
||||
h.UnsupportedVersion = true
|
||||
})
|
||||
if exit || err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, role := range roles {
|
||||
skip, _, entry, err := pkiFetchRole(e, role, func() {
|
||||
h.UnsupportedVersion = true
|
||||
})
|
||||
if skip || err != nil || entry == nil {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
h.RoleEntryMap[role] = entry
|
||||
}
|
||||
|
||||
exit, _, leaves, err := pkiFetchLeaves(e, func() {
|
||||
h.UnsupportedVersion = true
|
||||
})
|
||||
if exit || err != nil {
|
||||
return err
|
||||
}
|
||||
h.CertCounts = len(leaves)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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}}/role/" + 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)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
|
@ -199,6 +199,9 @@ func (c *PKIHealthCheckCommand) Run(args []string) int {
|
|||
executor.AddCheck(healthcheck.NewCRLValidityPeriodCheck())
|
||||
executor.AddCheck(healthcheck.NewHardwareBackedRootCheck())
|
||||
executor.AddCheck(healthcheck.NewRootIssuedLeavesCheck())
|
||||
executor.AddCheck(healthcheck.NewRoleAllowsLocalhostCheck())
|
||||
executor.AddCheck(healthcheck.NewRoleAllowsGlobWildcardsCheck())
|
||||
executor.AddCheck(healthcheck.NewRoleNoStoreFalseCheck())
|
||||
if c.flagDefaultDisabled {
|
||||
executor.DefaultEnabled = false
|
||||
}
|
||||
|
|
|
@ -45,6 +45,12 @@ func TestPKIHC_Run(t *testing.T) {
|
|||
t.Fatalf("failed to rotate CRLs: %v", err)
|
||||
}
|
||||
|
||||
if _, err := client.Logical().Write("pki/roles/testing", map[string]interface{}{
|
||||
"allow_any_name": true,
|
||||
}); err != nil {
|
||||
t.Fatalf("failed to write role: %v", err)
|
||||
}
|
||||
|
||||
stdout := bytes.NewBuffer(nil)
|
||||
stderr := bytes.NewBuffer(nil)
|
||||
runOpts := &RunOptions{
|
||||
|
|
Loading…
Reference in New Issue