25960fd034
* wip * wip * Got it 'working', but not happy about cleanliness yet * Switch to a dedicated defaultSeal with recovery keys This is simpler than trying to hijack SealAccess as before. Instead, if the operator has requested recovery unseal mode (via a flag in the seal stanza), we new up a shamir seal with the recovery unseal key path instead of the auto seal. Then everything proceeds as if you had a shamir seal to begin with. * Handle recovery rekeying * changelog * Revert go.mod redirect * revert multi-blob info * Dumb nil unmarshal target * More comments * Update vault/seal.go Co-authored-by: Nick Cabatoff <ncabatoff@hashicorp.com> * Update changelog/18683.txt Co-authored-by: Nick Cabatoff <ncabatoff@hashicorp.com> * pr feedback * Fix recovery rekey, which needs to fetch root keys and restore them under the new recovery split * Better comment on recovery seal during adjustSealMigration * Make it possible to migrate from an auto-seal in recovery mode to shamir * Fix sealMigrated to account for a recovery seal * comments * Update changelog/18683.txt Co-authored-by: Nick Cabatoff <ncabatoff@hashicorp.com> * Address PR feedback * Refactor duplicated migration code into helpers, using UnsealRecoveryKey/RecoveryKey where appropriate * Don't shortcut the reast of seal migration * get rid of redundant transit server cleanup Co-authored-by: Nick Cabatoff <ncabatoff@hashicorp.com>
504 lines
15 KiB
Go
504 lines
15 KiB
Go
package vault
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"sync/atomic"
|
|
|
|
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
|
|
"github.com/hashicorp/vault/physical/raft"
|
|
"github.com/hashicorp/vault/vault/seal"
|
|
|
|
aeadwrapper "github.com/hashicorp/go-kms-wrapping/wrappers/aead/v2"
|
|
"github.com/hashicorp/vault/helper/namespace"
|
|
"github.com/hashicorp/vault/helper/pgpkeys"
|
|
"github.com/hashicorp/vault/shamir"
|
|
)
|
|
|
|
// InitParams keeps the init function from being littered with too many
|
|
// params, that's it!
|
|
type InitParams struct {
|
|
BarrierConfig *SealConfig
|
|
RecoveryConfig *SealConfig
|
|
RootTokenPGPKey string
|
|
// LegacyShamirSeal should only be used in test code, we don't want to
|
|
// give the user a way to create legacy shamir seals.
|
|
LegacyShamirSeal bool
|
|
}
|
|
|
|
// InitResult is used to provide the key parts back after
|
|
// they are generated as part of the initialization.
|
|
type InitResult struct {
|
|
SecretShares [][]byte
|
|
RecoveryShares [][]byte
|
|
RootToken string
|
|
}
|
|
|
|
var (
|
|
initPTFunc = func(c *Core) func() { return nil }
|
|
initInProgress uint32
|
|
ErrInitWithoutAutoloading = errors.New("cannot initialize storage without an autoloaded license")
|
|
)
|
|
|
|
func (c *Core) InitializeRecovery(ctx context.Context) error {
|
|
if !c.recoveryMode {
|
|
return nil
|
|
}
|
|
|
|
raftStorage, ok := c.underlyingPhysical.(*raft.RaftBackend)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
parsedClusterAddr, err := url.Parse(c.ClusterAddr())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.postRecoveryUnsealFuncs = append(c.postRecoveryUnsealFuncs, func() error {
|
|
return raftStorage.StartRecoveryCluster(context.Background(), raft.Peer{
|
|
ID: raftStorage.NodeID(),
|
|
Address: parsedClusterAddr.Host,
|
|
})
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// Initialized checks if the Vault is already initialized. This means one of
|
|
// two things: either the barrier has been created (with keyring and master key)
|
|
// and the seal config written to storage, or Raft is forming a cluster and a
|
|
// join/bootstrap is in progress.
|
|
func (c *Core) Initialized(ctx context.Context) (bool, error) {
|
|
// Check the barrier first
|
|
init, err := c.InitializedLocally(ctx)
|
|
if err != nil || init {
|
|
return init, err
|
|
}
|
|
|
|
if c.isRaftUnseal() {
|
|
return true, nil
|
|
}
|
|
|
|
rb := c.getRaftBackend()
|
|
if rb != nil && rb.Initialized() {
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// InitializedLocally checks if the Vault is already initialized from the
|
|
// local node's perspective. This is the same thing as Initialized, unless
|
|
// using Raft, in which case Initialized may return true (because a peer
|
|
// we're joining to has been initialized) while InitializedLocally returns
|
|
// false (because we're not done bootstrapping raft on the local node).
|
|
func (c *Core) InitializedLocally(ctx context.Context) (bool, error) {
|
|
// Check the barrier first
|
|
init, err := c.barrier.Initialized(ctx)
|
|
if err != nil {
|
|
c.logger.Error("barrier init check failed", "error", err)
|
|
return false, err
|
|
}
|
|
if !init {
|
|
c.logger.Info("security barrier not initialized")
|
|
return false, nil
|
|
}
|
|
|
|
// Verify the seal configuration
|
|
sealConf, err := c.seal.BarrierConfig(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if sealConf == nil {
|
|
return false, fmt.Errorf("core: barrier reports initialized but no seal configuration found")
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (c *Core) generateShares(sc *SealConfig) ([]byte, [][]byte, error) {
|
|
// Generate a root key
|
|
rootKey, err := c.barrier.GenerateKey(c.secureRandomReader)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("key generation failed: %w", err)
|
|
}
|
|
|
|
// Return the root key if only a single key part is used
|
|
var unsealKeys [][]byte
|
|
if sc.SecretShares == 1 {
|
|
unsealKeys = append(unsealKeys, rootKey)
|
|
} else {
|
|
// Split the root key using the Shamir algorithm
|
|
shares, err := shamir.Split(rootKey, sc.SecretShares, sc.SecretThreshold)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to generate barrier shares: %w", err)
|
|
}
|
|
unsealKeys = shares
|
|
}
|
|
|
|
// If we have PGP keys, perform the encryption
|
|
if len(sc.PGPKeys) > 0 {
|
|
hexEncodedShares := make([][]byte, len(unsealKeys))
|
|
for i := range unsealKeys {
|
|
hexEncodedShares[i] = []byte(hex.EncodeToString(unsealKeys[i]))
|
|
}
|
|
_, encryptedShares, err := pgpkeys.EncryptShares(hexEncodedShares, sc.PGPKeys)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
unsealKeys = encryptedShares
|
|
}
|
|
|
|
return rootKey, unsealKeys, nil
|
|
}
|
|
|
|
// Initialize is used to initialize the Vault with the given
|
|
// configurations.
|
|
func (c *Core) Initialize(ctx context.Context, initParams *InitParams) (*InitResult, error) {
|
|
if err := LicenseInitCheck(c); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
atomic.StoreUint32(&initInProgress, 1)
|
|
defer atomic.StoreUint32(&initInProgress, 0)
|
|
barrierConfig := initParams.BarrierConfig
|
|
recoveryConfig := initParams.RecoveryConfig
|
|
|
|
// N.B. Although the core is capable of handling situations where some keys
|
|
// are stored and some aren't, in practice, replication + HSMs makes this
|
|
// extremely hard to reason about, to the point that it will probably never
|
|
// be supported. The reason is that each HSM needs to encode the root key
|
|
// separately, which means the shares must be generated independently,
|
|
// which means both that the shares will be different *AND* there would
|
|
// need to be a way to actually allow fetching of the generated keys by
|
|
// operators.
|
|
if c.SealAccess().StoredKeysSupported() == seal.StoredKeysSupportedGeneric {
|
|
if len(barrierConfig.PGPKeys) > 0 {
|
|
return nil, fmt.Errorf("PGP keys not supported when storing shares")
|
|
}
|
|
barrierConfig.SecretShares = 1
|
|
barrierConfig.SecretThreshold = 1
|
|
if barrierConfig.StoredShares != 1 {
|
|
c.Logger().Warn("stored keys supported on init, forcing shares/threshold to 1")
|
|
}
|
|
}
|
|
|
|
if initParams.LegacyShamirSeal {
|
|
barrierConfig.StoredShares = 0
|
|
} else {
|
|
barrierConfig.StoredShares = 1
|
|
}
|
|
|
|
if len(barrierConfig.PGPKeys) > 0 && len(barrierConfig.PGPKeys) != barrierConfig.SecretShares {
|
|
return nil, fmt.Errorf("incorrect number of PGP keys")
|
|
}
|
|
|
|
if c.SealAccess().RecoveryKeySupported() {
|
|
if len(recoveryConfig.PGPKeys) > 0 && len(recoveryConfig.PGPKeys) != recoveryConfig.SecretShares {
|
|
return nil, fmt.Errorf("incorrect number of PGP keys for recovery")
|
|
}
|
|
}
|
|
|
|
if c.seal.RecoveryKeySupported() {
|
|
if recoveryConfig == nil {
|
|
return nil, fmt.Errorf("recovery configuration must be supplied")
|
|
}
|
|
|
|
if recoveryConfig.SecretShares < 1 {
|
|
return nil, fmt.Errorf("recovery configuration must specify a positive number of shares")
|
|
}
|
|
|
|
// Check if the seal configuration is valid
|
|
if err := recoveryConfig.Validate(); err != nil {
|
|
c.logger.Error("invalid recovery configuration", "error", err)
|
|
return nil, fmt.Errorf("invalid recovery configuration: %w", err)
|
|
}
|
|
}
|
|
|
|
// Check if the seal configuration is valid
|
|
if err := barrierConfig.Validate(); err != nil {
|
|
c.logger.Error("invalid seal configuration", "error", err)
|
|
return nil, fmt.Errorf("invalid seal configuration: %w", err)
|
|
}
|
|
|
|
// Avoid an initialization race
|
|
c.stateLock.Lock()
|
|
defer c.stateLock.Unlock()
|
|
|
|
// Check if we are initialized
|
|
init, err := c.Initialized(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if init {
|
|
return nil, ErrAlreadyInit
|
|
}
|
|
|
|
// Bootstrap the raft backend if that's provided as the physical or
|
|
// HA backend.
|
|
raftBackend := c.getRaftBackend()
|
|
if raftBackend != nil {
|
|
err := c.RaftBootstrap(ctx, true)
|
|
if err != nil {
|
|
c.logger.Error("failed to bootstrap raft", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Teardown cluster after bootstrap setup
|
|
defer func() {
|
|
if err := raftBackend.TeardownCluster(nil); err != nil {
|
|
c.logger.Error("failed to stop raft", "error", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
err = c.seal.Init(ctx)
|
|
if err != nil {
|
|
c.logger.Error("failed to initialize seal", "error", err)
|
|
return nil, fmt.Errorf("error initializing seal: %w", err)
|
|
}
|
|
|
|
initPTCleanup := initPTFunc(c)
|
|
if initPTCleanup != nil {
|
|
defer initPTCleanup()
|
|
}
|
|
|
|
barrierKey, barrierKeyShares, err := c.generateShares(barrierConfig)
|
|
if err != nil {
|
|
c.logger.Error("error generating shares", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
var sealKey []byte
|
|
var sealKeyShares [][]byte
|
|
|
|
if barrierConfig.StoredShares == 1 && c.seal.BarrierType() == wrapping.WrapperTypeShamir {
|
|
sealKey, sealKeyShares, err = c.generateShares(barrierConfig)
|
|
if err != nil {
|
|
c.logger.Error("error generating shares", "error", err)
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Initialize the barrier
|
|
if err := c.barrier.Initialize(ctx, barrierKey, sealKey, c.secureRandomReader); err != nil {
|
|
c.logger.Error("failed to initialize barrier", "error", err)
|
|
return nil, fmt.Errorf("failed to initialize barrier: %w", err)
|
|
}
|
|
if c.logger.IsInfo() {
|
|
c.logger.Info("security barrier initialized", "stored", barrierConfig.StoredShares, "shares", barrierConfig.SecretShares, "threshold", barrierConfig.SecretThreshold)
|
|
}
|
|
|
|
// Unseal the barrier
|
|
if err := c.barrier.Unseal(ctx, barrierKey); err != nil {
|
|
c.logger.Error("failed to unseal barrier", "error", err)
|
|
return nil, fmt.Errorf("failed to unseal barrier: %w", err)
|
|
}
|
|
|
|
// Ensure the barrier is re-sealed
|
|
defer func() {
|
|
// Defers are LIFO so we need to run this here too to ensure the stop
|
|
// happens before sealing. preSeal also stops, so we just make the
|
|
// stopping safe against multiple calls.
|
|
if err := c.barrier.Seal(); err != nil {
|
|
c.logger.Error("failed to seal barrier", "error", err)
|
|
}
|
|
}()
|
|
|
|
err = c.seal.SetBarrierConfig(ctx, barrierConfig)
|
|
if err != nil {
|
|
c.logger.Error("failed to save barrier configuration", "error", err)
|
|
return nil, fmt.Errorf("barrier configuration saving failed: %w", err)
|
|
}
|
|
|
|
results := &InitResult{
|
|
SecretShares: [][]byte{},
|
|
}
|
|
|
|
// If we are storing shares, pop them out of the returned results and push
|
|
// them through the seal
|
|
switch c.seal.StoredKeysSupported() {
|
|
case seal.StoredKeysSupportedShamirRoot:
|
|
keysToStore := [][]byte{barrierKey}
|
|
if err := c.seal.GetAccess().Wrapper.(*aeadwrapper.ShamirWrapper).SetAesGcmKeyBytes(sealKey); err != nil {
|
|
c.logger.Error("failed to set seal key", "error", err)
|
|
return nil, fmt.Errorf("failed to set seal key: %w", err)
|
|
}
|
|
if err := c.seal.SetStoredKeys(ctx, keysToStore); err != nil {
|
|
c.logger.Error("failed to store keys", "error", err)
|
|
return nil, fmt.Errorf("failed to store keys: %w", err)
|
|
}
|
|
results.SecretShares = sealKeyShares
|
|
case seal.StoredKeysSupportedGeneric:
|
|
keysToStore := [][]byte{barrierKey}
|
|
if err := c.seal.SetStoredKeys(ctx, keysToStore); err != nil {
|
|
c.logger.Error("failed to store keys", "error", err)
|
|
return nil, fmt.Errorf("failed to store keys: %w", err)
|
|
}
|
|
default:
|
|
// We don't support initializing an old-style Shamir seal anymore, so
|
|
// this case is only reachable by tests.
|
|
results.SecretShares = barrierKeyShares
|
|
}
|
|
|
|
// Perform initial setup
|
|
if err := c.setupCluster(ctx); err != nil {
|
|
c.logger.Error("cluster setup failed during init", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Start tracking
|
|
if initPTCleanup != nil {
|
|
initPTCleanup()
|
|
}
|
|
|
|
activeCtx, ctxCancel := context.WithCancel(namespace.RootContext(nil))
|
|
if err := c.postUnseal(activeCtx, ctxCancel, standardUnsealStrategy{}); err != nil {
|
|
c.logger.Error("post-unseal setup failed during init", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Save the configuration regardless, but only generate a key if it's not
|
|
// disabled. When using recovery keys they are stored in the barrier, so
|
|
// this must happen post-unseal.
|
|
if c.seal.RecoveryKeySupported() {
|
|
err = c.seal.SetRecoveryConfig(ctx, recoveryConfig)
|
|
if err != nil {
|
|
c.logger.Error("failed to save recovery configuration", "error", err)
|
|
return nil, fmt.Errorf("recovery configuration saving failed: %w", err)
|
|
}
|
|
|
|
if recoveryConfig.SecretShares > 0 {
|
|
recoveryKey, recoveryUnsealKeys, err := c.generateShares(recoveryConfig)
|
|
if err != nil {
|
|
c.logger.Error("failed to generate recovery shares", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
err = c.seal.SetRecoveryKey(ctx, recoveryKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
results.RecoveryShares = recoveryUnsealKeys
|
|
|
|
if !initParams.RecoveryConfig.DisableUnsealRecovery {
|
|
wrapper := aeadwrapper.NewShamirWrapper()
|
|
wrapper.SetAesGcmKeyBytes(recoveryKey)
|
|
recoverySeal := NewRecoverySeal(&seal.Access{
|
|
Wrapper: wrapper,
|
|
})
|
|
recoverySeal.SetCore(c)
|
|
if err := recoverySeal.SetStoredKeys(ctx, [][]byte{barrierKey}); err != nil {
|
|
c.logger.Error("failed to store recovery unseal keys", "error", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Generate a new root token
|
|
rootToken, err := c.tokenStore.rootToken(ctx)
|
|
if err != nil {
|
|
c.logger.Error("root token generation failed", "error", err)
|
|
return nil, err
|
|
}
|
|
results.RootToken = rootToken.ExternalID
|
|
c.logger.Info("root token generated")
|
|
|
|
if initParams.RootTokenPGPKey != "" {
|
|
_, encryptedVals, err := pgpkeys.EncryptShares([][]byte{[]byte(results.RootToken)}, []string{initParams.RootTokenPGPKey})
|
|
if err != nil {
|
|
c.logger.Error("root token encryption failed", "error", err)
|
|
return nil, err
|
|
}
|
|
results.RootToken = base64.StdEncoding.EncodeToString(encryptedVals[0])
|
|
}
|
|
|
|
if raftBackend != nil {
|
|
if _, err := c.raftCreateTLSKeyring(ctx); err != nil {
|
|
c.logger.Error("failed to create raft TLS keyring", "error", err)
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Prepare to re-seal
|
|
if err := c.preSeal(); err != nil {
|
|
c.logger.Error("pre-seal teardown failed", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
if c.serviceRegistration != nil {
|
|
if err := c.serviceRegistration.NotifyInitializedStateChange(true); err != nil {
|
|
if c.logger.IsWarn() {
|
|
c.logger.Warn("notification of initialization failed", "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
// UnsealWithStoredKeys performs auto-unseal using stored keys. An error
|
|
// return value of "nil" implies the Vault instance is unsealed.
|
|
//
|
|
// Callers should attempt to retry any NonFatalErrors. Callers should
|
|
// not re-attempt fatal errors.
|
|
func (c *Core) UnsealWithStoredKeys(ctx context.Context) error {
|
|
c.unsealWithStoredKeysLock.Lock()
|
|
defer c.unsealWithStoredKeysLock.Unlock()
|
|
|
|
if c.seal.BarrierType() == wrapping.WrapperTypeShamir {
|
|
return nil
|
|
}
|
|
|
|
// Disallow auto-unsealing when migrating
|
|
if c.IsInSealMigrationMode() && !c.IsSealMigrated() {
|
|
return NewNonFatalError(errors.New("cannot auto-unseal during seal migration"))
|
|
}
|
|
|
|
c.stateLock.Lock()
|
|
defer c.stateLock.Unlock()
|
|
|
|
sealed := c.Sealed()
|
|
if !sealed {
|
|
c.Logger().Warn("attempted unseal with stored keys, but vault is already unsealed")
|
|
return nil
|
|
}
|
|
|
|
c.Logger().Info("stored unseal keys supported, attempting fetch")
|
|
keys, err := c.seal.GetStoredKeys(ctx)
|
|
if err != nil {
|
|
return NewNonFatalError(fmt.Errorf("fetching stored unseal keys failed: %w", err))
|
|
}
|
|
|
|
// This usually happens when auto-unseal is configured, but the servers have
|
|
// not been initialized yet.
|
|
if len(keys) == 0 {
|
|
return NewNonFatalError(errors.New("stored unseal keys are supported, but none were found"))
|
|
}
|
|
if len(keys) != 1 {
|
|
return NewNonFatalError(errors.New("expected exactly one stored key"))
|
|
}
|
|
|
|
err = c.unsealInternal(ctx, keys[0])
|
|
if err != nil {
|
|
return NewNonFatalError(fmt.Errorf("unseal with stored key failed: %w", err))
|
|
}
|
|
|
|
if c.Sealed() {
|
|
// This most likely means that the user configured Vault to only store a
|
|
// subset of the required threshold of keys. We still consider this a
|
|
// "success", since trying again would yield the same result.
|
|
c.Logger().Warn("vault still sealed after using stored unseal key")
|
|
} else {
|
|
c.Logger().Info("unsealed with stored key")
|
|
}
|
|
|
|
return nil
|
|
}
|