Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core: re-encrypt barrier and recovery keys if the unseal key is updated #7493

Closed
wants to merge 12 commits into from
Closed
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 {
mgaffney marked this conversation as resolved.
Show resolved Hide resolved
if err := seal.UpgradeKeys(c.activeContext); err != nil {
return 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 {
mgaffney marked this conversation as resolved.
Show resolved Hide resolved
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)
mgaffney marked this conversation as resolved.
Show resolved Hide resolved
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")
mgaffney marked this conversation as resolved.
Show resolved Hide resolved
}

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