c8837f2010
* Add ACME health checks to pki health-check CLI - Verify we have the required header values listed within allowed_response_headers: 'Replay-Nonce', 'Link', 'Location' - Make sure the local cluster config path variable contains an URL with an https scheme * Split ACME health checks into two separate verifications - Promote ACME usage through the enable_acme_issuance check, if ACME is disabled currently - If ACME is enabled verify that we have a valid 'path' field within local cluster configuration as well as the proper response headers allowed. - Factor out response header verifications into a separate check mainly to work around possible permission issues. * Only recommend enabling ACME on mounts with intermediate issuers * Attempt to connect to the ACME directory based on the cluster path variable - Final health check is to attempt to connect to the ACME directory based on the cluster local 'path' value. Only if we successfully connect do we say ACME is healthy. * Fix broken unit test
156 lines
4.1 KiB
Go
156 lines
4.1 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package healthcheck
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/go-secure-stdlib/parseutil"
|
|
"github.com/hashicorp/go-secure-stdlib/strutil"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
type AllowAcmeHeaders struct {
|
|
Enabled bool
|
|
UnsupportedVersion bool
|
|
|
|
TuneFetcher *PathFetch
|
|
TuneData map[string]interface{}
|
|
|
|
AcmeConfigFetcher *PathFetch
|
|
}
|
|
|
|
func NewAllowAcmeHeaders() Check {
|
|
return &AllowAcmeHeaders{}
|
|
}
|
|
|
|
func (h *AllowAcmeHeaders) Name() string {
|
|
return "allow_acme_headers"
|
|
}
|
|
|
|
func (h *AllowAcmeHeaders) IsEnabled() bool {
|
|
return h.Enabled
|
|
}
|
|
|
|
func (h *AllowAcmeHeaders) DefaultConfig() map[string]interface{} {
|
|
return map[string]interface{}{}
|
|
}
|
|
|
|
func (h *AllowAcmeHeaders) 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 *AllowAcmeHeaders) FetchResources(e *Executor) error {
|
|
var err error
|
|
h.AcmeConfigFetcher, err = e.FetchIfNotFetched(logical.ReadOperation, "/{{mount}}/config/acme")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if h.AcmeConfigFetcher.IsUnsupportedPathError() {
|
|
h.UnsupportedVersion = true
|
|
}
|
|
|
|
_, h.TuneFetcher, h.TuneData, err = fetchMountTune(e, func() {
|
|
h.UnsupportedVersion = true
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (h *AllowAcmeHeaders) Evaluate(e *Executor) ([]*Result, error) {
|
|
if h.UnsupportedVersion {
|
|
ret := Result{
|
|
Status: ResultInvalidVersion,
|
|
Endpoint: h.AcmeConfigFetcher.Path,
|
|
Message: "This health check requires Vault 1.14+ but an earlier version of Vault Server was contacted, preventing this health check from running.",
|
|
}
|
|
return []*Result{&ret}, nil
|
|
}
|
|
|
|
if h.AcmeConfigFetcher.IsSecretPermissionsError() {
|
|
msg := "Without read access to ACME configuration, this health check is unable to function."
|
|
return craftInsufficientPermissionResult(e, h.AcmeConfigFetcher.Path, msg), nil
|
|
}
|
|
|
|
acmeEnabled, err := isAcmeEnabled(h.AcmeConfigFetcher)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !acmeEnabled {
|
|
ret := Result{
|
|
Status: ResultNotApplicable,
|
|
Endpoint: h.AcmeConfigFetcher.Path,
|
|
Message: "ACME is not enabled, no additional response headers required.",
|
|
}
|
|
return []*Result{&ret}, nil
|
|
}
|
|
|
|
if h.TuneFetcher.IsSecretPermissionsError() {
|
|
msg := "Without access to mount tune information, this health check is unable to function."
|
|
return craftInsufficientPermissionResult(e, h.TuneFetcher.Path, msg), nil
|
|
}
|
|
|
|
resp, err := StringList(h.TuneData["allowed_response_headers"])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse value from server for allowed_response_headers: %w", err)
|
|
}
|
|
|
|
requiredResponseHeaders := []string{"Replay-Nonce", "Link", "Location"}
|
|
foundResponseHeaders := []string{}
|
|
for _, param := range resp {
|
|
for _, reqHeader := range requiredResponseHeaders {
|
|
if strings.EqualFold(param, reqHeader) {
|
|
foundResponseHeaders = append(foundResponseHeaders, reqHeader)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
foundAllHeaders := strutil.EquivalentSlices(requiredResponseHeaders, foundResponseHeaders)
|
|
|
|
if !foundAllHeaders {
|
|
ret := Result{
|
|
Status: ResultWarning,
|
|
Endpoint: "/sys/mounts/{{mount}}/tune",
|
|
Message: "Mount hasn't enabled 'Replay-Nonce', 'Link', 'Location' response headers, these are required for ACME to function.",
|
|
}
|
|
return []*Result{&ret}, nil
|
|
}
|
|
|
|
ret := Result{
|
|
Status: ResultOK,
|
|
Endpoint: "/sys/mounts/{{mount}}/tune",
|
|
Message: "Mount has enabled 'Replay-Nonce', 'Link', 'Location' response headers.",
|
|
}
|
|
return []*Result{&ret}, nil
|
|
}
|
|
|
|
func craftInsufficientPermissionResult(e *Executor, path, errorMsg string) []*Result {
|
|
ret := Result{
|
|
Status: ResultInsufficientPermissions,
|
|
Endpoint: path,
|
|
Message: errorMsg,
|
|
}
|
|
|
|
if e.Client.Token() == "" {
|
|
ret.Message = "No token available so unable read the tune endpoint for this mount. " + ret.Message
|
|
} else {
|
|
ret.Message = "This token lacks permission to read the tune endpoint for this mount. " + ret.Message
|
|
}
|
|
|
|
return []*Result{&ret}
|
|
}
|