From f7c384fd4c08d0cadf21f0804a874ca4933d3b48 Mon Sep 17 00:00:00 2001 From: Nick Cabatoff Date: Fri, 23 Oct 2020 14:16:04 -0400 Subject: [PATCH] Same seal migration oss (#10224) * Refactoring and test improvements. * Support migrating from a given type of autoseal to that same type but with different parameters. --- command/server.go | 12 +- helper/testhelpers/seal/sealhelper.go | 5 +- http/sys_seal.go | 23 +- vault/core.go | 793 ++++++++++-------- .../seal_migration_pre14_test.go | 28 +- .../sealmigration/seal_migration_test.go | 288 +++---- vault/generate_root.go | 2 +- vault/generate_root_recovery.go | 2 +- vault/init.go | 30 +- vault/seal.go | 35 +- vault/seal/seal.go | 4 + vault/testing.go | 22 +- 12 files changed, 684 insertions(+), 560 deletions(-) diff --git a/command/server.go b/command/server.go index 3d170e978b..f5ed9446a3 100644 --- a/command/server.go +++ b/command/server.go @@ -1100,7 +1100,9 @@ func (c *ServerCommand) Run(args []string) int { Logger: c.logger.Named("shamir"), }), }) - wrapper, sealConfigError = configutil.ConfigureWrapper(configSeal, &infoKeys, &info, sealLogger) + var sealInfoKeys []string + var sealInfoMap = map[string]string{} + wrapper, sealConfigError = configutil.ConfigureWrapper(configSeal, &sealInfoKeys, &sealInfoMap, sealLogger) if sealConfigError != nil { if !errwrap.ContainsType(sealConfigError, new(logical.KeyNotFoundError)) { c.UI.Error(fmt.Sprintf( @@ -1116,12 +1118,18 @@ func (c *ServerCommand) Run(args []string) int { }) } + var infoPrefix = "" if configSeal.Disabled { unwrapSeal = seal + infoPrefix = "Old " } else { barrierSeal = seal barrierWrapper = wrapper } + for _, k := range sealInfoKeys { + infoKeys = append(infoKeys, infoPrefix+k) + info[infoPrefix+k] = sealInfoMap[k] + } // Ensure that the seal finalizer is called, even if using verify-only defer func() { @@ -1570,7 +1578,7 @@ CLUSTER_SYNTHESIS_COMPLETE: // Vault cluster with multiple servers is configured with auto-unseal but is // uninitialized. Once one server initializes the storage backend, this // goroutine will pick up the unseal keys and unseal this instance. - if !core.IsInSealMigration() { + if !core.IsInSealMigrationMode() { go func() { for { err := core.UnsealWithStoredKeys(context.Background()) diff --git a/helper/testhelpers/seal/sealhelper.go b/helper/testhelpers/seal/sealhelper.go index 3fa449e5a0..7705126f99 100644 --- a/helper/testhelpers/seal/sealhelper.go +++ b/helper/testhelpers/seal/sealhelper.go @@ -2,6 +2,7 @@ package sealhelper import ( "path" + "strconv" "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/api" @@ -20,7 +21,7 @@ type TransitSealServer struct { *vault.TestCluster } -func NewTransitSealServer(t testing.T) *TransitSealServer { +func NewTransitSealServer(t testing.T, idx int) *TransitSealServer { conf := &vault.CoreConfig{ LogicalBackends: map[string]logical.Factory{ "transit": transit.Factory, @@ -29,7 +30,7 @@ func NewTransitSealServer(t testing.T) *TransitSealServer { opts := &vault.TestClusterOptions{ NumCores: 1, HandlerFunc: http.Handler, - Logger: logging.NewVaultLogger(hclog.Trace).Named(t.Name()).Named("transit"), + Logger: logging.NewVaultLogger(hclog.Trace).Named(t.Name()).Named("transit-seal" + strconv.Itoa(idx)), } teststorage.InmemBackendSetup(conf, opts) cluster := vault.NewTestCluster(t, conf, opts) diff --git a/http/sys_seal.go b/http/sys_seal.go index 3221f90fac..000f438b22 100644 --- a/http/sys_seal.go +++ b/http/sys_seal.go @@ -101,20 +101,6 @@ func handleSysUnseal(core *vault.Core) http.Handler { return } - isInSealMigration := core.IsInSealMigration() - if !req.Migrate && isInSealMigration { - respondError( - w, http.StatusBadRequest, - errors.New("'migrate' parameter must be set true in JSON body when in seal migration mode")) - return - } - if req.Migrate && !isInSealMigration { - respondError( - w, http.StatusBadRequest, - errors.New("'migrate' parameter set true in JSON body when not in seal migration mode")) - return - } - if req.Key == "" { respondError( w, http.StatusBadRequest, @@ -138,9 +124,10 @@ func handleSysUnseal(core *vault.Core) http.Handler { } } - // Attempt the unseal - if core.SealAccess().RecoveryKeySupported() { - _, err = core.UnsealWithRecoveryKeys(key) + // Attempt the unseal. If migrate was specified, the key should correspond + // to the old seal. + if req.Migrate { + _, err = core.UnsealMigrate(key) } else { _, err = core.Unseal(key) } @@ -231,7 +218,7 @@ func handleSysSealStatusRaw(core *vault.Core, w http.ResponseWriter, r *http.Req Progress: progress, Nonce: nonce, Version: version.GetVersion().VersionNumber(), - Migration: core.IsInSealMigration(), + Migration: core.IsInSealMigrationMode() && !core.IsSealMigrated(), ClusterName: clusterName, ClusterID: clusterID, RecoverySeal: core.SealAccess().RecoveryKeySupported(), diff --git a/vault/core.go b/vault/core.go index abf57ef0db..d8a1abbc77 100644 --- a/vault/core.go +++ b/vault/core.go @@ -47,7 +47,6 @@ import ( "github.com/hashicorp/vault/vault/quotas" vaultseal "github.com/hashicorp/vault/vault/seal" "github.com/patrickmn/go-cache" - uberAtomic "go.uber.org/atomic" "google.golang.org/grpc" ) @@ -171,14 +170,14 @@ type raftInformation struct { type migrationInformation struct { // seal to use during a migration operation. It is the // seal we're migrating *from*. - seal Seal - masterKey []byte - recoveryKey []byte + seal Seal - // shamirCombinedKey is the key that is used to store master key when shamir - // seal is in use. This will be set as the recovery key when a migration happens - // from shamir to auto-seal. - shamirCombinedKey []byte + // unsealKey was the unseal key provided for the migration seal. + // This will be set as the recovery key when migrating from shamir to auto-seal. + // We don't need to do anything with it when migrating auto->shamir because + // we don't store the shamir combined key for shamir seals, nor when + // migrating auto->auto because then the recovery key doesn't change. + unsealKey []byte } // Core is used as the central manager of Vault activity. It is the primary point of @@ -234,14 +233,13 @@ type Core struct { // peer to an existing raft cluster raftInfo *raftInformation - // migrationInfo is used during a seal migration. This contains information - // about the seal we are migrating *from*. - migrationInfo *migrationInformation - sealMigrated *uint32 - inSealMigration *uberAtomic.Bool - - // unwrapSeal is the old seal when migrating to a new seal. - unwrapSeal Seal + // migrationInfo is used during (and possibly after) a seal migration. + // This contains information about the seal we are migrating *from*. Even + // post seal migration, provided the old seal is still in configuration + // migrationInfo will be populated, which on enterprise may be necessary for + // seal rewrap. + migrationInfo *migrationInformation + sealMigrationDone *uint32 // barrier is the security barrier wrapping the physical backend barrier SecurityBarrier @@ -559,7 +557,12 @@ type CoreConfig struct { ServiceRegistration sr.ServiceRegistration - Seal Seal + // Seal is the configured seal, or if none is configured explicitly, a + // shamir seal. In migration scenarios this is the new seal. + Seal Seal + + // Unwrap seal is the optional seal marked "disabled"; this is the old + // seal in migration scenarios. UnwrapSeal Seal SecureRandomReader io.Reader @@ -724,8 +727,7 @@ func NewCore(conf *CoreConfig) (*Core, error) { seal: conf.Seal, router: NewRouter(), sealed: new(uint32), - sealMigrated: new(uint32), - inSealMigration: uberAtomic.NewBool(false), + sealMigrationDone: new(uint32), standby: true, standbyStopCh: new(atomic.Value), baseLogger: conf.Logger, @@ -1010,189 +1012,206 @@ func (c *Core) ResetUnsealProcess() { c.unlockInfo = nil } +func (c *Core) UnsealMigrate(key []byte) (bool, error) { + err := c.unsealFragment(key, true) + return !c.Sealed(), err +} + // Unseal is used to provide one of the key parts to unseal the Vault. -// -// They key given as a parameter will automatically be zerod after -// this method is done with it. If you want to keep the key around, a copy -// should be made. func (c *Core) Unseal(key []byte) (bool, error) { - return c.unseal(key, false) + err := c.unsealFragment(key, false) + return !c.Sealed(), err } -func (c *Core) UnsealWithRecoveryKeys(key []byte) (bool, error) { - return c.unseal(key, true) -} - -func (c *Core) unseal(key []byte, useRecoveryKeys bool) (bool, error) { +// unseal takes a key fragment and attempts to use it to unseal Vault. +// Vault may remain unsealed afterwards even when no error is returned, +// depending on whether enough key fragments were provided to meet the +// target threshold. +// +// The provided key should be a recovery key fragment if the seal +// is an autoseal, or a regular seal key fragment for shamir. In +// migration scenarios "seal" in the preceding sentance refers to +// the migration seal in c.migrationInfo.seal. +// +// We use getUnsealKey to work out if we have enough fragments, +// and if we don't have enough we return early. Otherwise we get +// back the combined key. +// +// For legacy shamir the combined key *is* the master key. For +// shamir the combined key is used to decrypt the master key +// read from storage. For autoseal the combined key isn't used +// except to verify that the stored recovery key matches. +// +// In migration scenarios a side-effect of unsealing is that +// the members of c.migrationInfo are populated (excluding +// .seal, which must already be populated before unseal is called.) +func (c *Core) unsealFragment(key []byte, migrate bool) error { defer metrics.MeasureSince([]string{"core", "unseal"}, time.Now()) c.stateLock.Lock() defer c.stateLock.Unlock() - c.logger.Debug("unseal key supplied") - ctx := context.Background() + if migrate && c.migrationInfo == nil { + return fmt.Errorf("can't perform a seal migration, no migration seal found") + } + if migrate && c.isRaftUnseal() { + return fmt.Errorf("can't perform a seal migration while joining a raft cluster") + } + if !migrate && c.migrationInfo != nil { + done, err := c.sealMigrated(ctx) + if err != nil { + return fmt.Errorf("error checking to see if seal is migrated: %w", err) + } + if !done { + return fmt.Errorf("migrate option not provided and seal migration is pending") + } + } + + c.logger.Debug("unseal key supplied", "migrate", migrate) + // Explicitly check for init status. This also checks if the seal // configuration is valid (i.e. non-nil). init, err := c.Initialized(ctx) if err != nil { - return false, err + return err } if !init && !c.isRaftUnseal() { - return false, ErrNotInit + return ErrNotInit } // Verify the key length min, max := c.barrier.KeyLength() max += shamir.ShareOverhead if len(key) < min { - return false, &ErrInvalidKey{fmt.Sprintf("key is shorter than minimum %d bytes", min)} + return &ErrInvalidKey{fmt.Sprintf("key is shorter than minimum %d bytes", min)} } if len(key) > max { - return false, &ErrInvalidKey{fmt.Sprintf("key is longer than maximum %d bytes", max)} + return &ErrInvalidKey{fmt.Sprintf("key is longer than maximum %d bytes", max)} } // Check if already unsealed if !c.Sealed() { - return true, nil + return nil } sealToUse := c.seal - if c.migrationInfo != nil { + if migrate { c.logger.Info("unsealing using migration seal") sealToUse = c.migrationInfo.seal } - // unsealPart returns either a master key (legacy shamir) or an unseal - // key (new-style shamir). - masterKey, err := c.unsealPart(ctx, sealToUse, key, useRecoveryKeys) + newKey, err := c.recordUnsealPart(key) + if !newKey || err != nil { + return err + } + + // getUnsealKey returns either a recovery key (in the case of an autoseal) + // or a master key (legacy shamir) or an unseal key (new-style shamir). + combinedKey, err := c.getUnsealKey(ctx, sealToUse) + if err != nil || combinedKey == nil { + return err + } + if migrate { + c.migrationInfo.unsealKey = combinedKey + } + + if c.isRaftUnseal() { + return c.unsealWithRaft(combinedKey) + } + masterKey, err := c.unsealKeyToMasterKeyPreUnseal(ctx, sealToUse, combinedKey) if err != nil { - return false, err + return err } - - if masterKey != nil { - if sealToUse.BarrierType() == wrapping.Shamir && c.migrationInfo == nil { - // If this is a legacy shamir seal this serves no purpose but it - // doesn't hurt. - err = sealToUse.GetAccess().Wrapper.(*aeadwrapper.ShamirWrapper).SetAESGCMKeyBytes(masterKey) - if err != nil { - return false, err - } - } - - if !c.isRaftUnseal() { - if sealToUse.BarrierType() == wrapping.Shamir { - cfg, err := sealToUse.BarrierConfig(ctx) - if err != nil { - return false, err - } - - // If there is a stored key, retrieve it. - if cfg.StoredShares > 0 { - // Here's where we actually test that the provided unseal - // key is valid: can it decrypt the stored master key? - storedKeys, err := sealToUse.GetStoredKeys(ctx) - if err != nil { - return false, err - } - if len(storedKeys) == 0 { - return false, fmt.Errorf("shamir seal with stored keys configured but no stored keys found") - } - masterKey = storedKeys[0] - } - } - - return c.unsealInternal(ctx, masterKey) - } - - switch c.raftInfo.joinInProgress { - case true: - // JoinRaftCluster is already trying to perform a join based on retry_join configuration. - // Inform that routine that unseal key validation is complete so that it can continue to - // try and join possible leader nodes, and wait for it to complete. - - atomic.StoreUint32(c.postUnsealStarted, 1) - - c.logger.Info("waiting for raft retry join process to complete") - <-c.raftJoinDoneCh - - default: - // This is the case for manual raft join. Send the answer to the leader node and - // wait for data to start streaming in. - if err := c.joinRaftSendAnswer(ctx, sealToUse.GetAccess(), c.raftInfo); err != nil { - return false, err - } - // Reset the state - c.raftInfo = nil - } - - go func() { - keyringFound := false - haveMasterKey := sealToUse.StoredKeysSupported() != vaultseal.StoredKeysSupportedShamirMaster - defer func() { - if keyringFound && haveMasterKey { - _, err := c.unsealInternal(ctx, masterKey) - if err != nil { - c.logger.Error("failed to unseal", "error", err) - } - } - }() - - // Wait until we at least have the keyring before we attempt to - // unseal the node. - for { - if !keyringFound { - keys, err := c.underlyingPhysical.List(ctx, keyringPrefix) - if err != nil { - c.logger.Error("failed to list physical keys", "error", err) - return - } - if strutil.StrListContains(keys, "keyring") { - keyringFound = true - } - } - if !haveMasterKey { - keys, err := sealToUse.GetStoredKeys(ctx) - if err != nil { - c.logger.Error("failed to read master key", "error", err) - return - } - if len(keys) > 0 { - haveMasterKey = true - masterKey = keys[0] - } - } - if keyringFound && haveMasterKey { - return - } - time.Sleep(1 * time.Second) - } - }() - - // Return Vault as sealed since unsealing happens in background - // which gets delayed until the data from the leader is streamed to - // the follower. - return true, nil - } - - return false, nil + return c.unsealInternal(ctx, masterKey) } -// unsealPart takes in a key share, and returns the master key if the threshold -// is met. If recovery keys are supported, recovery key shares may be provided. -func (c *Core) unsealPart(ctx context.Context, seal Seal, key []byte, useRecoveryKeys bool) ([]byte, error) { +func (c *Core) unsealWithRaft(combinedKey []byte) error { + ctx := context.Background() + + if c.seal.BarrierType() == wrapping.Shamir { + // If this is a legacy shamir seal this serves no purpose but it + // doesn't hurt. + err := c.seal.GetAccess().Wrapper.(*aeadwrapper.ShamirWrapper).SetAESGCMKeyBytes(combinedKey) + if err != nil { + return err + } + } + + switch c.raftInfo.joinInProgress { + case true: + // JoinRaftCluster is already trying to perform a join based on retry_join configuration. + // Inform that routine that unseal key validation is complete so that it can continue to + // try and join possible leader nodes, and wait for it to complete. + + atomic.StoreUint32(c.postUnsealStarted, 1) + + c.logger.Info("waiting for raft retry join process to complete") + <-c.raftJoinDoneCh + + default: + // This is the case for manual raft join. Send the answer to the leader node and + // wait for data to start streaming in. + if err := c.joinRaftSendAnswer(ctx, c.seal.GetAccess(), c.raftInfo); err != nil { + return err + } + // Reset the state + c.raftInfo = nil + } + + go func() { + var masterKey []byte + keyringFound := false + + // Wait until we at least have the keyring before we attempt to + // unseal the node. + for { + if !keyringFound { + keys, err := c.underlyingPhysical.List(ctx, keyringPrefix) + if err != nil { + c.logger.Error("failed to list physical keys", "error", err) + return + } + if strutil.StrListContains(keys, "keyring") { + keyringFound = true + } + } + if keyringFound && len(masterKey) == 0 { + var err error + masterKey, err = c.unsealKeyToMasterKeyPreUnseal(ctx, c.seal, combinedKey) + if err != nil { + c.logger.Error("failed to read master key", "error", err) + return + } + } + if keyringFound && len(masterKey) > 0 { + err := c.unsealInternal(ctx, masterKey) + if err != nil { + c.logger.Error("failed to unseal", "error", err) + } + return + } + time.Sleep(1 * time.Second) + } + }() + + return nil +} + +// recordUnsealPart takes in a key fragment, and returns true if it's a new fragment. +func (c *Core) recordUnsealPart(key []byte) (bool, error) { // Check if we already have this piece if c.unlockInfo != nil { for _, existing := range c.unlockInfo.Parts { if subtle.ConstantTimeCompare(existing, key) == 1 { - return nil, nil + return false, nil } } } else { uuid, err := uuid.GenerateUUID() if err != nil { - return nil, err + return false, err } c.unlockInfo = &unlockInformation{ Nonce: uuid, @@ -1201,12 +1220,19 @@ func (c *Core) unsealPart(ctx context.Context, seal Seal, key []byte, useRecover // Store this key c.unlockInfo.Parts = append(c.unlockInfo.Parts, key) + return true, nil +} +// getUnsealKey uses key fragments recorded by recordUnsealPart and +// returns the combined key if the key share threshold is met. +// If the key fragments are part of a recovery key, also verify that +// it matches the stored recovery key on disk. +func (c *Core) getUnsealKey(ctx context.Context, seal Seal) ([]byte, error) { var config *SealConfig var err error switch { - case seal.RecoveryKeySupported() && (useRecoveryKeys || c.migrationInfo != nil): + case seal.RecoveryKeySupported(): config, err = seal.RecoveryConfig(ctx) case c.isRaftUnseal(): // Ignore follower's seal config and refer to leader's barrier @@ -1228,122 +1254,98 @@ func (c *Core) unsealPart(ctx context.Context, seal Seal, key []byte, useRecover return nil, nil } - // Best-effort memzero of unlock parts once we're done with them defer func() { - for i := range c.unlockInfo.Parts { - memzero(c.unlockInfo.Parts[i]) - } c.unlockInfo = nil }() // Recover the split key. recoveredKey is the shamir combined // key, or the single provided key if the threshold is 1. - var recoveredKey []byte - var masterKey []byte - var recoveryKey []byte + var unsealKey []byte if config.SecretThreshold == 1 { - recoveredKey = make([]byte, len(c.unlockInfo.Parts[0])) - copy(recoveredKey, c.unlockInfo.Parts[0]) + unsealKey = make([]byte, len(c.unlockInfo.Parts[0])) + copy(unsealKey, c.unlockInfo.Parts[0]) } else { - recoveredKey, err = shamir.Combine(c.unlockInfo.Parts) + unsealKey, err = shamir.Combine(c.unlockInfo.Parts) if err != nil { - return nil, errwrap.Wrapf("failed to compute master key: {{err}}", err) + return nil, errwrap.Wrapf("failed to compute combined key: {{err}}", err) } } - if seal.RecoveryKeySupported() && (useRecoveryKeys || c.migrationInfo != nil) { - // Verify recovery key. - if err := seal.VerifyRecoveryKey(ctx, recoveredKey); err != nil { + if seal.RecoveryKeySupported() { + if err := seal.VerifyRecoveryKey(ctx, unsealKey); err != nil { return nil, err } - recoveryKey = recoveredKey - - // Get stored keys and shamir combine into single master key. Unsealing with - // recovery keys currently does not support: 1) mixed stored and non-stored - // keys setup, nor 2) seals that support recovery keys but not stored keys. - // If insufficient shares are provided, shamir.Combine will error, and if - // no stored keys are found it will return masterKey as nil. - if seal.StoredKeysSupported() == vaultseal.StoredKeysSupportedGeneric { - masterKeyShares, err := seal.GetStoredKeys(ctx) - if err != nil { - return nil, errwrap.Wrapf("unable to retrieve stored keys: {{err}}", err) - } - - switch len(masterKeyShares) { - case 0: - return nil, errors.New("seal returned no master key shares") - case 1: - masterKey = masterKeyShares[0] - default: - masterKey, err = shamir.Combine(masterKeyShares) - if err != nil { - return nil, errwrap.Wrapf("failed to compute master key: {{err}}", err) - } - } - } - } else { - masterKey = recoveredKey } - switch { - case c.migrationInfo != nil: - // Make copies of fields that gets passed on to migration via migrationInfo to - // avoid accidental reference changes - c.migrationInfo.shamirCombinedKey = make([]byte, len(recoveredKey)) - copy(c.migrationInfo.shamirCombinedKey, recoveredKey) - if seal.StoredKeysSupported() == vaultseal.StoredKeysSupportedShamirMaster { - err = seal.GetAccess().Wrapper.(*aeadwrapper.ShamirWrapper).SetAESGCMKeyBytes(recoveredKey) - if err != nil { - return nil, errwrap.Wrapf("failed to set master key in seal: {{err}}", err) - } - storedKeys, err := seal.GetStoredKeys(ctx) - if err != nil { - return nil, errwrap.Wrapf("unable to retrieve stored keys: {{err}}", err) - } - masterKey = storedKeys[0] - } - c.migrationInfo.masterKey = make([]byte, len(masterKey)) - copy(c.migrationInfo.masterKey, masterKey) - c.migrationInfo.recoveryKey = make([]byte, len(recoveryKey)) - copy(c.migrationInfo.recoveryKey, recoveryKey) - } - - return masterKey, nil + return unsealKey, nil } -func (c *Core) migrateSeal(ctx context.Context) error { - - // Check if migration is already in progress - if !c.inSealMigration.CAS(false, true) { - c.logger.Warn("migration is already in progress") - return nil - } - defer c.inSealMigration.CAS(true, false) - - if c.migrationInfo == nil { - return nil +// sealMigrated must be called with the stateLock held. It returns true if +// the seal configured in HCL and the seal configured in storage match. +// For the auto->auto same seal migration scenario, it will return false even +// if the preceding conditions are true but we cannot decrypt the master key +// in storage using the configured seal. +func (c *Core) sealMigrated(ctx context.Context) (bool, error) { + if atomic.LoadUint32(c.sealMigrationDone) == 1 { + return true, nil } existBarrierSealConfig, existRecoverySealConfig, err := c.PhysicalSealConfigs(ctx) if err != nil { - return fmt.Errorf("failed to read existing seal configuration during migration: %v", err) + return false, err } - if existBarrierSealConfig.Type != c.migrationInfo.seal.BarrierType() { - // If the existing barrier type is not the same as the type of seal we are - // migrating from, it can be concluded that migration has already been performed - c.logger.Info("migration is already performed since existing seal type and source seal types are different") - c.migrationInfo = nil - atomic.StoreUint32(c.sealMigrated, 1) + + if existBarrierSealConfig.Type != c.seal.BarrierType() { + return false, nil + } + if c.seal.RecoveryKeySupported() && existRecoverySealConfig.Type != c.seal.RecoveryType() { + return false, nil + } + + if c.seal.BarrierType() != c.migrationInfo.seal.BarrierType() { + return true, nil + } + + // The above checks can handle the auto->shamir and shamir->auto + // and auto1->auto2 cases. For auto1->auto1, we need to actually try + // to read and decrypt the keys. + + keysMig, errMig := c.migrationInfo.seal.GetStoredKeys(ctx) + keys, err := c.seal.GetStoredKeys(ctx) + + switch { + case len(keys) > 0 && err == nil: + return true, nil + case len(keysMig) > 0 && errMig == nil: + return false, nil + case errors.Is(err, &ErrDecrypt{}) && errors.Is(errMig, &ErrDecrypt{}): + return false, fmt.Errorf("decrypt error, neither the old nor new seal can read stored keys: old seal err=%v, new seal err=%v", errMig, err) + default: + return false, fmt.Errorf("neither the old nor new seal can read stored keys: old seal err=%v, new seal err=%v", errMig, err) + } +} + +// migrateSeal must be called with the stateLock held. +func (c *Core) migrateSeal(ctx context.Context) error { + if c.migrationInfo == nil { + return nil + } + + ok, err := c.sealMigrated(ctx) + if err != nil { + return fmt.Errorf("error checking if seal is migrated or not: %w", err) + } + if ok { + c.logger.Info("migration is already performed") return nil } c.logger.Info("seal migration initiated") - // We need to update the cached seal configs because they may have been wiped out by various means. - c.adjustSealConfigDuringMigration(existBarrierSealConfig, existRecoverySealConfig) switch { case c.migrationInfo.seal.RecoveryKeySupported() && c.seal.RecoveryKeySupported(): - c.logger.Info("migrating from one auto-unseal to another", "from", c.migrationInfo.seal.BarrierType(), "to", c.seal.BarrierType()) + c.logger.Info("migrating from one auto-unseal to another", "from", + c.migrationInfo.seal.BarrierType(), "to", c.seal.BarrierType()) // Set the recovery and barrier keys to be the same. recoveryKey, err := c.migrationInfo.seal.RecoveryKey(ctx) @@ -1368,20 +1370,23 @@ func (c *Core) migrateSeal(ctx context.Context) error { c.logger.Info("migrating from one auto-unseal to shamir", "from", c.migrationInfo.seal.BarrierType()) // Auto to Shamir, since recovery key isn't supported on new seal - // In this case we have to ensure that the recovery information was - // set properly. - if c.migrationInfo.recoveryKey == nil { - return errors.New("did not get expected recovery information to set new seal during migration") + recoveryKey, err := c.migrationInfo.seal.RecoveryKey(ctx) + if err != nil { + return errwrap.Wrapf("error getting recovery key to set on new seal: {{err}}", err) } - // We have recovery keys; we're going to use them as the new - // shamir KeK. - err := c.seal.GetAccess().Wrapper.(*aeadwrapper.ShamirWrapper).SetAESGCMKeyBytes(c.migrationInfo.recoveryKey) + // We have recovery keys; we're going to use them as the new shamir KeK. + err = c.seal.GetAccess().Wrapper.(*aeadwrapper.ShamirWrapper).SetAESGCMKeyBytes(recoveryKey) if err != nil { return errwrap.Wrapf("failed to set master key in seal: {{err}}", err) } - if err := c.seal.SetStoredKeys(ctx, [][]byte{c.migrationInfo.masterKey}); err != nil { + barrierKeys, err := c.migrationInfo.seal.GetStoredKeys(ctx) + if err != nil { + return errwrap.Wrapf("error getting stored keys to set on new seal: {{err}}", err) + } + + if err := c.seal.SetStoredKeys(ctx, barrierKeys); err != nil { return errwrap.Wrapf("error setting new barrier key information during migrate: {{err}}", err) } @@ -1389,7 +1394,7 @@ func (c *Core) migrateSeal(ctx context.Context) error { c.logger.Info("migrating from shamir to auto-unseal", "to", c.seal.BarrierType()) // Migration is happening from shamir -> auto. In this case use the shamir // combined key that was used to store the master key as the new recovery key. - if err := c.seal.SetRecoveryKey(ctx, c.migrationInfo.shamirCombinedKey); err != nil { + if err := c.seal.SetRecoveryKey(ctx, c.migrationInfo.unsealKey); err != nil { return errwrap.Wrapf("error setting new recovery key information: {{err}}", err) } @@ -1399,7 +1404,8 @@ func (c *Core) migrateSeal(ctx context.Context) error { return errwrap.Wrapf("error generating new master key: {{err}}", err) } - // Rekey the barrier + // Rekey the barrier. This handles the case where the shamir seal we're + // migrating from was a legacy seal without a stored master key. if err := c.barrier.Rekey(ctx, newMasterKey); err != nil { return errwrap.Wrapf("error rekeying barrier during migration: {{err}}", err) } @@ -1413,32 +1419,13 @@ func (c *Core) migrateSeal(ctx context.Context) error { return errors.New("unhandled migration case (shamir to shamir)") } - // At this point we've swapped things around and need to ensure we - // don't migrate again - c.migrationInfo = nil - atomic.StoreUint32(c.sealMigrated, 1) - - // Ensure we populate the new values - bc, err := c.seal.BarrierConfig(ctx) + err = c.migrateSealConfig(ctx) if err != nil { - return errwrap.Wrapf("error fetching barrier config after migration: {{err}}", err) + return errwrap.Wrapf("error storing new seal configs: {{err}}", err) } - if err := c.seal.SetBarrierConfig(ctx, bc); err != nil { - return errwrap.Wrapf("error storing barrier config after migration: {{err}}", err) - } - - if c.seal.RecoveryKeySupported() { - rc, err := c.seal.RecoveryConfig(ctx) - if err != nil { - return errwrap.Wrapf("error fetching recovery config after migration: {{err}}", err) - } - if err := c.seal.SetRecoveryConfig(ctx, rc); err != nil { - return errwrap.Wrapf("error storing recovery config after migration: {{err}}", err) - } - } else if err := c.physical.Delete(ctx, recoverySealConfigPlaintextPath); err != nil { - return errwrap.Wrapf("failed to delete old recovery seal configuration during migration: {{err}}", err) - } + // Flag migration performed for seal-rewrap later + atomic.StoreUint32(c.sealMigrationDone, 1) c.logger.Info("seal migration complete") return nil @@ -1446,24 +1433,22 @@ func (c *Core) migrateSeal(ctx context.Context) error { // unsealInternal takes in the master key and attempts to unseal the barrier. // N.B.: This must be called with the state write lock held. -func (c *Core) unsealInternal(ctx context.Context, masterKey []byte) (bool, error) { - defer memzero(masterKey) - +func (c *Core) unsealInternal(ctx context.Context, masterKey []byte) error { // Attempt to unlock if err := c.barrier.Unseal(ctx, masterKey); err != nil { - return false, err + return err } if err := preUnsealInternal(ctx, c); err != nil { - return false, err + return err } if err := c.startClusterListener(ctx); err != nil { - return false, err + return err } if err := c.startRaftBackend(ctx); err != nil { - return false, err + return err } if err := c.setupReplicationResolverHandler(); err != nil { @@ -1478,14 +1463,14 @@ func (c *Core) unsealInternal(ctx context.Context, masterKey []byte) (bool, erro c.logger.Error("cluster setup failed", "error", err) c.barrier.Seal() c.logger.Warn("vault is sealed") - return false, err + return err } if err := c.migrateSeal(ctx); err != nil { c.logger.Error("seal migration error", "error", err) c.barrier.Seal() c.logger.Warn("vault is sealed") - return false, err + return err } ctx, ctxCancel := context.WithCancel(namespace.RootContext(nil)) @@ -1493,7 +1478,7 @@ func (c *Core) unsealInternal(ctx context.Context, masterKey []byte) (bool, erro c.logger.Error("post-unseal setup failed", "error", err) c.barrier.Seal() c.logger.Warn("vault is sealed") - return false, err + return err } // Force a cache bust here, which will also run migration code @@ -1530,7 +1515,7 @@ func (c *Core) unsealInternal(ctx context.Context, masterKey []byte) (bool, erro } } } - return true, nil + return nil } // SealWithRequest takes in a logical.Request, acquires the lock, and passes @@ -2031,8 +2016,7 @@ func (c *Core) postUnseal(ctx context.Context, ctxCancelFunc context.CancelFunc, v() } - if atomic.LoadUint32(c.sealMigrated) == 1 { - defer func() { atomic.StoreUint32(c.sealMigrated, 0) }() + if atomic.LoadUint32(c.sealMigrationDone) == 1 { if err := c.postSealMigration(ctx); err != nil { c.logger.Warn("post-unseal post seal migration failed", "error", err) } @@ -2222,12 +2206,28 @@ func (c *Core) PhysicalSealConfigs(ctx context.Context) (*SealConfig, *SealConfi return barrierConf, recoveryConf, nil } -// adjustForSealMigration takes the unwrapSeal (in a migration scenario, this -// is the old seal we're migrating from; in any scenario, it's the seal that -// the master key is currently encrypted with). After doing some sanity checking -// it sets up the seals and migrationInfo to allow for a migration if needed. +// adjustForSealMigration takes the unwrapSeal, which is nil if (a) we're not +// configured for seal migration or (b) we might be doing a seal migration away +// from shamir. It will only be non-nil if there is a configured seal with +// the config key disabled=true, which implies a migration away from autoseal. +// +// For case (a), the common case, we expect that the stored barrier +// config matches the seal type, in which case we simply return nil. If they +// don't match, and the stored seal config is of type Shamir but the configured +// seal is not Shamir, that is case (b) and we make an unwrapSeal of type Shamir. +// Any other unwrapSeal=nil scenario is treated as an error. +// +// Given a non-nil unwrapSeal or case (b), we setup c.migrationInfo to prepare +// for a migration upon receiving a valid migration unseal request. We cannot +// check at this time for already performed (or incomplete) migrations because +// we haven't yet been unsealed, so we have no way of checking whether a +// shamir seal works to read stored seal-encrypted data. +// +// The assumption throughout is that the very last step of seal migration is +// to write the new barrier/recovery stored seal config. func (c *Core) adjustForSealMigration(unwrapSeal Seal) error { - existBarrierSealConfig, existRecoverySealConfig, err := c.PhysicalSealConfigs(context.Background()) + ctx := context.Background() + existBarrierSealConfig, existRecoverySealConfig, err := c.PhysicalSealConfigs(ctx) if err != nil { return fmt.Errorf("Error checking for existing seal: %s", err) } @@ -2239,39 +2239,51 @@ func (c *Core) adjustForSealMigration(unwrapSeal Seal) error { } if unwrapSeal == nil { - // We have the same barrier type and the unwrap seal is nil so we're not - // migrating from same to same, IOW we assume it's not a migration - if existBarrierSealConfig.Type == c.seal.BarrierType() { + // With unwrapSeal==nil, either we're not migrating, or we're migrating + // from shamir. + switch { + case existBarrierSealConfig.Type == c.seal.BarrierType(): + // We have the same barrier type and the unwrap seal is nil so we're not + // migrating from same to same, IOW we assume it's not a migration. return nil + case c.seal.BarrierType() == wrapping.Shamir: + // The stored barrier config is not shamir, there is no disabled seal + // in config, and either no configured seal (which equates to Shamir) + // or an explicitly configured Shamir seal. + return fmt.Errorf("cannot seal migrate from %q to Shamir, no disabled seal in configuration", + existBarrierSealConfig.Type) + case existBarrierSealConfig.Type == wrapping.Shamir: + // The configured seal is not Shamir, the stored seal config is Shamir. + // This is a migration away from Shamir. + unwrapSeal = NewDefaultSeal(&vaultseal.Access{ + Wrapper: aeadwrapper.NewShamirWrapper(&wrapping.WrapperOptions{ + Logger: c.logger.Named("shamir"), + }), + }) + default: + // We know at this point that there is a configured non-Shamir seal, + // that it does not match the stored non-Shamir seal config, and that + // there is no explicit disabled seal stanza. + return fmt.Errorf("cannot seal migrate from %q to %q, no disabled seal in configuration", + existBarrierSealConfig.Type, c.seal.BarrierType()) } - - // If we're not coming from Shamir, and the existing type doesn't match - // the barrier type, we need both the migration seal and the new seal - if existBarrierSealConfig.Type != wrapping.Shamir && c.seal.BarrierType() != wrapping.Shamir { - return errors.New(`Trying to migrate from auto-seal to auto-seal but no "disabled" seal stanza found`) - } - - c.unwrapSeal = NewDefaultSeal(&vaultseal.Access{ - Wrapper: aeadwrapper.NewShamirWrapper(&wrapping.WrapperOptions{ - Logger: c.logger.Named("shamir"), - }), - }) } else { // If we're not coming from Shamir we expect the previous seal to be // in the config and disabled. if unwrapSeal.BarrierType() == wrapping.Shamir { return errors.New("Shamir seals cannot be set disabled (they should simply not be set)") } - c.unwrapSeal = unwrapSeal } - c.unwrapSeal.SetCore(c) - // If we've reached this point it's a migration attempt. + // If we've reached this point it's a migration attempt and we should have both + // c.migrationInfo.seal (old seal) and c.seal (new seal) populated. + unwrapSeal.SetCore(c) + // No stored recovery seal config found, what about the legacy recovery config? if existBarrierSealConfig.Type != wrapping.Shamir && existRecoverySealConfig == nil { - entry, err := c.physical.Get(c.activeContext, recoverySealConfigPlaintextPath) + entry, err := c.physical.Get(ctx, recoverySealConfigPath) if err != nil { - return errwrap.Wrapf(fmt.Sprintf("failed to read %q seal configuration: {{err}}", existBarrierSealConfig.Type), err) + return errwrap.Wrapf(fmt.Sprintf("failed to read %q recovery seal configuration: {{err}}", existBarrierSealConfig.Type), err) } if entry == nil { return errors.New("Recovery seal configuration not found for existing seal") @@ -2279,44 +2291,78 @@ func (c *Core) adjustForSealMigration(unwrapSeal Seal) error { return errors.New("Cannot migrate seals while using a legacy recovery seal config") } - if c.unwrapSeal.BarrierType() == c.seal.BarrierType() { - return errors.New("Migrating between same seal types is currently not supported") - } - - // Not entirely sure why this is here, but I theorize it could be to handle - // the case where the migration has already completed, e.g. on another node, - // but the disabled seal stanza wasn't removed from the HCL config yet. - if existBarrierSealConfig.Type == c.seal.BarrierType() { - return nil - } - c.migrationInfo = &migrationInformation{ - seal: c.unwrapSeal, + seal: unwrapSeal, + } + if existBarrierSealConfig.Type != c.seal.BarrierType() { + // It's unnecessary to call this when doing an auto->auto + // same-seal-type migration, since they'll have the same configs before + // and after migration. + c.adjustSealConfigDuringMigration(existBarrierSealConfig, existRecoverySealConfig) } - c.adjustSealConfigDuringMigration(existBarrierSealConfig, existRecoverySealConfig) c.initSealsForMigration() c.logger.Warn("entering seal migration mode; Vault will not automatically unseal even if using an autoseal", "from_barrier_type", c.migrationInfo.seal.BarrierType(), "to_barrier_type", c.seal.BarrierType()) return nil } -func (c *Core) adjustSealConfigDuringMigration(existBarrierSealConfig, existRecoverySealConfig *SealConfig) { - if c.migrationInfo == nil { - return +func (c *Core) migrateSealConfig(ctx context.Context) error { + existBarrierSealConfig, existRecoverySealConfig, err := c.PhysicalSealConfigs(ctx) + if err != nil { + return fmt.Errorf("failed to read existing seal configuration during migration: %v", err) } + var bc, rc *SealConfig + switch { - case c.unwrapSeal.RecoveryKeySupported() && c.seal.RecoveryKeySupported(): + case c.migrationInfo.seal.RecoveryKeySupported() && c.seal.RecoveryKeySupported(): // Migrating from auto->auto, copy the configs over - c.seal.SetCachedBarrierConfig(existBarrierSealConfig) - c.seal.SetCachedRecoveryConfig(existRecoverySealConfig) - case c.unwrapSeal.RecoveryKeySupported(): + bc, rc = existBarrierSealConfig, existRecoverySealConfig + case c.migrationInfo.seal.RecoveryKeySupported(): // Migrating from auto->shamir, clone auto's recovery config and set // stored keys to 1. + bc = existRecoverySealConfig.Clone() + bc.StoredShares = 1 + case c.seal.RecoveryKeySupported(): + // Migrating from shamir->auto, set a new barrier config and set + // recovery config to a clone of shamir's barrier config with stored + // keys set to 0. + bc = &SealConfig{ + Type: c.seal.BarrierType(), + SecretShares: 1, + SecretThreshold: 1, + StoredShares: 1, + } + + rc = existBarrierSealConfig.Clone() + rc.StoredShares = 0 + } + + if err := c.seal.SetBarrierConfig(ctx, bc); err != nil { + return errwrap.Wrapf("error storing barrier config after migration: {{err}}", err) + } + + if c.seal.RecoveryKeySupported() { + if err := c.seal.SetRecoveryConfig(ctx, rc); err != nil { + return errwrap.Wrapf("error storing recovery config after migration: {{err}}", err) + } + } else if err := c.physical.Delete(ctx, recoverySealConfigPlaintextPath); err != nil { + return errwrap.Wrapf("failed to delete old recovery seal configuration during migration: {{err}}", err) + } + + return nil +} + +func (c *Core) adjustSealConfigDuringMigration(existBarrierSealConfig, existRecoverySealConfig *SealConfig) { + switch { + case c.migrationInfo.seal.RecoveryKeySupported() && existRecoverySealConfig != nil: + // Migrating from auto->shamir, clone auto's recovery config and set + // stored keys to 1. Unless the recover config doesn't exist, in which + // case the migration is assumed to already have been performed. newSealConfig := existRecoverySealConfig.Clone() newSealConfig.StoredShares = 1 c.seal.SetCachedBarrierConfig(newSealConfig) - case c.seal.RecoveryKeySupported(): + case !c.migrationInfo.seal.RecoveryKeySupported() && c.seal.RecoveryKeySupported(): // Migrating from shamir->auto, set a new barrier config and set // recovery config to a clone of shamir's barrier config with stored // keys set to 0. @@ -2334,48 +2380,74 @@ func (c *Core) adjustSealConfigDuringMigration(existBarrierSealConfig, existReco } } +func (c *Core) unsealKeyToMasterKeyPostUnseal(ctx context.Context, combinedKey []byte) ([]byte, error) { + return c.unsealKeyToMasterKey(ctx, c.seal, combinedKey, true, false) +} + +func (c *Core) unsealKeyToMasterKeyPreUnseal(ctx context.Context, seal Seal, combinedKey []byte) ([]byte, error) { + return c.unsealKeyToMasterKey(ctx, seal, combinedKey, false, true) +} + // unsealKeyToMasterKey takes a key provided by the user, either a recovery key // if using an autoseal or an unseal key with Shamir. It returns a nil error // if the key is valid and an error otherwise. It also returns the master key // that can be used to unseal the barrier. -func (c *Core) unsealKeyToMasterKey(ctx context.Context, combinedKey []byte) ([]byte, error) { - switch c.seal.StoredKeysSupported() { +// If useTestSeal is true, seal will not be modified; this is used when not +// invoked as part of an unseal process. Otherwise in the non-legacy shamir +// case the combinedKey will be set in the seal, which means subsequent attempts +// to use the seal to read the master key will succeed, assuming combinedKey is +// valid. +// If allowMissing is true, a failure to find the master key in storage results +// in a nil error and a nil master key being returned. +func (c *Core) unsealKeyToMasterKey(ctx context.Context, seal Seal, combinedKey []byte, useTestSeal bool, allowMissing bool) ([]byte, error) { + switch seal.StoredKeysSupported() { case vaultseal.StoredKeysSupportedGeneric: - if err := c.seal.VerifyRecoveryKey(ctx, combinedKey); err != nil { + if err := seal.VerifyRecoveryKey(ctx, combinedKey); err != nil { return nil, errwrap.Wrapf("recovery key verification failed: {{err}}", err) } - storedKeys, err := c.seal.GetStoredKeys(ctx) + storedKeys, err := seal.GetStoredKeys(ctx) + if storedKeys == nil && err == nil && allowMissing { + return nil, nil + } + if err == nil && len(storedKeys) != 1 { err = fmt.Errorf("expected exactly one stored key, got %d", len(storedKeys)) } if err != nil { - return nil, errwrap.Wrapf("unable to retrieve stored keys", err) + return nil, errwrap.Wrapf("unable to retrieve stored keys: {{err}}", err) } return storedKeys[0], nil case vaultseal.StoredKeysSupportedShamirMaster: - testseal := NewDefaultSeal(&vaultseal.Access{ - Wrapper: aeadwrapper.NewShamirWrapper(&wrapping.WrapperOptions{ - Logger: c.logger.Named("testseal"), - }), - }) - testseal.SetCore(c) - cfg, err := c.seal.BarrierConfig(ctx) - if err != nil { - return nil, errwrap.Wrapf("failed to setup test barrier config: {{err}}", err) + if useTestSeal { + testseal := NewDefaultSeal(&vaultseal.Access{ + Wrapper: aeadwrapper.NewShamirWrapper(&wrapping.WrapperOptions{ + Logger: c.logger.Named("testseal"), + }), + }) + testseal.SetCore(c) + cfg, err := seal.BarrierConfig(ctx) + if err != nil { + return nil, errwrap.Wrapf("failed to setup test barrier config: {{err}}", err) + } + testseal.SetCachedBarrierConfig(cfg) + seal = testseal } - testseal.SetCachedBarrierConfig(cfg) - err = testseal.GetAccess().Wrapper.(*aeadwrapper.ShamirWrapper).SetAESGCMKeyBytes(combinedKey) + + err := seal.GetAccess().Wrapper.(*aeadwrapper.ShamirWrapper).SetAESGCMKeyBytes(combinedKey) if err != nil { return nil, errwrap.Wrapf("failed to setup unseal key: {{err}}", err) } - storedKeys, err := testseal.GetStoredKeys(ctx) + storedKeys, err := seal.GetStoredKeys(ctx) + if storedKeys == nil && err == nil && allowMissing { + return nil, nil + } if err == nil && len(storedKeys) != 1 { err = fmt.Errorf("expected exactly one stored key, got %d", len(storedKeys)) } if err != nil { - return nil, errwrap.Wrapf("unable to retrieve stored keys", err) + return nil, errwrap.Wrapf("unable to retrieve stored keys: {{err}}", err) } return storedKeys[0], nil @@ -2385,12 +2457,31 @@ func (c *Core) unsealKeyToMasterKey(ctx context.Context, combinedKey []byte) ([] return nil, fmt.Errorf("invalid seal") } -func (c *Core) IsInSealMigration() bool { +// IsInSealMigrationMode returns true if we're configured to perform a seal migration, +// meaning either that we have a disabled seal in HCL configuration or the seal +// configuration in storage is Shamir but the seal in HCL is not. In this +// mode we should not auto-unseal (even if the migration is done) and we will +// accept unseal requests with and without the `migrate` option, though the migrate +// option is required if we haven't yet performed the seal migration. +func (c *Core) IsInSealMigrationMode() bool { c.stateLock.RLock() defer c.stateLock.RUnlock() return c.migrationInfo != nil } +// IsSealMigrated returns true if we're in seal migration mode but migration +// has already been performed (possibly by another node, or prior to this node's +// current invocation.) +func (c *Core) IsSealMigrated() bool { + if !c.IsInSealMigrationMode() { + return false + } + c.stateLock.RLock() + defer c.stateLock.RUnlock() + done, _ := c.sealMigrated(context.Background()) + return done +} + func (c *Core) BarrierEncryptorAccess() *BarrierEncryptorAccess { return NewBarrierEncryptorAccess(c.barrier) } diff --git a/vault/external_tests/sealmigration/seal_migration_pre14_test.go b/vault/external_tests/sealmigration/seal_migration_pre14_test.go index 0b446326f5..cedfb82f4f 100644 --- a/vault/external_tests/sealmigration/seal_migration_pre14_test.go +++ b/vault/external_tests/sealmigration/seal_migration_pre14_test.go @@ -1,5 +1,3 @@ -// +build !enterprise - package sealmigration import ( @@ -32,23 +30,24 @@ func TestSealMigration_TransitToShamir_Pre14(t *testing.T) { func testSealMigrationTransitToShamir_Pre14(t *testing.T, logger hclog.Logger, storage teststorage.ReusableStorage, basePort int) { // Create the transit server. - tss := sealhelper.NewTransitSealServer(t) + tss := sealhelper.NewTransitSealServer(t, 0) defer func() { if tss != nil { tss.Cleanup() } }() - tss.MakeKey(t, "transit-seal-key") + sealKeyName := "transit-seal-key" + tss.MakeKey(t, sealKeyName) // Initialize the backend with transit. - cluster, _, transitSeal := initializeTransit(t, logger, storage, basePort, tss) + cluster, opts := initializeTransit(t, logger, storage, basePort, tss, sealKeyName) rootToken, recoveryKeys := cluster.RootToken, cluster.RecoveryKeys cluster.EnsureCoresSealed(t) cluster.Cleanup() storage.Cleanup(t, cluster) // Migrate the backend from transit to shamir - migrateFromTransitToShamir_Pre14(t, logger, storage, basePort, tss, transitSeal, rootToken, recoveryKeys) + migrateFromTransitToShamir_Pre14(t, logger, storage, basePort, tss, opts.SealFunc, rootToken, recoveryKeys) // Now that migration is done, we can nuke the transit server, since we // can unseal without it. @@ -60,25 +59,20 @@ func testSealMigrationTransitToShamir_Pre14(t *testing.T, logger hclog.Logger, s runShamir(t, logger, storage, basePort, rootToken, recoveryKeys) } -func migrateFromTransitToShamir_Pre14(t *testing.T, logger hclog.Logger, storage teststorage.ReusableStorage, basePort int, tss *sealhelper.TransitSealServer, transitSeal vault.Seal, rootToken string, recoveryKeys [][]byte) { +func migrateFromTransitToShamir_Pre14(t *testing.T, logger hclog.Logger, storage teststorage.ReusableStorage, basePort int, + tss *sealhelper.TransitSealServer, sealFunc func() vault.Seal, rootToken string, recoveryKeys [][]byte) { + var baseClusterPort = basePort + 10 - var conf = vault.CoreConfig{ - Logger: logger.Named("migrateFromTransitToShamir"), - // N.B. Providing an UnwrapSeal puts us in migration mode. This is the - // equivalent of doing the following in HCL: - // seal "transit" { - // // ... - // disabled = "true" - // } - UnwrapSeal: transitSeal, - } + var conf vault.CoreConfig var opts = vault.TestClusterOptions{ + Logger: logger.Named("migrateFromTransitToShamir"), HandlerFunc: vaulthttp.Handler, NumCores: numTestCores, BaseListenAddress: fmt.Sprintf("127.0.0.1:%d", basePort), BaseClusterListenPort: baseClusterPort, SkipInit: true, + UnwrapSealFunc: sealFunc, } storage.Setup(&conf, &opts) cluster := vault.NewTestCluster(t, &conf, &opts) diff --git a/vault/external_tests/sealmigration/seal_migration_test.go b/vault/external_tests/sealmigration/seal_migration_test.go index 840deb758d..f47863f244 100644 --- a/vault/external_tests/sealmigration/seal_migration_test.go +++ b/vault/external_tests/sealmigration/seal_migration_test.go @@ -24,7 +24,7 @@ import ( ) const ( - numTestCores = 5 + numTestCores = 3 keyShares = 3 keyThreshold = 3 @@ -32,6 +32,7 @@ const ( basePort_TransitToShamir_Pre14 = 21000 basePort_ShamirToTransit_Post14 = 22000 basePort_TransitToShamir_Post14 = 23000 + basePort_TransitToTransit = 24000 ) type testFunc func(t *testing.T, logger hclog.Logger, storage teststorage.ReusableStorage, basePort int) @@ -52,7 +53,6 @@ func testVariousBackends(t *testing.T, tf testFunc, basePort int, includeRaft bo t.Run("file", func(t *testing.T) { t.Parallel() - t.Skip("fails intermittently") logger := logger.Named("file") storage, cleanup := teststorage.MakeReusableStorage( @@ -103,31 +103,28 @@ func testSealMigrationShamirToTransit_Pre14(t *testing.T, logger hclog.Logger, s // Initialize the backend using shamir cluster, _ := initializeShamir(t, logger, storage, basePort) rootToken, barrierKeys := cluster.RootToken, cluster.BarrierKeys - cluster.EnsureCoresSealed(t) cluster.Cleanup() storage.Cleanup(t, cluster) // Create the transit server. - tss := sealhelper.NewTransitSealServer(t) + tss := sealhelper.NewTransitSealServer(t, 0) defer func() { tss.EnsureCoresSealed(t) tss.Cleanup() }() - tss.MakeKey(t, "transit-seal-key") + tss.MakeKey(t, "transit-seal-key-1") // Migrate the backend from shamir to transit. Note that the barrier keys // are now the recovery keys. - transitSeal := migrateFromShamirToTransit_Pre14(t, logger, storage, basePort, tss, rootToken, barrierKeys) + sealFunc := migrateFromShamirToTransit_Pre14(t, logger, storage, basePort, tss, rootToken, barrierKeys) // Run the backend with transit. - runTransit(t, logger, storage, basePort, rootToken, transitSeal) + runAutoseal(t, logger, storage, basePort, rootToken, sealFunc) } -func migrateFromShamirToTransit_Pre14(t *testing.T, logger hclog.Logger, storage teststorage.ReusableStorage, basePort int, tss *sealhelper.TransitSealServer, rootToken string, recoveryKeys [][]byte) vault.Seal { +func migrateFromShamirToTransit_Pre14(t *testing.T, logger hclog.Logger, storage teststorage.ReusableStorage, basePort int, tss *sealhelper.TransitSealServer, rootToken string, recoveryKeys [][]byte) func() vault.Seal { var baseClusterPort = basePort + 10 - var transitSeal vault.Seal - var conf = vault.CoreConfig{} var opts = vault.TestClusterOptions{ Logger: logger.Named("migrateFromShamirToTransit"), @@ -138,8 +135,7 @@ func migrateFromShamirToTransit_Pre14(t *testing.T, logger hclog.Logger, storage SkipInit: true, // N.B. Providing a transit seal puts us in migration mode. SealFunc: func() vault.Seal { - transitSeal = tss.MakeSeal(t, "transit-seal-key") - return transitSeal + return tss.MakeSeal(t, "transit-seal-key") }, } storage.Setup(&conf, &opts) @@ -159,6 +155,8 @@ func migrateFromShamirToTransit_Pre14(t *testing.T, logger hclog.Logger, storage // Wait for migration to finish. awaitMigration(t, leader.Client) + verifySealConfigTransit(t, leader) + // Read the secrets secret, err := leader.Client.Logical().Read("kv-wrapped/foo") if err != nil { @@ -176,17 +174,7 @@ func migrateFromShamirToTransit_Pre14(t *testing.T, logger hclog.Logger, storage t.Fatal(err) } - // Make sure the seal configs were updated correctly. - b, r, err := leader.Core.PhysicalSealConfigs(context.Background()) - if err != nil { - t.Fatal(err) - } - verifyBarrierConfig(t, b, wrapping.Transit, 1, 1, 1) - verifyBarrierConfig(t, r, wrapping.Shamir, keyShares, keyThreshold, 0) - - cluster.EnsureCoresSealed(t) - - return transitSeal + return opts.SealFunc } // TestSealMigration_ShamirToTransit_Post14 tests shamir-to-transit seal @@ -202,59 +190,25 @@ func testSealMigrationShamirToTransit_Post14(t *testing.T, logger hclog.Logger, cluster, opts := initializeShamir(t, logger, storage, basePort) // Create the transit server. - tss := sealhelper.NewTransitSealServer(t) - defer func() { - tss.EnsureCoresSealed(t) - tss.Cleanup() - }() - tss.MakeKey(t, "transit-seal-key") + tss := sealhelper.NewTransitSealServer(t, 0) + defer tss.Cleanup() + sealKeyName := "transit-seal-key-1" + tss.MakeKey(t, sealKeyName) // Migrate the backend from shamir to transit. - transitSeal := migrateFromShamirToTransit_Post14(t, logger, storage, basePort, tss, cluster, opts) - cluster.EnsureCoresSealed(t) - - cluster.Cleanup() - storage.Cleanup(t, cluster) - - // Run the backend with transit. - runTransit(t, logger, storage, basePort, cluster.RootToken, transitSeal) -} - -func migrateFromShamirToTransit_Post14(t *testing.T, logger hclog.Logger, storage teststorage.ReusableStorage, basePort int, tss *sealhelper.TransitSealServer, cluster *vault.TestCluster, opts *vault.TestClusterOptions) vault.Seal { - - // N.B. Providing a transit seal puts us in migration mode. - var transitSeal vault.Seal opts.SealFunc = func() vault.Seal { - transitSeal = tss.MakeSeal(t, "transit-seal-key") - return transitSeal - } - modifyCoreConfig := func(tcc *vault.TestClusterCore) { - tcc.CoreConfig.Seal = transitSeal + return tss.MakeSeal(t, sealKeyName) } // Restart each follower with the new config, and migrate to Transit. // Note that the barrier keys are being used as recovery keys. - leaderIdx := migratePost14(t, logger, storage, cluster, opts, cluster.BarrierKeys, modifyCoreConfig) - leader := cluster.Cores[leaderIdx] + leaderIdx := migratePost14(t, storage, cluster, opts, cluster.BarrierKeys) + validateMigration(t, storage, cluster, leaderIdx, verifySealConfigTransit) + cluster.Cleanup() + storage.Cleanup(t, cluster) - // Read the secret - secret, err := leader.Client.Logical().Read("kv-wrapped/foo") - if err != nil { - t.Fatal(err) - } - if diff := deep.Equal(secret.Data, map[string]interface{}{"zork": "quux"}); len(diff) > 0 { - t.Fatal(diff) - } - - // Make sure the seal configs were updated correctly. - b, r, err := leader.Core.PhysicalSealConfigs(context.Background()) - if err != nil { - t.Fatal(err) - } - verifyBarrierConfig(t, b, wrapping.Transit, 1, 1, 1) - verifyBarrierConfig(t, r, wrapping.Shamir, keyShares, keyThreshold, 0) - - return transitSeal + // Run the backend with transit. + runAutoseal(t, logger, storage, basePort, cluster.RootToken, opts.SealFunc) } // TestSealMigration_TransitToShamir_Post14 tests transit-to-shamir seal @@ -267,21 +221,25 @@ func TestSealMigration_TransitToShamir_Post14(t *testing.T) { func testSealMigrationTransitToShamir_Post14(t *testing.T, logger hclog.Logger, storage teststorage.ReusableStorage, basePort int) { // Create the transit server. - tss := sealhelper.NewTransitSealServer(t) + tss := sealhelper.NewTransitSealServer(t, 0) defer func() { if tss != nil { tss.Cleanup() } }() - tss.MakeKey(t, "transit-seal-key") + sealKeyName := "transit-seal-key-1" + tss.MakeKey(t, sealKeyName) // Initialize the backend with transit. - cluster, opts, transitSeal := initializeTransit(t, logger, storage, basePort, tss) + cluster, opts := initializeTransit(t, logger, storage, basePort, tss, sealKeyName) rootToken, recoveryKeys := cluster.RootToken, cluster.RecoveryKeys // Migrate the backend from transit to shamir - migrateFromTransitToShamir_Post14(t, logger, storage, basePort, tss, transitSeal, cluster, opts) - cluster.EnsureCoresSealed(t) + opts.UnwrapSealFunc = opts.SealFunc + opts.SealFunc = func() vault.Seal { return nil } + leaderIdx := migratePost14(t, storage, cluster, opts, cluster.RecoveryKeys) + validateMigration(t, storage, cluster, leaderIdx, verifySealConfigShamir) + cluster.Cleanup() storage.Cleanup(t, cluster) @@ -295,27 +253,12 @@ func testSealMigrationTransitToShamir_Post14(t *testing.T, logger hclog.Logger, runShamir(t, logger, storage, basePort, rootToken, recoveryKeys) } -func migrateFromTransitToShamir_Post14(t *testing.T, logger hclog.Logger, storage teststorage.ReusableStorage, basePort int, tss *sealhelper.TransitSealServer, transitSeal vault.Seal, cluster *vault.TestCluster, opts *vault.TestClusterOptions) { +func validateMigration(t *testing.T, storage teststorage.ReusableStorage, + cluster *vault.TestCluster, leaderIdx int, f func(t *testing.T, core *vault.TestClusterCore)) { + t.Helper() - opts.SealFunc = nil - modifyCoreConfig := func(tcc *vault.TestClusterCore) { - // Nil out the seal so it will be initialized as shamir. - tcc.CoreConfig.Seal = nil - - // N.B. Providing an UnwrapSeal puts us in migration mode. This is the - // equivalent of doing the following in HCL: - // seal "transit" { - // // ... - // disabled = "true" - // } - tcc.CoreConfig.UnwrapSeal = transitSeal - } - - // Restart each follower with the new config, and migrate to Shamir. - leaderIdx := migratePost14(t, logger, storage, cluster, opts, cluster.RecoveryKeys, modifyCoreConfig) leader := cluster.Cores[leaderIdx] - // Read the secret secret, err := leader.Client.Logical().Read("kv-wrapped/foo") if err != nil { t.Fatal(err) @@ -334,27 +277,70 @@ func migrateFromTransitToShamir_Post14(t *testing.T, logger hclog.Logger, storag testhelpers.WaitForRaftApply(t, core, appliedIndex) } - // Make sure the seal configs were updated correctly. - b, r, err := core.Core.PhysicalSealConfigs(context.Background()) - if err != nil { - t.Fatal(err) - } - verifyBarrierConfig(t, b, wrapping.Shamir, keyShares, keyThreshold, 1) - if r != nil { - t.Fatalf("expected nil recovery config, got: %#v", r) - } + f(t, core) } } -func migratePost14(t *testing.T, logger hclog.Logger, storage teststorage.ReusableStorage, cluster *vault.TestCluster, opts *vault.TestClusterOptions, unsealKeys [][]byte, modifyCoreConfig func(*vault.TestClusterCore)) int { +// TestSealMigration_TransitToTransit tests transit-to-shamir seal +// migration, using the post-1.4 method of bring individual nodes in the +// cluster to do the migration. +func TestSealMigration_TransitToTransit(t *testing.T) { + testVariousBackends(t, testSealMigration_TransitToTransit, basePort_TransitToTransit, true) +} +func testSealMigration_TransitToTransit(t *testing.T, logger hclog.Logger, + storage teststorage.ReusableStorage, basePort int) { + + // Create the transit server. + tss1 := sealhelper.NewTransitSealServer(t, 0) + defer func() { + if tss1 != nil { + tss1.Cleanup() + } + }() + sealKeyName := "transit-seal-key-1" + tss1.MakeKey(t, sealKeyName) + + // Initialize the backend with transit. + cluster, opts := initializeTransit(t, logger, storage, basePort, tss1, sealKeyName) + rootToken := cluster.RootToken + + // Create the transit server. + tss2 := sealhelper.NewTransitSealServer(t, 1) + defer func() { + tss2.Cleanup() + }() + tss2.MakeKey(t, "transit-seal-key-2") + + // Migrate the backend from transit to transit. + opts.UnwrapSealFunc = opts.SealFunc + opts.SealFunc = func() vault.Seal { + return tss2.MakeSeal(t, "transit-seal-key-2") + } + leaderIdx := migratePost14(t, storage, cluster, opts, cluster.RecoveryKeys) + validateMigration(t, storage, cluster, leaderIdx, verifySealConfigTransit) + cluster.Cleanup() + storage.Cleanup(t, cluster) + + // Now that migration is done, we can nuke the transit server, since we + // can unseal without it. + tss1.Cleanup() + tss1 = nil + + // Run the backend with transit. + runAutoseal(t, logger, storage, basePort, rootToken, opts.SealFunc) +} + +func migratePost14(t *testing.T, storage teststorage.ReusableStorage, cluster *vault.TestCluster, + opts *vault.TestClusterOptions, unsealKeys [][]byte) int { + + cluster.Logger = cluster.Logger.Named("migration") // Restart each follower with the new config, and migrate. for i := 1; i < len(cluster.Cores); i++ { cluster.StopCore(t, i) if storage.IsRaft { teststorage.CloseRaftStorage(t, cluster, i) } - modifyCoreConfig(cluster.Cores[i]) cluster.StartCore(t, i, opts) unsealMigrate(t, cluster.Cores[i].Client, unsealKeys, true) @@ -385,7 +371,7 @@ func migratePost14(t *testing.T, logger hclog.Logger, storage teststorage.Reusab } leader := cluster.Cores[leaderIdx] - // Wait for migration to occur on one of the 2 unsealed nodes + // Wait for migration to occur on the leader awaitMigration(t, leader.Client) var appliedIndex uint64 @@ -400,10 +386,9 @@ func migratePost14(t *testing.T, logger hclog.Logger, storage teststorage.Reusab teststorage.CloseRaftStorage(t, cluster, 0) } - // Modify the core - modifyCoreConfig(cluster.Cores[0]) - - // Bring core 0 back up + // Bring core 0 back up; we still have the seal migration config in place, + // but now that migration has been performed we should be able to unseal + // with the new seal and without using the `migrate` unseal option. cluster.StartCore(t, 0, opts) unseal(t, cluster.Cores[0].Client, unsealKeys) @@ -420,16 +405,16 @@ func migratePost14(t *testing.T, logger hclog.Logger, storage teststorage.Reusab func unsealMigrate(t *testing.T, client *api.Client, keys [][]byte, transitServerAvailable bool) { t.Helper() - for i, key := range keys { - // Try to unseal with missing "migrate" parameter - _, err := client.Sys().UnsealWithOptions(&api.UnsealOpts{ - Key: base64.StdEncoding.EncodeToString(key), - }) - if err == nil { - t.Fatal("expected error due to lack of migrate parameter") - } + if err := attemptUnseal(client, keys); err == nil { + t.Fatal("expected error due to lack of migrate parameter") + } + if err := attemptUnsealMigrate(client, keys, transitServerAvailable); err != nil { + t.Fatal(err) + } +} - // Unseal with "migrate" parameter +func attemptUnsealMigrate(client *api.Client, keys [][]byte, transitServerAvailable bool) error { + for i, key := range keys { resp, err := client.Sys().UnsealWithOptions(&api.UnsealOpts{ Key: base64.StdEncoding.EncodeToString(key), Migrate: true, @@ -438,26 +423,27 @@ func unsealMigrate(t *testing.T, client *api.Client, keys [][]byte, transitServe if i < keyThreshold-1 { // Not enough keys have been provided yet. if err != nil { - t.Fatal(err) + return err } } else { if transitServerAvailable { // The transit server is running. if err != nil { - t.Fatal(err) + return err } if resp == nil || resp.Sealed { - t.Fatalf("expected unsealed state; got %#v", resp) + return fmt.Errorf("expected unsealed state; got %#v", resp) } } else { // The transit server is stopped. if err == nil { - t.Fatal("expected error due to transit server being stopped.") + return fmt.Errorf("expected error due to transit server being stopped.") } } break } } + return nil } // awaitMigration waits for migration to finish. @@ -484,6 +470,12 @@ func awaitMigration(t *testing.T, client *api.Client) { func unseal(t *testing.T, client *api.Client, keys [][]byte) { t.Helper() + if err := attemptUnseal(client, keys); err != nil { + t.Fatal(err) + } +} + +func attemptUnseal(client *api.Client, keys [][]byte) error { for i, key := range keys { resp, err := client.Sys().UnsealWithOptions(&api.UnsealOpts{ @@ -492,18 +484,41 @@ func unseal(t *testing.T, client *api.Client, keys [][]byte) { if i < keyThreshold-1 { // Not enough keys have been provided yet. if err != nil { - t.Fatal(err) + return err } } else { if err != nil { - t.Fatal(err) + return err } if resp == nil || resp.Sealed { - t.Fatalf("expected unsealed state; got %#v", resp) + return fmt.Errorf("expected unsealed state; got %#v", resp) } break } } + return nil +} + +func verifySealConfigShamir(t *testing.T, core *vault.TestClusterCore) { + t.Helper() + b, r, err := core.PhysicalSealConfigs(context.Background()) + if err != nil { + t.Fatal(err) + } + verifyBarrierConfig(t, b, wrapping.Shamir, keyShares, keyThreshold, 1) + if r != nil { + t.Fatal("should not have recovery config for shamir") + } +} + +func verifySealConfigTransit(t *testing.T, core *vault.TestClusterCore) { + t.Helper() + b, r, err := core.PhysicalSealConfigs(context.Background()) + if err != nil { + t.Fatal(err) + } + verifyBarrierConfig(t, b, wrapping.Transit, 1, 1, 1) + verifyBarrierConfig(t, r, wrapping.Shamir, keyShares, keyThreshold, 0) } // verifyBarrierConfig verifies that a barrier configuration is correct. @@ -554,7 +569,7 @@ func initializeShamir(t *testing.T, logger hclog.Logger, storage teststorage.Reu } else { cluster.UnsealCores(t) } - testhelpers.WaitForNCoresUnsealed(t, cluster, len(cluster.Cores)) + testhelpers.WaitForActiveNodeAndStandbys(t, cluster) err := client.Sys().Mount("kv-wrapped", &api.MountInput{ SealWrap: true, @@ -640,29 +655,25 @@ func runShamir(t *testing.T, logger hclog.Logger, storage teststorage.ReusableSt if diff := deep.Equal(secret.Data, map[string]interface{}{"zork": "quux"}); len(diff) > 0 { t.Fatal(diff) } - - // Seal the cluster - cluster.EnsureCoresSealed(t) } // initializeTransit initializes a brand new backend storage with Transit. -func initializeTransit(t *testing.T, logger hclog.Logger, storage teststorage.ReusableStorage, basePort int, tss *sealhelper.TransitSealServer) (*vault.TestCluster, *vault.TestClusterOptions, vault.Seal) { +func initializeTransit(t *testing.T, logger hclog.Logger, storage teststorage.ReusableStorage, basePort int, + tss *sealhelper.TransitSealServer, sealKeyName string) (*vault.TestCluster, *vault.TestClusterOptions) { t.Helper() - var transitSeal vault.Seal var baseClusterPort = basePort + 10 // Start the cluster var conf = vault.CoreConfig{} var opts = vault.TestClusterOptions{ - Logger: logger, + Logger: logger.Named("initializeTransit"), HandlerFunc: vaulthttp.Handler, NumCores: numTestCores, BaseListenAddress: fmt.Sprintf("127.0.0.1:%d", basePort), BaseClusterListenPort: baseClusterPort, SealFunc: func() vault.Seal { - transitSeal = tss.MakeSeal(t, "transit-seal-key") - return transitSeal + return tss.MakeSeal(t, sealKeyName) }, } storage.Setup(&conf, &opts) @@ -698,16 +709,15 @@ func initializeTransit(t *testing.T, logger hclog.Logger, storage teststorage.Re t.Fatal(err) } - return cluster, &opts, transitSeal + return cluster, &opts } -func runTransit(t *testing.T, logger hclog.Logger, storage teststorage.ReusableStorage, basePort int, rootToken string, transitSeal vault.Seal) { +func runAutoseal(t *testing.T, logger hclog.Logger, storage teststorage.ReusableStorage, basePort int, rootToken string, sealFunc func() vault.Seal) { + var baseClusterPort = basePort + 10 // Start the cluster - var conf = vault.CoreConfig{ - Seal: transitSeal, - } + var conf = vault.CoreConfig{} var opts = vault.TestClusterOptions{ Logger: logger.Named("runTransit"), HandlerFunc: vaulthttp.Handler, @@ -715,6 +725,7 @@ func runTransit(t *testing.T, logger hclog.Logger, storage teststorage.ReusableS BaseListenAddress: fmt.Sprintf("127.0.0.1:%d", basePort), BaseClusterListenPort: baseClusterPort, SkipInit: true, + SealFunc: sealFunc, } storage.Setup(&conf, &opts) cluster := vault.NewTestCluster(t, &conf, &opts) @@ -771,9 +782,6 @@ func runTransit(t *testing.T, logger hclog.Logger, storage teststorage.ReusableS if diff := deep.Equal(secret.Data, map[string]interface{}{"zork": "quux"}); len(diff) > 0 { t.Fatal(diff) } - - // Seal the cluster - cluster.EnsureCoresSealed(t) } // joinRaftFollowers unseals the leader, and then joins-and-unseals the diff --git a/vault/generate_root.go b/vault/generate_root.go index ca8ab54e66..745f57d5a1 100644 --- a/vault/generate_root.go +++ b/vault/generate_root.go @@ -39,7 +39,7 @@ type GenerateRootStrategy interface { type generateStandardRootToken struct{} func (g generateStandardRootToken) authenticate(ctx context.Context, c *Core, combinedKey []byte) error { - masterKey, err := c.unsealKeyToMasterKey(ctx, combinedKey) + masterKey, err := c.unsealKeyToMasterKeyPostUnseal(ctx, combinedKey) if err != nil { return errwrap.Wrapf("unable to authenticate: {{err}}", err) } diff --git a/vault/generate_root_recovery.go b/vault/generate_root_recovery.go index e677802e28..4ad839ea22 100644 --- a/vault/generate_root_recovery.go +++ b/vault/generate_root_recovery.go @@ -21,7 +21,7 @@ type generateRecoveryToken struct { } func (g *generateRecoveryToken) authenticate(ctx context.Context, c *Core, combinedKey []byte) error { - key, err := c.unsealKeyToMasterKey(ctx, combinedKey) + key, err := c.unsealKeyToMasterKeyPostUnseal(ctx, combinedKey) if err != nil { return errwrap.Wrapf("unable to authenticate: {{err}}", err) } diff --git a/vault/init.go b/vault/init.go index 17aac352a3..87c0eb63a8 100644 --- a/vault/init.go +++ b/vault/init.go @@ -413,10 +413,13 @@ func (c *Core) UnsealWithStoredKeys(ctx context.Context) error { } // Disallow auto-unsealing when migrating - if c.IsInSealMigration() { + 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") @@ -434,27 +437,22 @@ func (c *Core) UnsealWithStoredKeys(ctx context.Context) error { if len(keys) == 0 { return NewNonFatalError(errors.New("stored unseal keys are supported, but none were found")) } - - unsealed := false - keysUsed := 0 - for _, key := range keys { - unsealed, err = c.Unseal(key) - if err != nil { - return NewNonFatalError(errwrap.Wrapf("unseal with stored key failed: {{err}}", err)) - } - keysUsed++ - if unsealed { - break - } + if len(keys) != 1 { + return NewNonFatalError(errors.New("expected exactly one stored key")) } - if !unsealed { + err = c.unsealInternal(ctx, keys[0]) + if err != nil { + return NewNonFatalError(errwrap.Wrapf("unseal with stored key failed: {{err}}", 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 keys", "stored_keys_used", keysUsed) + c.Logger().Warn("vault still sealed after using stored unseal key") } else { - c.Logger().Info("unsealed with stored keys", "stored_keys_used", keysUsed) + c.Logger().Info("unsealed with stored key") } return nil diff --git a/vault/seal.go b/vault/seal.go index e7589a6de0..f6e76e5741 100644 --- a/vault/seal.go +++ b/vault/seal.go @@ -5,6 +5,7 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "fmt" "github.com/hashicorp/vault/sdk/helper/jsonutil" "github.com/hashicorp/vault/sdk/physical" @@ -401,6 +402,36 @@ func (s *SealConfig) Clone() *SealConfig { return ret } +type ErrEncrypt struct { + Err error +} + +var _ error = &ErrEncrypt{} + +func (e *ErrEncrypt) Error() string { + return e.Err.Error() +} + +func (e *ErrEncrypt) Is(target error) bool { + _, ok := target.(*ErrEncrypt) + return ok || errors.Is(e.Err, target) +} + +type ErrDecrypt struct { + Err error +} + +var _ error = &ErrDecrypt{} + +func (e *ErrDecrypt) Error() string { + return e.Err.Error() +} + +func (e *ErrDecrypt) Is(target error) bool { + _, ok := target.(*ErrDecrypt) + return ok || errors.Is(e.Err, target) +} + func writeStoredKeys(ctx context.Context, storage physical.Backend, encryptor *seal.Access, keys [][]byte) error { if keys == nil { return fmt.Errorf("keys were nil") @@ -417,7 +448,7 @@ func writeStoredKeys(ctx context.Context, storage physical.Backend, encryptor *s // Encrypt and marshal the keys blobInfo, err := encryptor.Encrypt(ctx, buf, nil) if err != nil { - return errwrap.Wrapf("failed to encrypt keys for storage: {{err}}", err) + return &ErrEncrypt{Err: errwrap.Wrapf("failed to encrypt keys for storage: {{err}}", err)} } value, err := proto.Marshal(blobInfo) @@ -457,7 +488,7 @@ func readStoredKeys(ctx context.Context, storage physical.Backend, encryptor *se pt, err := encryptor.Decrypt(ctx, blobInfo, nil) if err != nil { - return nil, errwrap.Wrapf("failed to decrypt encrypted stored keys: {{err}}", err) + return nil, &ErrDecrypt{Err: errwrap.Wrapf("failed to encrypt keys for storage: {{err}}", err)} } // Decode the barrier entry diff --git a/vault/seal/seal.go b/vault/seal/seal.go index 0df1cf0bf4..2b318cfd86 100644 --- a/vault/seal/seal.go +++ b/vault/seal/seal.go @@ -49,6 +49,7 @@ func (a *Access) Type() string { return a.Wrapper.Type() } +// Encrypt uses the underlying seal to encrypt the plaintext and returns it. func (a *Access) Encrypt(ctx context.Context, plaintext, aad []byte) (blob *wrapping.EncryptedBlobInfo, err error) { defer func(now time.Time) { metrics.MeasureSince([]string{"seal", "encrypt", "time"}, now) @@ -66,6 +67,9 @@ func (a *Access) Encrypt(ctx context.Context, plaintext, aad []byte) (blob *wrap return a.Wrapper.Encrypt(ctx, plaintext, aad) } +// Decrypt uses the underlying seal to decrypt the cryptotext and returns it. +// Note that it is possible depending on the wrapper used that both pt and err +// are populated. func (a *Access) Decrypt(ctx context.Context, data *wrapping.EncryptedBlobInfo, aad []byte) (pt []byte, err error) { defer func(now time.Time) { metrics.MeasureSince([]string{"seal", "decrypt", "time"}, now) diff --git a/vault/testing.go b/vault/testing.go index 89ca6daeaf..b32e695cd7 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -295,10 +295,6 @@ func TestCoreUnseal(core *Core, key []byte) (bool, error) { return core.Unseal(key) } -func TestCoreUnsealWithRecoveryKeys(core *Core, key []byte) (bool, error) { - return core.UnsealWithRecoveryKeys(key) -} - // TestCoreUnsealed returns a pure in-memory core that is already // initialized and unsealed. func TestCoreUnsealed(t testing.T) (*Core, [][]byte, string) { @@ -830,6 +826,7 @@ func (c *TestCluster) UnsealCoresWithError(useStoredKeys bool) error { } func (c *TestCluster) UnsealCore(t testing.T, core *TestClusterCore) { + t.Helper() var keys [][]byte if core.seal.RecoveryKeySupported() { keys = c.RecoveryKeys @@ -844,6 +841,7 @@ func (c *TestCluster) UnsealCore(t testing.T, core *TestClusterCore) { } func (c *TestCluster) UnsealCoreWithStoredKeys(t testing.T, core *TestClusterCore) { + t.Helper() if err := core.UnsealWithStoredKeys(context.Background()); err != nil { t.Fatal(err) } @@ -1018,12 +1016,13 @@ type TestClusterOptions struct { // do not clash with any other explicitly assigned ports in other tests. BaseClusterListenPort int - NumCores int - SealFunc func() Seal - Logger log.Logger - TempDir string - CACert []byte - CAKey *ecdsa.PrivateKey + NumCores int + SealFunc func() Seal + UnwrapSealFunc func() Seal + Logger log.Logger + TempDir string + CACert []byte + CAKey *ecdsa.PrivateKey // PhysicalFactory is used to create backends. // The int argument is the index of the core within the cluster, i.e. first // core in cluster will have 0, second 1, etc. @@ -1702,6 +1701,9 @@ func (testCluster *TestCluster) newCore(t testing.T, idx int, coreConfig *CoreCo if opts != nil && opts.SealFunc != nil { localConfig.Seal = opts.SealFunc() } + if opts != nil && opts.UnwrapSealFunc != nil { + localConfig.UnwrapSeal = opts.UnwrapSealFunc() + } if coreConfig.Logger == nil || (opts != nil && opts.Logger != nil) { localConfig.Logger = testCluster.Logger.Named(fmt.Sprintf("core%d", idx))