Steven Clark bc42d56c7a
Seal-HA: Match multiple seals using name/type only (#23203)
* Match multiple seals using name/type only

 - This fix addresses an issue that changing any seal configuration in an existing seal stanza such as the Vault token would cause negate the seal matching.
 - If this was the only seal that was previously used or slight tweaks happened to all the seals Vault would fail to start with an error of

 "must have at least one seal in common with the old generation."

 - Also add a little more output to the validation error messages about
   the current seal and configured seal information to help in
   diagnosing errors in the future

* Tweak formatting and text on method doc

* Update comment around forcing a seal rewrap
2023-09-20 16:51:52 -04:00

494 lines
15 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package configutil
import (
"context"
"crypto/rand"
"errors"
"fmt"
"io"
"os"
"regexp"
"strings"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-kms-wrapping/entropy/v2"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
aeadwrapper "github.com/hashicorp/go-kms-wrapping/wrappers/aead/v2"
"github.com/hashicorp/go-kms-wrapping/wrappers/alicloudkms/v2"
"github.com/hashicorp/go-kms-wrapping/wrappers/awskms/v2"
"github.com/hashicorp/go-kms-wrapping/wrappers/azurekeyvault/v2"
"github.com/hashicorp/go-kms-wrapping/wrappers/gcpckms/v2"
"github.com/hashicorp/go-kms-wrapping/wrappers/ocikms/v2"
"github.com/hashicorp/go-kms-wrapping/wrappers/transit/v2"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-secure-stdlib/parseutil"
"github.com/hashicorp/hcl"
"github.com/hashicorp/hcl/hcl/ast"
"github.com/hashicorp/vault/sdk/helper/strutil"
"github.com/hashicorp/vault/sdk/logical"
)
var (
ConfigureWrapper = configureWrapper
CreateSecureRandomReaderFunc = createSecureRandomReader
GetEnvConfigFunc = getEnvConfig
)
// Entropy contains Entropy configuration for the server
type EntropyMode int
const (
EntropyUnknown EntropyMode = iota
EntropyAugmentation
KmsRenameDisabledSuffix = "-disabled"
)
type Entropy struct {
Mode EntropyMode
SealName string
}
type EntropySourcerInfo struct {
Sourcer entropy.Sourcer
Name string
}
// KMS contains KMS configuration for the server
type KMS struct {
UnusedKeys []string `hcl:",unusedKeys"`
Type string
// Purpose can be used to allow a string-based specification of what this
// KMS is designated for, in situations where we want to allow more than
// one KMS to be specified
Purpose []string `hcl:"-"`
Disabled bool
Config map[string]string
Priority int `hcl:"priority"`
Name string `hcl:"name"`
}
func (k *KMS) GoString() string {
return fmt.Sprintf("*%#v", *k)
}
func parseKMS(result *[]*KMS, list *ast.ObjectList, blockName string, maxKMS int) error {
if len(list.Items) > maxKMS {
return fmt.Errorf("only %d or less %q blocks are permitted", maxKMS, blockName)
}
seals := make([]*KMS, 0, len(list.Items))
for _, item := range list.Items {
key := blockName
if len(item.Keys) > 0 {
key = item.Keys[0].Token.Value().(string)
}
// We first decode into a map[string]interface{} because purpose isn't
// necessarily a string. Then we migrate everything else over to
// map[string]string and error if it doesn't work.
var m map[string]interface{}
if err := hcl.DecodeObject(&m, item.Val); err != nil {
return multierror.Prefix(err, fmt.Sprintf("%s.%s:", blockName, key))
}
var purpose []string
var err error
if v, ok := m["purpose"]; ok {
if purpose, err = parseutil.ParseCommaStringSlice(v); err != nil {
return multierror.Prefix(fmt.Errorf("unable to parse 'purpose' in kms type %q: %w", key, err), fmt.Sprintf("%s.%s:", blockName, key))
}
for i, p := range purpose {
purpose[i] = strings.ToLower(p)
}
delete(m, "purpose")
}
var disabled bool
if v, ok := m["disabled"]; ok {
disabled, err = parseutil.ParseBool(v)
if err != nil {
return multierror.Prefix(err, fmt.Sprintf("%s.%s:", blockName, key))
}
delete(m, "disabled")
}
var priority int
if v, ok := m["priority"]; ok {
priority, err = parseutil.SafeParseInt(v)
if err != nil {
return multierror.Prefix(fmt.Errorf("unable to parse 'priority' in kms type %q: %w", key, err), fmt.Sprintf("%s.%s", blockName, key))
}
delete(m, "priority")
if priority < 1 {
return multierror.Prefix(fmt.Errorf("invalid priority in kms type %q: %d", key, priority), fmt.Sprintf("%s.%s", blockName, key))
}
}
name := strings.ToLower(key)
// ensure that seals of the same type will have unique names for seal migration
if disabled {
name += KmsRenameDisabledSuffix
}
if v, ok := m["name"]; ok {
name, ok = v.(string)
if !ok {
return multierror.Prefix(fmt.Errorf("unable to parse 'name' in kms type %q: unexpected type %T", key, v), fmt.Sprintf("%s.%s", blockName, key))
}
delete(m, "name")
if !regexp.MustCompile("^[a-zA-Z0-9-_]+$").MatchString(name) {
return multierror.Prefix(errors.New("'name' field can only include alphanumeric characters, hyphens, and underscores"), fmt.Sprintf("%s.%s", blockName, key))
}
}
strMap := make(map[string]string, len(m))
for k, v := range m {
s, err := parseutil.ParseString(v)
if err != nil {
return multierror.Prefix(err, fmt.Sprintf("%s.%s:", blockName, key))
}
strMap[k] = s
}
seal := &KMS{
Type: strings.ToLower(key),
Purpose: purpose,
Disabled: disabled,
Priority: priority,
Name: name,
}
if len(strMap) > 0 {
seal.Config = strMap
}
seals = append(seals, seal)
}
*result = append(*result, seals...)
return nil
}
func ParseKMSes(d string) ([]*KMS, error) {
// Parse!
obj, err := hcl.Parse(d)
if err != nil {
return nil, err
}
// Start building the result
var result struct {
Seals []*KMS `hcl:"-"`
}
if err := hcl.DecodeObject(&result, obj); err != nil {
return nil, err
}
list, ok := obj.Node.(*ast.ObjectList)
if !ok {
return nil, fmt.Errorf("error parsing: file doesn't contain a root object")
}
if o := list.Filter("seal"); len(o.Items) > 0 {
if err := parseKMS(&result.Seals, o, "seal", 3); err != nil {
return nil, fmt.Errorf("error parsing 'seal': %w", err)
}
}
if o := list.Filter("kms"); len(o.Items) > 0 {
if err := parseKMS(&result.Seals, o, "kms", 3); err != nil {
return nil, fmt.Errorf("error parsing 'kms': %w", err)
}
}
return result.Seals, nil
}
func configureWrapper(configKMS *KMS, infoKeys *[]string, info *map[string]string, logger hclog.Logger, opts ...wrapping.Option) (wrapping.Wrapper, error) {
var wrapper wrapping.Wrapper
var kmsInfo map[string]string
var err error
envConfig := GetEnvConfigFunc(configKMS)
if len(envConfig) > 0 && configKMS.Config == nil {
configKMS.Config = make(map[string]string)
}
// transit is a special case, because some config values take precedence over env vars
if configKMS.Type == wrapping.WrapperTypeTransit.String() {
mergeTransitConfig(configKMS.Config, envConfig)
} else {
for name, val := range envConfig {
configKMS.Config[name] = val
}
}
switch wrapping.WrapperType(configKMS.Type) {
case wrapping.WrapperTypeShamir:
return nil, nil
case wrapping.WrapperTypeAead:
wrapper, kmsInfo, err = GetAEADKMSFunc(configKMS, opts...)
case wrapping.WrapperTypeAliCloudKms:
wrapper, kmsInfo, err = GetAliCloudKMSFunc(configKMS, opts...)
case wrapping.WrapperTypeAwsKms:
wrapper, kmsInfo, err = GetAWSKMSFunc(configKMS, opts...)
case wrapping.WrapperTypeAzureKeyVault:
wrapper, kmsInfo, err = GetAzureKeyVaultKMSFunc(configKMS, opts...)
case wrapping.WrapperTypeGcpCkms:
wrapper, kmsInfo, err = GetGCPCKMSKMSFunc(configKMS, opts...)
case wrapping.WrapperTypeOciKms:
if keyId, ok := configKMS.Config["key_id"]; ok {
opts = append(opts, wrapping.WithKeyId(keyId))
}
wrapper, kmsInfo, err = GetOCIKMSKMSFunc(configKMS, opts...)
case wrapping.WrapperTypeTransit:
wrapper, kmsInfo, err = GetTransitKMSFunc(configKMS, opts...)
case wrapping.WrapperTypePkcs11:
return nil, fmt.Errorf("KMS type 'pkcs11' requires the Vault Enterprise HSM binary")
default:
return nil, fmt.Errorf("Unknown KMS type %q", configKMS.Type)
}
if err != nil {
return nil, err
}
if infoKeys != nil && info != nil {
for k, v := range kmsInfo {
*infoKeys = append(*infoKeys, k)
(*info)[k] = v
}
}
return wrapper, nil
}
func GetAEADKMSFunc(kms *KMS, opts ...wrapping.Option) (wrapping.Wrapper, map[string]string, error) {
wrapper := aeadwrapper.NewWrapper()
wrapperInfo, err := wrapper.SetConfig(context.Background(), append(opts, wrapping.WithConfigMap(kms.Config))...)
if err != nil {
return nil, nil, err
}
info := make(map[string]string)
if wrapperInfo != nil {
str := "AEAD Type"
if len(kms.Purpose) > 0 {
str = fmt.Sprintf("%v %s", kms.Purpose, str)
}
info[str] = wrapperInfo.Metadata["aead_type"]
}
return wrapper, info, nil
}
func GetAliCloudKMSFunc(kms *KMS, opts ...wrapping.Option) (wrapping.Wrapper, map[string]string, error) {
wrapper := alicloudkms.NewWrapper()
wrapperInfo, err := wrapper.SetConfig(context.Background(), append(opts, wrapping.WithDisallowEnvVars(true), wrapping.WithConfigMap(kms.Config))...)
if err != nil {
// If the error is any other than logical.KeyNotFoundError, return the error
if !errwrap.ContainsType(err, new(logical.KeyNotFoundError)) {
return nil, nil, err
}
}
info := make(map[string]string)
if wrapperInfo != nil {
info["AliCloud KMS Region"] = wrapperInfo.Metadata["region"]
info["AliCloud KMS KeyID"] = wrapperInfo.Metadata["kms_key_id"]
if domain, ok := wrapperInfo.Metadata["domain"]; ok {
info["AliCloud KMS Domain"] = domain
}
}
return wrapper, info, nil
}
var GetAWSKMSFunc = func(kms *KMS, opts ...wrapping.Option) (wrapping.Wrapper, map[string]string, error) {
wrapper := awskms.NewWrapper()
wrapperInfo, err := wrapper.SetConfig(context.Background(), append(opts, wrapping.WithDisallowEnvVars(true), wrapping.WithConfigMap(kms.Config))...)
if err != nil {
// If the error is any other than logical.KeyNotFoundError, return the error
if !errwrap.ContainsType(err, new(logical.KeyNotFoundError)) {
return nil, nil, err
}
}
info := make(map[string]string)
if wrapperInfo != nil {
info["AWS KMS Region"] = wrapperInfo.Metadata["region"]
info["AWS KMS KeyID"] = wrapperInfo.Metadata["kms_key_id"]
if endpoint, ok := wrapperInfo.Metadata["endpoint"]; ok {
info["AWS KMS Endpoint"] = endpoint
}
}
return wrapper, info, nil
}
func GetAzureKeyVaultKMSFunc(kms *KMS, opts ...wrapping.Option) (wrapping.Wrapper, map[string]string, error) {
wrapper := azurekeyvault.NewWrapper()
wrapperInfo, err := wrapper.SetConfig(context.Background(), append(opts, wrapping.WithDisallowEnvVars(true), wrapping.WithConfigMap(kms.Config))...)
if err != nil {
// If the error is any other than logical.KeyNotFoundError, return the error
if !errwrap.ContainsType(err, new(logical.KeyNotFoundError)) {
return nil, nil, err
}
}
info := make(map[string]string)
if wrapperInfo != nil {
info["Azure Environment"] = wrapperInfo.Metadata["environment"]
info["Azure Vault Name"] = wrapperInfo.Metadata["vault_name"]
info["Azure Key Name"] = wrapperInfo.Metadata["key_name"]
}
return wrapper, info, nil
}
func GetGCPCKMSKMSFunc(kms *KMS, opts ...wrapping.Option) (wrapping.Wrapper, map[string]string, error) {
wrapper := gcpckms.NewWrapper()
wrapperInfo, err := wrapper.SetConfig(context.Background(), append(opts, wrapping.WithDisallowEnvVars(true), wrapping.WithConfigMap(kms.Config))...)
if err != nil {
// If the error is any other than logical.KeyNotFoundError, return the error
if !errwrap.ContainsType(err, new(logical.KeyNotFoundError)) {
return nil, nil, err
}
}
info := make(map[string]string)
if wrapperInfo != nil {
info["GCP KMS Project"] = wrapperInfo.Metadata["project"]
info["GCP KMS Region"] = wrapperInfo.Metadata["region"]
info["GCP KMS Key Ring"] = wrapperInfo.Metadata["key_ring"]
info["GCP KMS Crypto Key"] = wrapperInfo.Metadata["crypto_key"]
}
return wrapper, info, nil
}
func GetOCIKMSKMSFunc(kms *KMS, opts ...wrapping.Option) (wrapping.Wrapper, map[string]string, error) {
wrapper := ocikms.NewWrapper()
wrapperInfo, err := wrapper.SetConfig(context.Background(), append(opts, wrapping.WithDisallowEnvVars(true), wrapping.WithConfigMap(kms.Config))...)
if err != nil {
return nil, nil, err
}
info := make(map[string]string)
if wrapperInfo != nil {
info["OCI KMS KeyID"] = wrapperInfo.Metadata[ocikms.KmsConfigKeyId]
info["OCI KMS Crypto Endpoint"] = wrapperInfo.Metadata[ocikms.KmsConfigCryptoEndpoint]
info["OCI KMS Management Endpoint"] = wrapperInfo.Metadata[ocikms.KmsConfigManagementEndpoint]
info["OCI KMS Principal Type"] = wrapperInfo.Metadata["principal_type"]
}
return wrapper, info, nil
}
var GetTransitKMSFunc = func(kms *KMS, opts ...wrapping.Option) (wrapping.Wrapper, map[string]string, error) {
wrapper := transit.NewWrapper()
var prefix string
if p, ok := kms.Config["key_id_prefix"]; ok {
prefix = p
} else {
prefix = kms.Name
}
if !strings.HasSuffix(prefix, "/") {
prefix = prefix + "/"
}
wrapperInfo, err := wrapper.SetConfig(context.Background(), append(opts, wrapping.WithDisallowEnvVars(true), wrapping.WithConfigMap(kms.Config),
transit.WithKeyIdPrefix(prefix))...)
if err != nil {
// If the error is any other than logical.KeyNotFoundError, return the error
if !errwrap.ContainsType(err, new(logical.KeyNotFoundError)) {
return nil, nil, err
}
}
info := make(map[string]string)
if wrapperInfo != nil {
info["Transit Address"] = wrapperInfo.Metadata["address"]
info["Transit Mount Path"] = wrapperInfo.Metadata["mount_path"]
info["Transit Key Name"] = wrapperInfo.Metadata["key_name"]
if namespace, ok := wrapperInfo.Metadata["namespace"]; ok {
info["Transit Namespace"] = namespace
}
}
return wrapper, info, nil
}
func createSecureRandomReader(_ *SharedConfig, _ []*EntropySourcerInfo, _ hclog.Logger) (io.Reader, error) {
return rand.Reader, nil
}
func getEnvConfig(kms *KMS) map[string]string {
envValues := make(map[string]string)
var wrapperEnvVars map[string]string
switch wrapping.WrapperType(kms.Type) {
case wrapping.WrapperTypeAliCloudKms:
wrapperEnvVars = AliCloudKMSEnvVars
case wrapping.WrapperTypeAwsKms:
wrapperEnvVars = AWSKMSEnvVars
case wrapping.WrapperTypeAzureKeyVault:
wrapperEnvVars = AzureEnvVars
case wrapping.WrapperTypeGcpCkms:
wrapperEnvVars = GCPCKMSEnvVars
case wrapping.WrapperTypeOciKms:
wrapperEnvVars = OCIKMSEnvVars
case wrapping.WrapperTypeTransit:
wrapperEnvVars = TransitEnvVars
default:
return nil
}
for envVar, configName := range wrapperEnvVars {
val := os.Getenv(envVar)
if val != "" {
envValues[configName] = val
}
}
return envValues
}
func mergeTransitConfig(config map[string]string, envConfig map[string]string) {
useFileTlsConfig := false
for _, varName := range TransitTLSConfigVars {
if _, ok := config[varName]; ok {
useFileTlsConfig = true
break
}
}
if useFileTlsConfig {
for _, varName := range TransitTLSConfigVars {
delete(envConfig, varName)
}
}
for varName, val := range envConfig {
// for some values, file config takes precedence
if strutil.StrListContains(TransitPrioritizeConfigValues, varName) && config[varName] != "" {
continue
}
config[varName] = val
}
}
func (k *KMS) Clone() *KMS {
ret := &KMS{
UnusedKeys: k.UnusedKeys,
Type: k.Type,
Purpose: k.Purpose,
Config: k.Config,
Name: k.Name,
Disabled: k.Disabled,
Priority: k.Priority,
}
return ret
}