mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-20 22:21:09 +02:00
* Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License. Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUS-1.1 * Fix test that expected exact offset on hcl file --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com> Co-authored-by: Sarah Thompson <sthompson@hashicorp.com> Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
293 lines
8.2 KiB
Go
293 lines
8.2 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
/*
|
|
* The healthcheck package attempts to allow generic checks of arbitrary
|
|
* engines, while providing a common framework with some performance
|
|
* efficiencies in mind.
|
|
*
|
|
* The core of this package is the Executor context; a caller would
|
|
* provision a set of checks, an API client, and a configuration,
|
|
* which the executor would use to decide which checks to execute
|
|
* and how.
|
|
*
|
|
* Checks are based around a series of remote paths that are fetched by
|
|
* the client; these are broken into two categories: static paths, which
|
|
* can always be fetched; and dynamic paths, which the check fetches based
|
|
* on earlier results.
|
|
*
|
|
* For instance, a basic PKI CA lifetime check will have static fetch against
|
|
* the list of CAs, and a dynamic fetch, using that earlier list, to fetch the
|
|
* PEMs of all CAs.
|
|
*
|
|
* This allows health checks to share data: many PKI checks will need the
|
|
* issuer list and so repeatedly fetching this may result in a performance
|
|
* impact.
|
|
*/
|
|
|
|
package healthcheck
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/vault/api"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
type Executor struct {
|
|
Client *api.Client
|
|
Mount string
|
|
DefaultEnabled bool
|
|
|
|
Config map[string]map[string]interface{}
|
|
|
|
Resources map[string]map[logical.Operation]*PathFetch
|
|
|
|
Checkers []Check
|
|
}
|
|
|
|
func NewExecutor(client *api.Client, mount string) *Executor {
|
|
return &Executor{
|
|
Client: client,
|
|
DefaultEnabled: true,
|
|
Mount: mount,
|
|
Config: make(map[string]map[string]interface{}),
|
|
Resources: make(map[string]map[logical.Operation]*PathFetch),
|
|
}
|
|
}
|
|
|
|
func (e *Executor) AddCheck(c Check) {
|
|
e.Checkers = append(e.Checkers, c)
|
|
}
|
|
|
|
func (e *Executor) BuildConfig(external map[string]interface{}) error {
|
|
merged := e.Config
|
|
|
|
for index, checker := range e.Checkers {
|
|
name := checker.Name()
|
|
if _, present := merged[name]; name == "" || present {
|
|
return fmt.Errorf("bad checker %v: name is empty or already present: %v", index, name)
|
|
}
|
|
|
|
// Fetch the default configuration; if the check returns enabled
|
|
// status, verify it matches our expectations (in the event it should
|
|
// be disabled by default), otherwise, add it in.
|
|
config := checker.DefaultConfig()
|
|
enabled, present := config["enabled"]
|
|
if !present {
|
|
config["enabled"] = e.DefaultEnabled
|
|
} else if enabled.(bool) && !e.DefaultEnabled {
|
|
config["enabled"] = e.DefaultEnabled
|
|
}
|
|
|
|
// Now apply any external config for this check.
|
|
if econfig, present := external[name]; present {
|
|
for param, evalue := range econfig.(map[string]interface{}) {
|
|
if _, ok := config[param]; !ok {
|
|
// Assumption: default configs have all possible
|
|
// configuration options. This external config has
|
|
// an unknown option, so we want to error out.
|
|
return fmt.Errorf("unknown configuration option for %v: %v", name, param)
|
|
}
|
|
|
|
config[param] = evalue
|
|
}
|
|
}
|
|
|
|
// Now apply it and save it.
|
|
if err := checker.LoadConfig(config); err != nil {
|
|
return fmt.Errorf("error saving merged config for %v: %w", name, err)
|
|
}
|
|
merged[name] = config
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *Executor) Execute() (map[string][]*Result, error) {
|
|
ret := make(map[string][]*Result)
|
|
for _, checker := range e.Checkers {
|
|
if !checker.IsEnabled() {
|
|
continue
|
|
}
|
|
|
|
if err := checker.FetchResources(e); err != nil {
|
|
return nil, fmt.Errorf("failed to fetch resources %v: %w", checker.Name(), err)
|
|
}
|
|
|
|
results, err := checker.Evaluate(e)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to evaluate %v: %w", checker.Name(), err)
|
|
}
|
|
|
|
if results == nil {
|
|
results = []*Result{}
|
|
}
|
|
|
|
for _, result := range results {
|
|
result.Endpoint = e.templatePath(result.Endpoint)
|
|
result.StatusDisplay = ResultStatusNameMap[result.Status]
|
|
}
|
|
|
|
ret[checker.Name()] = results
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (e *Executor) templatePath(path string) string {
|
|
return strings.ReplaceAll(path, "{{mount}}", e.Mount)
|
|
}
|
|
|
|
func (e *Executor) FetchIfNotFetched(op logical.Operation, rawPath string) (*PathFetch, error) {
|
|
path := e.templatePath(rawPath)
|
|
|
|
byOp, present := e.Resources[path]
|
|
if present && byOp != nil {
|
|
result, present := byOp[op]
|
|
if present && result != nil {
|
|
return result, result.FetchSurfaceError()
|
|
}
|
|
}
|
|
|
|
// Must not exist in cache; create it.
|
|
if byOp == nil {
|
|
e.Resources[path] = make(map[logical.Operation]*PathFetch)
|
|
}
|
|
|
|
ret := &PathFetch{
|
|
Operation: op,
|
|
Path: path,
|
|
ParsedCache: make(map[string]interface{}),
|
|
}
|
|
|
|
data := map[string][]string{}
|
|
if op == logical.ListOperation {
|
|
data["list"] = []string{"true"}
|
|
} else if op != logical.ReadOperation {
|
|
return nil, fmt.Errorf("unknown operation: %v on %v", op, path)
|
|
}
|
|
|
|
// client.ReadRaw* methods require a manual timeout override
|
|
ctx, cancel := context.WithTimeout(context.Background(), e.Client.ClientTimeout())
|
|
defer cancel()
|
|
|
|
response, err := e.Client.Logical().ReadRawWithDataWithContext(ctx, path, data)
|
|
ret.Response = response
|
|
if err != nil {
|
|
ret.FetchError = err
|
|
} else {
|
|
// Not all secrets will parse correctly. Sometimes we really want
|
|
// to fetch a raw endpoint, sometimes we're run with a bad mount
|
|
// or missing permissions.
|
|
secret, secretErr := e.Client.Logical().ParseRawResponseAndCloseBody(response, err)
|
|
if secretErr != nil {
|
|
ret.SecretParseError = secretErr
|
|
} else {
|
|
ret.Secret = secret
|
|
}
|
|
}
|
|
|
|
e.Resources[path][op] = ret
|
|
return ret, ret.FetchSurfaceError()
|
|
}
|
|
|
|
type PathFetch struct {
|
|
Operation logical.Operation
|
|
Path string
|
|
Response *api.Response
|
|
FetchError error
|
|
Secret *api.Secret
|
|
SecretParseError error
|
|
ParsedCache map[string]interface{}
|
|
}
|
|
|
|
func (p *PathFetch) IsOK() bool {
|
|
return p.FetchError == nil && p.Response != nil
|
|
}
|
|
|
|
func (p *PathFetch) IsSecretOK() bool {
|
|
return p.IsOK() && p.SecretParseError == nil && p.Secret != nil
|
|
}
|
|
|
|
func (p *PathFetch) FetchSurfaceError() error {
|
|
if p.IsOK() || p.IsSecretPermissionsError() || p.IsUnsupportedPathError() || p.IsMissingResource() || p.Is404NotFound() {
|
|
return nil
|
|
}
|
|
|
|
if strings.Contains(p.FetchError.Error(), "route entry not found") {
|
|
return fmt.Errorf("Error making API request: was a bad mount given?\n\nOperation: %v\nPath: %v\nOriginal Error:\n%w", p.Operation, p.Path, p.FetchError)
|
|
}
|
|
|
|
return p.FetchError
|
|
}
|
|
|
|
func (p *PathFetch) IsSecretPermissionsError() bool {
|
|
return !p.IsOK() && strings.Contains(p.FetchError.Error(), "permission denied")
|
|
}
|
|
|
|
func (p *PathFetch) IsUnsupportedPathError() bool {
|
|
return !p.IsOK() && strings.Contains(p.FetchError.Error(), "unsupported path")
|
|
}
|
|
|
|
func (p *PathFetch) IsMissingResource() bool {
|
|
return !p.IsOK() && strings.Contains(p.FetchError.Error(), "unable to find")
|
|
}
|
|
|
|
func (p *PathFetch) Is404NotFound() bool {
|
|
return !p.IsOK() && strings.HasSuffix(strings.TrimSpace(p.FetchError.Error()), "Code: 404. Errors:")
|
|
}
|
|
|
|
type Check interface {
|
|
Name() string
|
|
IsEnabled() bool
|
|
|
|
DefaultConfig() map[string]interface{}
|
|
LoadConfig(config map[string]interface{}) error
|
|
|
|
FetchResources(e *Executor) error
|
|
|
|
Evaluate(e *Executor) ([]*Result, error)
|
|
}
|
|
|
|
type ResultStatus int
|
|
|
|
const (
|
|
ResultNotApplicable ResultStatus = iota
|
|
ResultOK
|
|
ResultInformational
|
|
ResultWarning
|
|
ResultCritical
|
|
ResultInvalidVersion
|
|
ResultInsufficientPermissions
|
|
)
|
|
|
|
var ResultStatusNameMap = map[ResultStatus]string{
|
|
ResultNotApplicable: "not_applicable",
|
|
ResultOK: "ok",
|
|
ResultInformational: "informational",
|
|
ResultWarning: "warning",
|
|
ResultCritical: "critical",
|
|
ResultInvalidVersion: "invalid_version",
|
|
ResultInsufficientPermissions: "insufficient_permissions",
|
|
}
|
|
|
|
var NameResultStatusMap = map[string]ResultStatus{
|
|
"not_applicable": ResultNotApplicable,
|
|
"ok": ResultOK,
|
|
"informational": ResultInformational,
|
|
"warning": ResultWarning,
|
|
"critical": ResultCritical,
|
|
"invalid_version": ResultInvalidVersion,
|
|
"insufficient_permissions": ResultInsufficientPermissions,
|
|
}
|
|
|
|
type Result struct {
|
|
Status ResultStatus `json:"status_code"`
|
|
StatusDisplay string `json:"status"`
|
|
Endpoint string `json:"endpoint,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|