Add the ability to unseal using recovery keys via an explicit seal option. (#18683)

* 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>
This commit is contained in:
Scott Miller 2023-01-24 14:57:56 -06:00 committed by GitHub
parent b69dad8a05
commit 25960fd034
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 354 additions and 92 deletions

View File

@ -51,14 +51,15 @@ func (c *Sys) InitWithContext(ctx context.Context, opts *InitRequest) (*InitResp
} }
type InitRequest struct { type InitRequest struct {
SecretShares int `json:"secret_shares"` SecretShares int `json:"secret_shares"`
SecretThreshold int `json:"secret_threshold"` SecretThreshold int `json:"secret_threshold"`
StoredShares int `json:"stored_shares"` StoredShares int `json:"stored_shares"`
PGPKeys []string `json:"pgp_keys"` PGPKeys []string `json:"pgp_keys"`
RecoveryShares int `json:"recovery_shares"` RecoveryShares int `json:"recovery_shares"`
RecoveryThreshold int `json:"recovery_threshold"` RecoveryThreshold int `json:"recovery_threshold"`
RecoveryPGPKeys []string `json:"recovery_pgp_keys"` RecoveryPGPKeys []string `json:"recovery_pgp_keys"`
RootTokenPGPKey string `json:"root_token_pgp_key"` RootTokenPGPKey string `json:"root_token_pgp_key"`
UnsealRecoveryDisabled bool `json:"disable_unseal_recovery"`
} }
type InitStatusResponse struct { type InitStatusResponse struct {
@ -66,9 +67,10 @@ type InitStatusResponse struct {
} }
type InitResponse struct { type InitResponse struct {
Keys []string `json:"keys"` Keys []string `json:"keys"`
KeysB64 []string `json:"keys_base64"` KeysB64 []string `json:"keys_base64"`
RecoveryKeys []string `json:"recovery_keys"` RecoveryKeys []string `json:"recovery_keys"`
RecoveryKeysB64 []string `json:"recovery_keys_base64"` RecoveryKeysB64 []string `json:"recovery_keys_base64"`
RootToken string `json:"root_token"` RootToken string `json:"root_token"`
UnsealRecoveryAvailable bool `json:"unseal_recovery_available"`
} }

4
changelog/18683.txt Normal file
View File

@ -0,0 +1,4 @@
```release-note:change
core: recovery keys may now be used to unseal an auto-sealed Vault via an
opt-out unseal recovery mode.
```

View File

@ -29,10 +29,11 @@ type OperatorInitCommand struct {
flagRootTokenPGPKey string flagRootTokenPGPKey string
// Auto Unseal // Auto Unseal
flagRecoveryShares int flagRecoveryShares int
flagRecoveryThreshold int flagRecoveryThreshold int
flagRecoveryPGPKeys []string flagRecoveryPGPKeys []string
flagStoredShares int flagStoredShares int
flagDisableUnsealRecovery bool
// Consul // Consul
flagConsulAuto bool flagConsulAuto bool
@ -149,6 +150,13 @@ func (c *OperatorInitCommand) Flags() *FlagSets {
Usage: "DEPRECATED: This flag does nothing. It will be removed in Vault 1.3.", Usage: "DEPRECATED: This flag does nothing. It will be removed in Vault 1.3.",
}) })
f.BoolVar(&BoolVar{
Name: "disable-unseal-recovery",
Target: &c.flagDisableUnsealRecovery,
Default: false,
Usage: "If disabled, unsealing Vault using recovery keys is not possible.",
})
// Consul Options // Consul Options
f = set.NewFlagSet("Consul Options") f = set.NewFlagSet("Consul Options")
@ -280,9 +288,10 @@ func (c *OperatorInitCommand) Run(args []string) int {
PGPKeys: c.flagPGPKeys, PGPKeys: c.flagPGPKeys,
RootTokenPGPKey: c.flagRootTokenPGPKey, RootTokenPGPKey: c.flagRootTokenPGPKey,
RecoveryShares: c.flagRecoveryShares, RecoveryShares: c.flagRecoveryShares,
RecoveryThreshold: c.flagRecoveryThreshold, RecoveryThreshold: c.flagRecoveryThreshold,
RecoveryPGPKeys: c.flagRecoveryPGPKeys, RecoveryPGPKeys: c.flagRecoveryPGPKeys,
UnsealRecoveryDisabled: c.flagDisableUnsealRecovery,
} }
// Check auto mode // Check auto mode

View File

@ -2378,7 +2378,7 @@ func setSeal(c *ServerCommand, config *server.Config, infoKeys []string, info ma
config.Seals = append(config.Seals, &configutil.KMS{Type: wrapping.WrapperTypeShamir.String()}) config.Seals = append(config.Seals, &configutil.KMS{Type: wrapping.WrapperTypeShamir.String()})
} }
} }
var createdSeals []vault.Seal = make([]vault.Seal, len(config.Seals)) createdSeals := make([]vault.Seal, len(config.Seals))
for _, configSeal := range config.Seals { for _, configSeal := range config.Seals {
sealType := wrapping.WrapperTypeShamir.String() sealType := wrapping.WrapperTypeShamir.String()
if !configSeal.Disabled && os.Getenv("VAULT_SEAL_TYPE") != "" { if !configSeal.Disabled && os.Getenv("VAULT_SEAL_TYPE") != "" {
@ -2403,7 +2403,11 @@ func setSeal(c *ServerCommand, config *server.Config, infoKeys []string, info ma
"Error parsing Seal configuration: %s", sealConfigError) "Error parsing Seal configuration: %s", sealConfigError)
} }
} }
if wrapper == nil { if configSeal.Recover {
seal = vault.NewRecoverySeal(&vaultseal.Access{
Wrapper: aeadwrapper.NewShamirWrapper(),
})
} else if wrapper == nil {
seal = defaultSeal seal = defaultSeal
} else { } else {
var err error var err error
@ -2428,6 +2432,7 @@ func setSeal(c *ServerCommand, config *server.Config, infoKeys []string, info ma
} }
createdSeals = append(createdSeals, seal) createdSeals = append(createdSeals, seal)
} }
return barrierSeal, barrierWrapper, unwrapSeal, createdSeals, sealConfigError, nil return barrierSeal, barrierWrapper, unwrapSeal, createdSeals, sealConfigError, nil
} }
@ -2617,9 +2622,7 @@ func runUnseal(c *ServerCommand, core *vault.Core, ctx context.Context) {
} }
} }
func createCoreConfig(c *ServerCommand, config *server.Config, backend physical.Backend, configSR sr.ServiceRegistration, barrierSeal, unwrapSeal vault.Seal, func createCoreConfig(c *ServerCommand, config *server.Config, backend physical.Backend, configSR sr.ServiceRegistration, barrierSeal, unwrapSeal vault.Seal, metricsHelper *metricsutil.MetricsHelper, metricSink *metricsutil.ClusterMetricSink, secureRandomReader io.Reader) vault.CoreConfig {
metricsHelper *metricsutil.MetricsHelper, metricSink *metricsutil.ClusterMetricSink, secureRandomReader io.Reader,
) vault.CoreConfig {
coreConfig := &vault.CoreConfig{ coreConfig := &vault.CoreConfig{
RawConfig: config, RawConfig: config,
Physical: backend, Physical: backend,

View File

@ -482,12 +482,16 @@ func CheckConfig(c *Config, e error) (*Config, error) {
return c, e return c, e
} }
if len(c.Seals) == 2 { switch len(c.Seals) {
case 2:
// Two seals indicates a seal migration, but one and only one must be disabled
switch { switch {
case c.Seals[0].Disabled && c.Seals[1].Disabled: case c.Seals[0].Disabled && c.Seals[1].Disabled:
return nil, errors.New("seals: two seals provided but both are disabled") return nil, errors.New("seals: two seals provided but both are disabled")
case !c.Seals[0].Disabled && !c.Seals[1].Disabled: case !c.Seals[0].Disabled && !c.Seals[1].Disabled:
return nil, errors.New("seals: two seals provided but neither is disabled") return nil, errors.New("seals: two seals provided but neither is disabled")
case (!c.Seals[0].Disabled && c.Seals[0].Recover) || (!c.Seals[1].Disabled && c.Seals[1].Recover):
return nil, errors.New("seals: migration target seal cannot be in recovery mode")
} }
} }

View File

@ -61,9 +61,10 @@ func handleSysInitPut(core *vault.Core, w http.ResponseWriter, r *http.Request)
} }
recoveryConfig := &vault.SealConfig{ recoveryConfig := &vault.SealConfig{
SecretShares: req.RecoveryShares, SecretShares: req.RecoveryShares,
SecretThreshold: req.RecoveryThreshold, SecretThreshold: req.RecoveryThreshold,
PGPKeys: req.RecoveryPGPKeys, PGPKeys: req.RecoveryPGPKeys,
DisableUnsealRecovery: req.DisableUnsealRecovery,
} }
initParams := &vault.InitParams{ initParams := &vault.InitParams{
@ -115,14 +116,15 @@ func handleSysInitPut(core *vault.Core, w http.ResponseWriter, r *http.Request)
} }
type InitRequest struct { type InitRequest struct {
SecretShares int `json:"secret_shares"` SecretShares int `json:"secret_shares"`
SecretThreshold int `json:"secret_threshold"` SecretThreshold int `json:"secret_threshold"`
StoredShares int `json:"stored_shares"` StoredShares int `json:"stored_shares"`
PGPKeys []string `json:"pgp_keys"` PGPKeys []string `json:"pgp_keys"`
RecoveryShares int `json:"recovery_shares"` RecoveryShares int `json:"recovery_shares"`
RecoveryThreshold int `json:"recovery_threshold"` RecoveryThreshold int `json:"recovery_threshold"`
RecoveryPGPKeys []string `json:"recovery_pgp_keys"` RecoveryPGPKeys []string `json:"recovery_pgp_keys"`
RootTokenPGPKey string `json:"root_token_pgp_key"` RootTokenPGPKey string `json:"root_token_pgp_key"`
DisableUnsealRecovery bool `json:"disable_unseal_recovery"`
} }
type InitResponse struct { type InitResponse struct {

View File

@ -51,6 +51,7 @@ type KMS struct {
Purpose []string `hcl:"-"` Purpose []string `hcl:"-"`
Disabled bool Disabled bool
Recover bool
Config map[string]string Config map[string]string
} }
@ -99,6 +100,15 @@ func parseKMS(result *[]*KMS, list *ast.ObjectList, blockName string, maxKMS int
delete(m, "disabled") delete(m, "disabled")
} }
var recover bool
if v, ok := m["recovery_keys_fallback"]; ok {
recover, err = parseutil.ParseBool(v)
if err != nil {
return multierror.Prefix(err, fmt.Sprintf("%s.%s:", blockName, key))
}
delete(m, "recovery_keys_fallback")
}
strMap := make(map[string]string, len(m)) strMap := make(map[string]string, len(m))
for k, v := range m { for k, v := range m {
s, err := parseutil.ParseString(v) s, err := parseutil.ParseString(v)
@ -112,6 +122,7 @@ func parseKMS(result *[]*KMS, list *ast.ObjectList, blockName string, maxKMS int
Type: strings.ToLower(key), Type: strings.ToLower(key),
Purpose: purpose, Purpose: purpose,
Disabled: disabled, Disabled: disabled,
Recover: recover,
} }
if len(strMap) > 0 { if len(strMap) > 0 {
seal.Config = strMap seal.Config = strMap

View File

@ -1696,7 +1696,11 @@ func (c *Core) sealMigrated(ctx context.Context) (bool, error) {
return false, nil return false, nil
} }
if c.seal.BarrierType() != c.migrationInfo.seal.BarrierType() { // If the types of the seals differ, e.g. auto->shamir or shamir->auto, we're done. BUT,
// with an auto seal in recovery mode as the migration seal, they will match even though
// the migration seal was really an auto seal
if c.seal.BarrierType() != c.migrationInfo.seal.BarrierType() ||
(isAutoSeal(c.migrationInfo.seal) && c.seal.BarrierType() == wrapping.WrapperTypeShamir) {
return true, nil return true, nil
} }
@ -1748,20 +1752,34 @@ func (c *Core) migrateSeal(ctx context.Context) error {
return fmt.Errorf("error getting recovery key to set on new seal: %w", err) return fmt.Errorf("error getting recovery key to set on new seal: %w", err)
} }
if err := c.seal.SetRecoveryKey(ctx, recoveryKey); err != nil { if err := c.migrateAutoToAuto(ctx, recoveryKey); err != nil {
return fmt.Errorf("error setting new recovery key information during migrate: %w", err) return err
} }
case isUnsealRecoverySeal(c.migrationInfo.seal) && c.seal.RecoveryKeySupported():
c.logger.Info("migrating from one auto-unseal to another", "from",
c.migrationInfo.seal.BarrierType(), "to", c.seal.BarrierType())
barrierKeys, err := c.migrationInfo.seal.GetStoredKeys(ctx) recoveryKey, err := c.migrationInfo.seal.UnsealRecoveryKey(ctx)
if err != nil { if err != nil {
return fmt.Errorf("error getting stored keys to set on new seal: %w", err) return fmt.Errorf("error getting recovery key to set on new seal: %w", err)
} }
if err := c.seal.SetStoredKeys(ctx, barrierKeys); err != nil { if err := c.migrateAutoToAuto(ctx, recoveryKey); err != nil {
return fmt.Errorf("error setting new barrier key information during migrate: %w", err) return err
}
case isUnsealRecoverySeal(c.migrationInfo.seal):
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
recoveryKey, err := c.migrationInfo.seal.UnsealRecoveryKey(ctx)
if err != nil {
return fmt.Errorf("error getting recovery key to set on new seal: %w", err)
} }
case c.migrationInfo.seal.RecoveryKeySupported(): if err := c.migrateAutoToShamir(ctx, recoveryKey); err != nil {
return err
}
case isAutoSeal(c.migrationInfo.seal):
c.logger.Info("migrating from one auto-unseal to shamir", "from", c.migrationInfo.seal.BarrierType()) 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 // Auto to Shamir, since recovery key isn't supported on new seal
@ -1770,21 +1788,9 @@ func (c *Core) migrateSeal(ctx context.Context) error {
return fmt.Errorf("error getting recovery key to set on new seal: %w", err) return fmt.Errorf("error getting recovery key to set on new seal: %w", err)
} }
// We have recovery keys; we're going to use them as the new shamir KeK. if err := c.migrateAutoToShamir(ctx, recoveryKey); err != nil {
err = c.seal.GetAccess().Wrapper.(*aeadwrapper.ShamirWrapper).SetAesGcmKeyBytes(recoveryKey) return err
if err != nil {
return fmt.Errorf("failed to set master key in seal: %w", err)
} }
barrierKeys, err := c.migrationInfo.seal.GetStoredKeys(ctx)
if err != nil {
return fmt.Errorf("error getting stored keys to set on new seal: %w", err)
}
if err := c.seal.SetStoredKeys(ctx, barrierKeys); err != nil {
return fmt.Errorf("error setting new barrier key information during migrate: %w", err)
}
case c.seal.RecoveryKeySupported(): case c.seal.RecoveryKeySupported():
c.logger.Info("migrating from shamir to auto-unseal", "to", c.seal.BarrierType()) 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 // Migration is happening from shamir -> auto. In this case use the shamir
@ -1810,6 +1816,16 @@ func (c *Core) migrateSeal(ctx context.Context) error {
return fmt.Errorf("error storing new master key: %w", err) return fmt.Errorf("error storing new master key: %w", err)
} }
// Store the unseal recovery key
wrapper := aeadwrapper.NewShamirWrapper()
wrapper.SetAesGcmKeyBytes(c.migrationInfo.unsealKey)
recoverySeal := NewRecoverySeal(&vaultseal.Access{
Wrapper: wrapper,
})
recoverySeal.SetCore(c)
if err := recoverySeal.SetStoredKeys(ctx, [][]byte{newMasterKey}); err != nil {
c.logger.Error("failed to store recovery unseal keys", "error", err)
}
default: default:
return errors.New("unhandled migration case (shamir to shamir)") return errors.New("unhandled migration case (shamir to shamir)")
} }
@ -1826,6 +1842,40 @@ func (c *Core) migrateSeal(ctx context.Context) error {
return nil return nil
} }
func (c *Core) migrateAutoToAuto(ctx context.Context, recoveryKey []byte) error {
if err := c.seal.SetRecoveryKey(ctx, recoveryKey); err != nil {
return fmt.Errorf("error setting new recovery key information during migrate: %w", err)
}
barrierKeys, err := c.migrationInfo.seal.GetStoredKeys(ctx)
if err != nil {
return fmt.Errorf("error getting stored keys to set on new seal: %w", err)
}
if err := c.seal.SetStoredKeys(ctx, barrierKeys); err != nil {
return fmt.Errorf("error setting new barrier key information during migrate: %w", err)
}
return nil
}
func (c *Core) migrateAutoToShamir(ctx context.Context, recoveryKey []byte) error {
// 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 fmt.Errorf("failed to set master key in seal: %w", err)
}
barrierKeys, err := c.migrationInfo.seal.GetStoredKeys(ctx)
if err != nil {
return fmt.Errorf("error getting stored keys to set on new seal: %w", err)
}
if err := c.seal.SetStoredKeys(ctx, barrierKeys); err != nil {
return fmt.Errorf("error setting new barrier key information during migrate: %w", err)
}
return nil
}
// unsealInternal takes in the master key and attempts to unseal the barrier. // unsealInternal takes in the master key and attempts to unseal the barrier.
// N.B.: This must be called with the state write lock held. // N.B.: This must be called with the state write lock held.
func (c *Core) unsealInternal(ctx context.Context, masterKey []byte) error { func (c *Core) unsealInternal(ctx context.Context, masterKey []byte) error {
@ -2755,6 +2805,12 @@ func (c *Core) adjustForSealMigration(unwrapSeal Seal) error {
// We have the same barrier type and the unwrap seal is nil so we're not // 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. // migrating from same to same, IOW we assume it's not a migration.
return nil return nil
case c.seal.BarrierType() == wrapping.WrapperTypeShamir && isUnsealRecoverySeal(c.seal):
// The stored barrier config is not shamir, but we have a shamir seal anyway, because
// seal recovery mode has been requested. Note that this isn't for migration, but this function
// is called regardless of whether migration is occurring or not, and this is a valid state
// for the seal to be in, thus we mustn't reject it.
return nil
case c.seal.BarrierType() == wrapping.WrapperTypeShamir: case c.seal.BarrierType() == wrapping.WrapperTypeShamir:
// The stored barrier config is not shamir, there is no disabled seal // The stored barrier config is not shamir, there is no disabled seal
// in config, and either no configured seal (which equates to Shamir) // in config, and either no configured seal (which equates to Shamir)
@ -2778,7 +2834,7 @@ func (c *Core) adjustForSealMigration(unwrapSeal Seal) error {
// If we're not coming from Shamir we expect the previous seal to be // If we're not coming from Shamir we expect the previous seal to be
// in the config and disabled. // in the config and disabled.
if unwrapSeal.BarrierType() == wrapping.WrapperTypeShamir { if unwrapSeal.BarrierType() == wrapping.WrapperTypeShamir && !isUnsealRecoverySeal(unwrapSeal) {
return errors.New("Shamir seals cannot be set disabled (they should simply not be set)") return errors.New("Shamir seals cannot be set disabled (they should simply not be set)")
} }
} }
@ -2814,6 +2870,21 @@ func (c *Core) adjustForSealMigration(unwrapSeal Seal) error {
return nil return nil
} }
// With the addition of unseal recovery mode, we need a more subtle check for whether a Seal is an auto
// seal, as it may be a Shamir seal in due to recovery mode but would otherwise have been an auto seal.
func isAutoSeal(seal Seal) bool {
return seal.RecoveryKeySupported() || isUnsealRecoverySeal(seal)
}
// Returns whether a seal is a recovery seal, e.g. a shamir seal pointing at the root key encrypted by
// recovery keys rather than the seal wrapper.
func isUnsealRecoverySeal(seal Seal) bool {
if ds, ok := seal.(*defaultSeal); ok {
return ds.unsealKeyPath == recoveryUnsealKeyPath
}
return false
}
func (c *Core) migrateSealConfig(ctx context.Context) error { func (c *Core) migrateSealConfig(ctx context.Context) error {
existBarrierSealConfig, existRecoverySealConfig, err := c.PhysicalSealConfigs(ctx) existBarrierSealConfig, existRecoverySealConfig, err := c.PhysicalSealConfigs(ctx)
if err != nil { if err != nil {
@ -2823,10 +2894,10 @@ func (c *Core) migrateSealConfig(ctx context.Context) error {
var bc, rc *SealConfig var bc, rc *SealConfig
switch { switch {
case c.migrationInfo.seal.RecoveryKeySupported() && c.seal.RecoveryKeySupported(): case isAutoSeal(c.migrationInfo.seal) && c.seal.RecoveryKeySupported():
// Migrating from auto->auto, copy the configs over // Migrating from auto->auto, copy the configs over
bc, rc = existBarrierSealConfig, existRecoverySealConfig bc, rc = existBarrierSealConfig, existRecoverySealConfig
case c.migrationInfo.seal.RecoveryKeySupported(): case isAutoSeal(c.migrationInfo.seal):
// Migrating from auto->shamir, clone auto's recovery config and set // Migrating from auto->shamir, clone auto's recovery config and set
// stored keys to 1. // stored keys to 1.
bc = existRecoverySealConfig.Clone() bc = existRecoverySealConfig.Clone()
@ -2863,7 +2934,7 @@ func (c *Core) migrateSealConfig(ctx context.Context) error {
func (c *Core) adjustSealConfigDuringMigration(existBarrierSealConfig, existRecoverySealConfig *SealConfig) { func (c *Core) adjustSealConfigDuringMigration(existBarrierSealConfig, existRecoverySealConfig *SealConfig) {
switch { switch {
case c.migrationInfo.seal.RecoveryKeySupported() && existRecoverySealConfig != nil: case isAutoSeal(c.migrationInfo.seal) && existRecoverySealConfig != nil:
// Migrating from auto->shamir, clone auto's recovery config and set // Migrating from auto->shamir, clone auto's recovery config and set
// stored keys to 1. Unless the recover config doesn't exist, in which // stored keys to 1. Unless the recover config doesn't exist, in which
// case the migration is assumed to already have been performed. // case the migration is assumed to already have been performed.

View File

@ -59,6 +59,11 @@ func TestSealMigration_TransitToShamir_Post14(t *testing.T) {
testVariousBackends(t, ParamTestSealMigrationTransitToShamir_Post14, BasePort_TransitToShamir_Post14, true) testVariousBackends(t, ParamTestSealMigrationTransitToShamir_Post14, BasePort_TransitToShamir_Post14, true)
} }
func TestSealMigration_TransitToShamir_Recovery(t *testing.T) {
t.Parallel()
testVariousBackends(t, ParamTestSealMigrationTransitToShamir_Recovery, BasePort_TransitToShamir_Recovery, true)
}
// TestSealMigration_TransitToTransit tests transit-to-shamir seal // TestSealMigration_TransitToTransit tests transit-to-shamir seal
// migration, using the post-1.4 method of bring individual nodes in the // migration, using the post-1.4 method of bring individual nodes in the
// cluster to do the migration. // cluster to do the migration.

View File

@ -7,6 +7,9 @@ import (
"testing" "testing"
"time" "time"
"github.com/hashicorp/go-kms-wrapping/wrappers/aead/v2"
"github.com/hashicorp/vault/vault/seal"
"github.com/go-test/deep" "github.com/go-test/deep"
"github.com/hashicorp/go-hclog" "github.com/hashicorp/go-hclog"
wrapping "github.com/hashicorp/go-kms-wrapping/v2" wrapping "github.com/hashicorp/go-kms-wrapping/v2"
@ -25,11 +28,12 @@ const (
keyShares = 3 keyShares = 3
keyThreshold = 3 keyThreshold = 3
BasePort_ShamirToTransit_Pre14 = 20000 BasePort_ShamirToTransit_Pre14 = 20000
BasePort_TransitToShamir_Pre14 = 21000 BasePort_TransitToShamir_Pre14 = 21000
BasePort_ShamirToTransit_Post14 = 22000 BasePort_ShamirToTransit_Post14 = 22000
BasePort_TransitToShamir_Post14 = 23000 BasePort_TransitToShamir_Post14 = 23000
BasePort_TransitToTransit = 24000 BasePort_TransitToTransit = 24000
BasePort_TransitToShamir_Recovery = 25000
) )
func ParamTestSealMigrationTransitToShamir_Pre14(t *testing.T, logger hclog.Logger, storage teststorage.ReusableStorage, basePort int) { func ParamTestSealMigrationTransitToShamir_Pre14(t *testing.T, logger hclog.Logger, storage teststorage.ReusableStorage, basePort int) {
@ -120,6 +124,55 @@ func ParamTestSealMigrationTransitToShamir_Post14(t *testing.T, logger hclog.Log
runShamir(t, logger, storage, basePort, rootToken, recoveryKeys) runShamir(t, logger, storage, basePort, rootToken, recoveryKeys)
} }
func ParamTestSealMigrationTransitToShamir_Recovery(t *testing.T, logger hclog.Logger, storage teststorage.ReusableStorage, basePort int) {
// Create the transit server.
tss := sealhelper.NewTransitSealServer(t, 0)
defer func() {
if tss != nil {
tss.Cleanup()
}
}()
sealKeyName := "transit-seal-key-1"
tss.MakeKey(t, sealKeyName)
// Initialize the backend with transit.
cluster, opts := InitializeTransit(t, logger, storage, basePort, tss, sealKeyName)
rootToken, recoveryKeys := cluster.RootToken, cluster.RecoveryKeys
// Disable the transit seal, forcing recovery
tss.Cleanup()
tss = nil
// conf := cluster.Cores[0].GetCoreConfigInternal()
// conf.Seals[0].Recover = true
opts.UnwrapSealFunc = func() vault.Seal {
seal := vault.NewRecoverySeal(&seal.Access{
Wrapper: aead.NewShamirWrapper(),
WrapperType: wrapping.WrapperTypeShamir,
})
seal.SetCachedBarrierConfig(&vault.SealConfig{
SecretShares: len(recoveryKeys),
SecretThreshold: len(recoveryKeys),
StoredShares: len(recoveryKeys),
})
return seal
}
// Migrate the backend from transit to shamir
// 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)
// Run the backend with shamir. Note that the recovery keys are now the
// barrier keys.
runShamir(t, logger, storage, basePort, rootToken, recoveryKeys)
}
func ParamTestSealMigrationShamirToTransit_Post14(t *testing.T, logger hclog.Logger, storage teststorage.ReusableStorage, basePort int) { func ParamTestSealMigrationShamirToTransit_Post14(t *testing.T, logger hclog.Logger, storage teststorage.ReusableStorage, basePort int) {
// Initialize the backend using shamir // Initialize the backend using shamir
cluster, opts := initializeShamir(t, logger, storage, basePort) cluster, opts := initializeShamir(t, logger, storage, basePort)

View File

@ -386,6 +386,18 @@ func (c *Core) Initialize(ctx context.Context, initParams *InitParams) (*InitRes
} }
results.RecoveryShares = recoveryUnsealKeys 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)
}
}
} }
} }

View File

@ -252,7 +252,9 @@ func (c *Core) RecoveryRekeyInit(config *SealConfig) logical.HTTPCodedError {
c.logger.Error("invalid recovery configuration", "error", err) c.logger.Error("invalid recovery configuration", "error", err)
return logical.CodedError(http.StatusInternalServerError, fmt.Errorf("invalid recovery configuration: %w", err).Error()) return logical.CodedError(http.StatusInternalServerError, fmt.Errorf("invalid recovery configuration: %w", err).Error())
} }
if isUnsealRecoverySeal(c.seal) {
return logical.CodedError(http.StatusBadRequest, "cannot rekey while in unseal recovery mode")
}
if !c.seal.RecoveryKeySupported() { if !c.seal.RecoveryKeySupported() {
return logical.CodedError(http.StatusBadRequest, "recovery keys not supported") return logical.CodedError(http.StatusBadRequest, "recovery keys not supported")
} }
@ -757,6 +759,31 @@ func (c *Core) RecoveryRekeyUpdate(ctx context.Context, key []byte, nonce string
return nil, logical.CodedError(http.StatusInternalServerError, fmt.Errorf("failed to perform recovery rekey: %w", err).Error()) return nil, logical.CodedError(http.StatusInternalServerError, fmt.Errorf("failed to perform recovery rekey: %w", err).Error())
} }
// Store the unseal recovery key
wrapper := aeadwrapper.NewShamirWrapper()
// TODO, just test if keys existed in the recovery path
keys, err := c.physical.Get(ctx, recoveryUnsealKeyPath)
if err != nil {
return nil, logical.CodedError(http.StatusInternalServerError, fmt.Errorf("failed to perform recovery rekey, failed testing for recovery unseal keys: %w", err).Error())
}
if keys != nil {
rootKeys, err := c.seal.GetStoredKeys(ctx)
if err != nil {
return nil, logical.CodedError(http.StatusInternalServerError, fmt.Errorf("failed to perform recovery rekey, failed retrieving root keys: %w", err).Error())
}
wrapper.SetAesGcmKeyBytes(newRecoveryKey)
recoverySeal := NewRecoverySeal(&seal.Access{
Wrapper: wrapper,
})
recoverySeal.SetCore(c)
if err := recoverySeal.SetStoredKeys(ctx, rootKeys); err != nil {
c.logger.Error("failed to store recovery unseal keys", "error", err)
}
}
c.recoveryRekeyConfig = nil c.recoveryRekeyConfig = nil
return results, nil return results, nil
} }

View File

@ -10,6 +10,12 @@ import (
"strings" "strings"
"sync/atomic" "sync/atomic"
"github.com/hashicorp/go-kms-wrapping/wrappers/aead/v2"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/vault/sdk/helper/jsonutil" "github.com/hashicorp/vault/sdk/helper/jsonutil"
"github.com/hashicorp/vault/sdk/physical" "github.com/hashicorp/vault/sdk/physical"
@ -40,6 +46,12 @@ const (
// recoveryKeyPath is the path to the recovery key // recoveryKeyPath is the path to the recovery key
recoveryKeyPath = "core/recovery-key" recoveryKeyPath = "core/recovery-key"
// recoveryUnsealKeyPath is the path to a copy of the root key,
// encrypted using the shamir-combined recovery keys, just like
// StoredBarrierKeysPath is the root key encrypted by the seal
// (which in the case of a shamir seal is the shamir-combined unseal keys.)
recoveryUnsealKeyPath = "core/recovery-unseal-key"
// StoredBarrierKeysPath is the path used for storing HSM-encrypted unseal keys // StoredBarrierKeysPath is the path used for storing HSM-encrypted unseal keys
StoredBarrierKeysPath = "core/hsm/barrier-unseal-keys" StoredBarrierKeysPath = "core/hsm/barrier-unseal-keys"
@ -68,6 +80,7 @@ type Seal interface {
RecoveryType() string RecoveryType() string
RecoveryConfig(context.Context) (*SealConfig, error) RecoveryConfig(context.Context) (*SealConfig, error)
RecoveryKey(context.Context) ([]byte, error) RecoveryKey(context.Context) ([]byte, error)
UnsealRecoveryKey(ctx context.Context) ([]byte, error)
SetRecoveryConfig(context.Context, *SealConfig) error SetRecoveryConfig(context.Context, *SealConfig) error
SetCachedRecoveryConfig(*SealConfig) SetCachedRecoveryConfig(*SealConfig)
SetRecoveryKey(context.Context, []byte) error SetRecoveryKey(context.Context, []byte) error
@ -76,14 +89,26 @@ type Seal interface {
} }
type defaultSeal struct { type defaultSeal struct {
access *seal.Access access *seal.Access
config atomic.Value config atomic.Value
core *Core logger log.Logger
core *Core
unsealKeyPath string
} }
func NewDefaultSeal(lowLevel *seal.Access) Seal { func NewDefaultSeal(lowLevel *seal.Access) Seal {
ret := &defaultSeal{ ret := &defaultSeal{
access: lowLevel, access: lowLevel,
unsealKeyPath: StoredBarrierKeysPath,
}
ret.config.Store((*SealConfig)(nil))
return ret
}
func NewRecoverySeal(lowLevel *seal.Access) Seal {
ret := &defaultSeal{
access: lowLevel,
unsealKeyPath: recoveryUnsealKeyPath,
} }
ret.config.Store((*SealConfig)(nil)) ret.config.Store((*SealConfig)(nil))
return ret return ret
@ -110,6 +135,14 @@ func (d *defaultSeal) SetAccess(access *seal.Access) {
func (d *defaultSeal) SetCore(core *Core) { func (d *defaultSeal) SetCore(core *Core) {
d.core = core d.core = core
if d.logger == nil {
if isUnsealRecoverySeal(d) {
d.logger = d.core.Logger().Named("recoveryseal")
} else {
d.logger = d.core.Logger().Named("defaultseal")
}
d.core.AddLogger(d.logger)
}
} }
func (d *defaultSeal) Init(ctx context.Context) error { func (d *defaultSeal) Init(ctx context.Context) error {
@ -141,7 +174,7 @@ func (d *defaultSeal) SetStoredKeys(ctx context.Context, keys [][]byte) error {
if d.LegacySeal() { if d.LegacySeal() {
return fmt.Errorf("stored keys are not supported") return fmt.Errorf("stored keys are not supported")
} }
return writeStoredKeys(ctx, d.core.physical, d.access, keys) return writeStoredKeys(ctx, d.core.physical, d.access, keys, d.unsealKeyPath)
} }
func (d *defaultSeal) LegacySeal() bool { func (d *defaultSeal) LegacySeal() bool {
@ -156,7 +189,7 @@ func (d *defaultSeal) GetStoredKeys(ctx context.Context) ([][]byte, error) {
if d.LegacySeal() { if d.LegacySeal() {
return nil, fmt.Errorf("stored keys are not supported") return nil, fmt.Errorf("stored keys are not supported")
} }
keys, err := readStoredKeys(ctx, d.core.physical, d.access) keys, err := readStoredKeys(ctx, d.core.physical, d.access, d.unsealKeyPath)
return keys, err return keys, err
} }
@ -197,8 +230,10 @@ func (d *defaultSeal) BarrierConfig(ctx context.Context) (*SealConfig, error) {
conf.Type = d.BarrierType().String() conf.Type = d.BarrierType().String()
case d.BarrierType().String(): case d.BarrierType().String():
default: default:
d.core.logger.Error("barrier seal type does not match expected type", "barrier_seal_type", conf.Type, "loaded_seal_type", d.BarrierType()) if conf.Type == wrapping.WrapperTypeShamir.String() || d.unsealKeyPath != recoveryUnsealKeyPath {
return nil, fmt.Errorf("barrier seal type of %q does not match expected type of %q", conf.Type, d.BarrierType()) d.core.logger.Error("barrier seal type does not match expected type", "barrier_seal_type", conf.Type, "loaded_seal_type", d.BarrierType())
return nil, fmt.Errorf("barrier seal type of %q does not match expected type of %q", conf.Type, d.BarrierType())
}
} }
// Check for a valid seal configuration // Check for a valid seal configuration
@ -266,6 +301,10 @@ func (d *defaultSeal) RecoveryConfig(ctx context.Context) (*SealConfig, error) {
return nil, fmt.Errorf("recovery not supported") return nil, fmt.Errorf("recovery not supported")
} }
func (d *defaultSeal) UnsealRecoveryKey(ctx context.Context) ([]byte, error) {
return d.access.Wrapper.(*aead.ShamirWrapper).KeyBytes(ctx)
}
func (d *defaultSeal) RecoveryKey(ctx context.Context) ([]byte, error) { func (d *defaultSeal) RecoveryKey(ctx context.Context) ([]byte, error) {
return nil, fmt.Errorf("recovery not supported") return nil, fmt.Errorf("recovery not supported")
} }
@ -281,6 +320,10 @@ func (d *defaultSeal) VerifyRecoveryKey(ctx context.Context, key []byte) error {
return fmt.Errorf("recovery not supported") return fmt.Errorf("recovery not supported")
} }
func (d *defaultSeal) VerifyRecoveryUnsealKey(ctx context.Context, key []byte) error {
return fmt.Errorf("recovery unseal not supported")
}
func (d *defaultSeal) SetRecoveryKey(ctx context.Context, key []byte) error { func (d *defaultSeal) SetRecoveryKey(ctx context.Context, key []byte) error {
return fmt.Errorf("recovery not supported") return fmt.Errorf("recovery not supported")
} }
@ -316,6 +359,9 @@ type SealConfig struct {
// How many keys to store, for seals that support storage. Always 0 or 1. // How many keys to store, for seals that support storage. Always 0 or 1.
StoredShares int `json:"stored_shares" mapstructure:"stored_shares"` StoredShares int `json:"stored_shares" mapstructure:"stored_shares"`
// Whether unseal using recovery keys should be disabled
DisableUnsealRecovery bool `json:"disable_unseal_recovery" mapstructure:"disable_unseal_recovery"`
// Stores the progress of the rekey operation (key shares) // Stores the progress of the rekey operation (key shares)
RekeyProgress [][]byte `json:"-"` RekeyProgress [][]byte `json:"-"`
@ -429,7 +475,7 @@ func (e *ErrDecrypt) Is(target error) bool {
return ok || errors.Is(e.Err, target) return ok || errors.Is(e.Err, target)
} }
func writeStoredKeys(ctx context.Context, storage physical.Backend, encryptor *seal.Access, keys [][]byte) error { func writeStoredKeys(ctx context.Context, storage physical.Backend, encryptor *seal.Access, keys [][]byte, path string) error {
if keys == nil { if keys == nil {
return fmt.Errorf("keys were nil") return fmt.Errorf("keys were nil")
} }
@ -455,7 +501,7 @@ func writeStoredKeys(ctx context.Context, storage physical.Backend, encryptor *s
// Store the seal configuration. // Store the seal configuration.
pe := &physical.Entry{ pe := &physical.Entry{
Key: StoredBarrierKeysPath, Key: path,
Value: value, Value: value,
} }
@ -466,8 +512,8 @@ func writeStoredKeys(ctx context.Context, storage physical.Backend, encryptor *s
return nil return nil
} }
func readStoredKeys(ctx context.Context, storage physical.Backend, encryptor *seal.Access) ([][]byte, error) { func readStoredKeys(ctx context.Context, storage physical.Backend, encryptor *seal.Access, path string) ([][]byte, error) {
pe, err := storage.Get(ctx, StoredBarrierKeysPath) pe, err := storage.Get(ctx, path)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch stored keys: %w", err) return nil, fmt.Errorf("failed to fetch stored keys: %w", err)
} }
@ -478,17 +524,22 @@ func readStoredKeys(ctx context.Context, storage physical.Backend, encryptor *se
return nil, nil return nil, nil
} }
blobInfo := &wrapping.BlobInfo{} var blobInfo wrapping.BlobInfo
if err := proto.Unmarshal(pe.Value, blobInfo); err != nil { // Read as a multi-blob first
if err := proto.Unmarshal(pe.Value, &blobInfo); err != nil {
return nil, fmt.Errorf("failed to proto decode stored keys: %w", err) return nil, fmt.Errorf("failed to proto decode stored keys: %w", err)
} }
pt, err := encryptor.Decrypt(ctx, blobInfo, nil) pt, err := encryptor.Decrypt(ctx, &blobInfo, nil)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "message authentication failed") { if strings.Contains(err.Error(), "message authentication failed") {
return nil, &ErrInvalidKey{Reason: fmt.Sprintf("failed to decrypt keys from storage: %v", err)} err = multierror.Append(err, &ErrInvalidKey{Reason: fmt.Sprintf("failed to decrypt keys from storage: %v", err)})
} else {
err = multierror.Append(err, &ErrDecrypt{Err: fmt.Errorf("failed to decrypt keys from storage: %w", err)})
} }
return nil, &ErrDecrypt{Err: fmt.Errorf("failed to decrypt keys from storage: %w", err)} }
if len(pt) == 0 {
return nil, err
} }
// Decode the barrier entry // Decode the barrier entry

View File

@ -108,13 +108,13 @@ func (d *autoSeal) RecoveryKeySupported() bool {
// SetStoredKeys uses the autoSeal.Access.Encrypts method to wrap the keys. The stored entry // SetStoredKeys uses the autoSeal.Access.Encrypts method to wrap the keys. The stored entry
// does not need to be seal wrapped in this case. // does not need to be seal wrapped in this case.
func (d *autoSeal) SetStoredKeys(ctx context.Context, keys [][]byte) error { func (d *autoSeal) SetStoredKeys(ctx context.Context, keys [][]byte) error {
return writeStoredKeys(ctx, d.core.physical, d.Access, keys) return writeStoredKeys(ctx, d.core.physical, d.Access, keys, StoredBarrierKeysPath)
} }
// GetStoredKeys retrieves the key shares by unwrapping the encrypted key using the // GetStoredKeys retrieves the key shares by unwrapping the encrypted key using the
// autoseal. // autoseal.
func (d *autoSeal) GetStoredKeys(ctx context.Context) ([][]byte, error) { func (d *autoSeal) GetStoredKeys(ctx context.Context) ([][]byte, error) {
return readStoredKeys(ctx, d.core.physical, d.Access) return readStoredKeys(ctx, d.core.physical, d.Access, StoredBarrierKeysPath)
} }
func (d *autoSeal) upgradeStoredKeys(ctx context.Context) error { func (d *autoSeal) upgradeStoredKeys(ctx context.Context) error {
@ -435,14 +435,22 @@ func (d *autoSeal) RecoveryKey(ctx context.Context) ([]byte, error) {
return d.getRecoveryKeyInternal(ctx) return d.getRecoveryKeyInternal(ctx)
} }
func (d *autoSeal) UnsealRecoveryKey(ctx context.Context) ([]byte, error) {
return nil, fmt.Errorf("unseal recovery not supported")
}
func (d *autoSeal) getRecoveryKeyInternal(ctx context.Context) ([]byte, error) { func (d *autoSeal) getRecoveryKeyInternal(ctx context.Context) ([]byte, error) {
pe, err := d.core.physical.Get(ctx, recoveryKeyPath) return getRecoveryKeyInternal(ctx, d.core.physical, d.logger, d.Access)
}
func getRecoveryKeyInternal(ctx context.Context, storage physical.Backend, logger log.Logger, access *seal.Access) ([]byte, error) {
pe, err := storage.Get(ctx, recoveryKeyPath)
if err != nil { if err != nil {
d.logger.Error("failed to read recovery key", "error", err) logger.Error("failed to read recovery key", "error", err)
return nil, fmt.Errorf("failed to read recovery key: %w", err) return nil, fmt.Errorf("failed to read recovery key: %w", err)
} }
if pe == nil { if pe == nil {
d.logger.Warn("no recovery key found") logger.Warn("no recovery key found")
return nil, fmt.Errorf("no recovery key found") return nil, fmt.Errorf("no recovery key found")
} }
@ -451,7 +459,7 @@ func (d *autoSeal) getRecoveryKeyInternal(ctx context.Context) ([]byte, error) {
return nil, fmt.Errorf("failed to proto decode stored keys: %w", err) return nil, fmt.Errorf("failed to proto decode stored keys: %w", err)
} }
pt, err := d.Decrypt(ctx, blobInfo, nil) pt, err := access.Decrypt(ctx, blobInfo, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to decrypt encrypted stored keys: %w", err) return nil, fmt.Errorf("failed to decrypt encrypted stored keys: %w", err)
} }