Skip to content

Commit

Permalink
core: re-encrypt barrier and recovery keys if the unseal key is updat…
Browse files Browse the repository at this point in the history
…ed (#7493)

Seal keys can be rotated. When this happens, the barrier and recovery
keys should be re-encrypted with the new seal key. This change
automatically re-encrypts the barrier and recovery keys with the latest
seal key on the active node during the 'postUnseal' phase.
  • Loading branch information
mgaffney committed Oct 3, 2019
1 parent e8eecca commit b3a7ed8
Show file tree
Hide file tree
Showing 4 changed files with 289 additions and 23 deletions.
14 changes: 13 additions & 1 deletion vault/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -1654,7 +1654,7 @@ func (s standardUnsealStrategy) unseal(ctx context.Context, logger log.Logger, c
return nil
}

// postUnseal is invoked after the barrier is unsealed, but before
// postUnseal is invoked on the active node after the barrier is unsealed, but before
// allowing any user operations. This allows us to setup any state that
// requires the Vault to be unsealed such as mount tables, logical backends,
// credential stores, etc.
Expand Down Expand Up @@ -1692,6 +1692,18 @@ func (c *Core) postUnseal(ctx context.Context, ctxCancelFunc context.CancelFunc,
return err
}

// Automatically re-encrypt the keys used for auto unsealing when the
// seal's encryption key changes. The regular rotation of cryptographic
// keys is a NIST recommendation. Access to prior keys for decryption
// is normally supported for a configurable time period. Re-encrypting
// the keys used for auto unsealing ensures Vault and its data will
// continue to be accessible even after prior seal keys are destroyed.
if seal, ok := c.seal.(*autoSeal); ok {
if err := seal.UpgradeKeys(c.activeContext); err != nil {
c.logger.Warn("post-unseal upgrade seal keys failed", "error", err)
}
}

c.metricsCh = make(chan struct{})
go c.emitMetrics(c.metricsCh)

Expand Down
8 changes: 7 additions & 1 deletion vault/seal/seal_testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
type TestSeal struct {
Type string
secret []byte
keyId string
}

var _ Access = (*TestSeal)(nil)
Expand All @@ -18,6 +19,7 @@ func NewTestSeal(secret []byte) *TestSeal {
return &TestSeal{
Type: Test,
secret: secret,
keyId: "static-key",
}
}

Expand All @@ -34,7 +36,11 @@ func (t *TestSeal) SealType() string {
}

func (t *TestSeal) KeyID() string {
return "static-key"
return t.keyId
}

func (t *TestSeal) SetKeyID(k string) {
t.keyId = k
}

func (t *TestSeal) Encrypt(_ context.Context, plaintext []byte) (*physical.EncryptedBlobInfo, error) {
Expand Down
132 changes: 111 additions & 21 deletions vault/seal_autoseal.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

proto "github.com/golang/protobuf/proto"
"github.com/hashicorp/errwrap"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/sdk/physical"
"github.com/hashicorp/vault/vault/seal"
)
Expand All @@ -26,12 +27,13 @@ type autoSeal struct {
barrierConfig atomic.Value
recoveryConfig atomic.Value
core *Core
logger log.Logger
}

// Ensure we are implementing the Seal interface
var _ Seal = (*autoSeal)(nil)

func NewAutoSeal(lowLevel seal.Access) Seal {
func NewAutoSeal(lowLevel seal.Access) *autoSeal {
ret := &autoSeal{
Access: lowLevel,
}
Expand All @@ -53,6 +55,10 @@ func (d *autoSeal) checkCore() error {

func (d *autoSeal) SetCore(core *Core) {
d.core = core
if d.logger == nil {
d.logger = d.core.Logger().Named("autoseal")
d.core.AddLogger(d.logger)
}
}

func (d *autoSeal) Init(ctx context.Context) error {
Expand Down Expand Up @@ -147,6 +153,61 @@ func (d *autoSeal) GetStoredKeys(ctx context.Context) ([][]byte, error) {
return keys, nil
}

func (d *autoSeal) upgradeStoredKeys(ctx context.Context) error {
pe, err := d.core.physical.Get(ctx, StoredBarrierKeysPath)
if err != nil {
return errwrap.Wrapf("failed to fetch stored keys: {{err}}", err)
}
if pe == nil {
return fmt.Errorf("no stored keys found")
}

blobInfo := &physical.EncryptedBlobInfo{}
if err := proto.Unmarshal(pe.Value, blobInfo); err != nil {
return errwrap.Wrapf("failed to proto decode stored keys: {{err}}", err)
}

if blobInfo.KeyInfo != nil && blobInfo.KeyInfo.KeyID != d.Access.KeyID() {
d.logger.Info("upgrading stored keys")

pt, err := d.Decrypt(ctx, blobInfo)
if err != nil {
return errwrap.Wrapf("failed to decrypt encrypted stored keys: {{err}}", err)
}

// Decode the barrier entry
var keys [][]byte
if err := json.Unmarshal(pt, &keys); err != nil {
return errwrap.Wrapf("failed to decode stored keys: {{err}}", err)
}

if err := d.SetStoredKeys(ctx, keys); err != nil {
return errwrap.Wrapf("failed to save upgraded stored keys: {{err}}", err)
}
}
return nil
}

// UpgradeKeys re-encrypts and saves the stored keys and the recovery key
// with the current key if the current KeyID is different from the KeyID
// the stored keys and the recovery key are encrypted with. The provided
// Context must be non-nil.
func (d *autoSeal) UpgradeKeys(ctx context.Context) error {
// Many of the seals update their keys to the latest KeyID when Encrypt
// is called.
if _, err := d.Encrypt(ctx, []byte("a")); err != nil {
return err
}

if err := d.upgradeRecoveryKey(ctx); err != nil {
return err
}
if err := d.upgradeStoredKeys(ctx); err != nil {
return err
}
return nil
}

func (d *autoSeal) BarrierConfig(ctx context.Context) (*SealConfig, error) {
if d.barrierConfig.Load().(*SealConfig) != nil {
return d.barrierConfig.Load().(*SealConfig).Clone(), nil
Expand All @@ -160,35 +221,35 @@ func (d *autoSeal) BarrierConfig(ctx context.Context) (*SealConfig, error) {

entry, err := d.core.physical.Get(ctx, barrierSealConfigPath)
if err != nil {
d.core.logger.Error("autoseal: failed to read seal configuration", "seal_type", sealType, "error", err)
d.logger.Error("failed to read seal configuration", "seal_type", sealType, "error", err)
return nil, errwrap.Wrapf(fmt.Sprintf("failed to read %q seal configuration: {{err}}", sealType), err)
}

// If the seal configuration is missing, we are not initialized
if entry == nil {
if d.core.logger.IsInfo() {
d.core.logger.Info("autoseal: seal configuration missing, not initialized", "seal_type", sealType)
if d.logger.IsInfo() {
d.logger.Info("seal configuration missing, not initialized", "seal_type", sealType)
}
return nil, nil
}

conf := &SealConfig{}
err = json.Unmarshal(entry.Value, conf)
if err != nil {
d.core.logger.Error("autoseal: failed to decode seal configuration", "seal_type", sealType, "error", err)
d.logger.Error("failed to decode seal configuration", "seal_type", sealType, "error", err)
return nil, errwrap.Wrapf(fmt.Sprintf("failed to decode %q seal configuration: {{err}}", sealType), err)
}

// Check for a valid seal configuration
if err := conf.Validate(); err != nil {
d.core.logger.Error("autoseal: invalid seal configuration", "seal_type", sealType, "error", err)
d.logger.Error("invalid seal configuration", "seal_type", sealType, "error", err)
return nil, errwrap.Wrapf(fmt.Sprintf("%q seal validation failed: {{err}}", sealType), err)
}

barrierTypeUpgradeCheck(d.BarrierType(), conf)

if conf.Type != d.BarrierType() {
d.core.logger.Error("autoseal: barrier seal type does not match loaded type", "seal_type", conf.Type, "loaded_type", d.BarrierType())
d.logger.Error("barrier seal type does not match loaded type", "seal_type", conf.Type, "loaded_type", d.BarrierType())
return nil, fmt.Errorf("barrier seal type of %q does not match loaded type of %q", conf.Type, d.BarrierType())
}

Expand Down Expand Up @@ -221,7 +282,7 @@ func (d *autoSeal) SetBarrierConfig(ctx context.Context, conf *SealConfig) error
}

if err := d.core.physical.Put(ctx, pe); err != nil {
d.core.logger.Error("autoseal: failed to write barrier seal configuration", "error", err)
d.logger.Error("failed to write barrier seal configuration", "error", err)
return errwrap.Wrapf("failed to write barrier seal configuration: {{err}}", err)
}

Expand Down Expand Up @@ -254,13 +315,13 @@ func (d *autoSeal) RecoveryConfig(ctx context.Context) (*SealConfig, error) {
var err error
entry, err = d.core.physical.Get(ctx, recoverySealConfigPlaintextPath)
if err != nil {
d.core.logger.Error("autoseal: failed to read seal configuration", "seal_type", sealType, "error", err)
d.logger.Error("failed to read seal configuration", "seal_type", sealType, "error", err)
return nil, errwrap.Wrapf(fmt.Sprintf("failed to read %q seal configuration: {{err}}", sealType), err)
}

if entry == nil {
if d.core.Sealed() {
d.core.logger.Info("autoseal: seal configuration missing, but cannot check old path as core is sealed", "seal_type", sealType)
d.logger.Info("seal configuration missing, but cannot check old path as core is sealed", "seal_type", sealType)
return nil, nil
}

Expand All @@ -273,8 +334,8 @@ func (d *autoSeal) RecoveryConfig(ctx context.Context) (*SealConfig, error) {

// If the seal configuration is missing, then we are not initialized.
if be == nil {
if d.core.logger.IsInfo() {
d.core.logger.Info("autoseal: seal configuration missing, not initialized", "seal_type", sealType)
if d.logger.IsInfo() {
d.logger.Info("seal configuration missing, not initialized", "seal_type", sealType)
}
return nil, nil
}
Expand All @@ -288,18 +349,18 @@ func (d *autoSeal) RecoveryConfig(ctx context.Context) (*SealConfig, error) {

conf := &SealConfig{}
if err := json.Unmarshal(entry.Value, conf); err != nil {
d.core.logger.Error("autoseal: failed to decode seal configuration", "seal_type", sealType, "error", err)
d.logger.Error("failed to decode seal configuration", "seal_type", sealType, "error", err)
return nil, errwrap.Wrapf(fmt.Sprintf("failed to decode %q seal configuration: {{err}}", sealType), err)
}

// Check for a valid seal configuration
if err := conf.Validate(); err != nil {
d.core.logger.Error("autoseal: invalid seal configuration", "seal_type", sealType, "error", err)
d.logger.Error("invalid seal configuration", "seal_type", sealType, "error", err)
return nil, errwrap.Wrapf(fmt.Sprintf("%q seal validation failed: {{err}}", sealType), err)
}

if conf.Type != d.RecoveryType() {
d.core.logger.Error("autoseal: recovery seal type does not match loaded type", "seal_type", conf.Type, "loaded_type", d.RecoveryType())
d.logger.Error("recovery seal type does not match loaded type", "seal_type", conf.Type, "loaded_type", d.RecoveryType())
return nil, fmt.Errorf("recovery seal type of %q does not match loaded type of %q", conf.Type, d.RecoveryType())
}

Expand Down Expand Up @@ -339,7 +400,7 @@ func (d *autoSeal) SetRecoveryConfig(ctx context.Context, conf *SealConfig) erro
}

if err := d.core.physical.Put(ctx, pe); err != nil {
d.core.logger.Error("autoseal: failed to write recovery seal configuration", "error", err)
d.logger.Error("failed to write recovery seal configuration", "error", err)
return errwrap.Wrapf("failed to write recovery seal configuration: {{err}}", err)
}

Expand Down Expand Up @@ -395,7 +456,7 @@ func (d *autoSeal) SetRecoveryKey(ctx context.Context, key []byte) error {
}

if err := d.core.physical.Put(ctx, be); err != nil {
d.core.logger.Error("autoseal: failed to write recovery key", "error", err)
d.logger.Error("failed to write recovery key", "error", err)
return errwrap.Wrapf("failed to write recovery key: {{err}}", err)
}

Expand All @@ -409,11 +470,11 @@ func (d *autoSeal) RecoveryKey(ctx context.Context) ([]byte, error) {
func (d *autoSeal) getRecoveryKeyInternal(ctx context.Context) ([]byte, error) {
pe, err := d.core.physical.Get(ctx, recoveryKeyPath)
if err != nil {
d.core.logger.Error("autoseal: failed to read recovery key", "error", err)
d.logger.Error("failed to read recovery key", "error", err)
return nil, errwrap.Wrapf("failed to read recovery key: {{err}}", err)
}
if pe == nil {
d.core.logger.Warn("autoseal: no recovery key found")
d.logger.Warn("no recovery key found")
return nil, fmt.Errorf("no recovery key found")
}

Expand All @@ -430,6 +491,35 @@ func (d *autoSeal) getRecoveryKeyInternal(ctx context.Context) ([]byte, error) {
return pt, nil
}

func (d *autoSeal) upgradeRecoveryKey(ctx context.Context) error {
pe, err := d.core.physical.Get(ctx, recoveryKeyPath)
if err != nil {
return errwrap.Wrapf("failed to fetch recovery key: {{err}}", err)
}
if pe == nil {
return fmt.Errorf("no recovery key found")
}

blobInfo := &physical.EncryptedBlobInfo{}
if err := proto.Unmarshal(pe.Value, blobInfo); err != nil {
return errwrap.Wrapf("failed to proto decode recovery key: {{err}}", err)
}

if blobInfo.KeyInfo != nil && blobInfo.KeyInfo.KeyID != d.Access.KeyID() {
d.logger.Info("upgrading recovery key")

pt, err := d.Decrypt(ctx, blobInfo)
if err != nil {
return errwrap.Wrapf("failed to decrypt encrypted recovery key: {{err}}", err)

}
if err := d.SetRecoveryKey(ctx, pt); err != nil {
return errwrap.Wrapf("failed to save upgraded recovery key: {{err}}", err)
}
}
return nil
}

// migrateRecoveryConfig is a helper func to migrate the recovery config to
// live outside the barrier. This is called from SetRecoveryConfig which is
// always called with the stateLock.
Expand All @@ -446,8 +536,8 @@ func (d *autoSeal) migrateRecoveryConfig(ctx context.Context) error {
}

// Only log if we are performing the migration
d.core.logger.Debug("migrating recovery seal configuration")
defer d.core.logger.Debug("done migrating recovery seal configuration")
d.logger.Debug("migrating recovery seal configuration")
defer d.logger.Debug("done migrating recovery seal configuration")

// Perform migration
pe := &physical.Entry{
Expand Down
Loading

0 comments on commit b3a7ed8

Please sign in to comment.