Add ability to migrate autoseal to autoseal (#5930)
* Add ability to migrate autoseal to autoseal This adds the ability to migrate from shamir to autoseal, autoseal to shamir, or autoseal to autoseal, by allowing multiple seal stanzas. A disabled stanza will be used as the config being migrated from; this can also be used to provide an unwrap seal on ent over multiple unseals. A new test is added to ensure that autoseal to autoseal works as expected. * Fix test * Provide default shamir info if not given in config * Linting feedback * Remove context var that isn't used * Don't run auto unseal watcher when in migration, and move SetCores to SetSealsForMigration func * Slight logic cleanup * Fix test build and fix bug * Updates * remove GetRecoveryKey function
This commit is contained in:
parent
ad3605e657
commit
a83ed04730
|
@ -9,7 +9,6 @@ import (
|
|||
|
||||
hclog "github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/command/server"
|
||||
"github.com/hashicorp/vault/helper/logging"
|
||||
vaulthttp "github.com/hashicorp/vault/http"
|
||||
"github.com/hashicorp/vault/physical"
|
||||
|
@ -114,11 +113,7 @@ func TestSealMigration(t *testing.T) {
|
|||
newSeal := vault.NewAutoSeal(seal.NewTestSeal(nil))
|
||||
newSeal.SetCore(core)
|
||||
autoSeal = newSeal
|
||||
if err := adjustCoreForSealMigration(ctx, core, coreConfig, newSeal, &server.Config{
|
||||
Seal: &server.Seal{
|
||||
Type: "test-auto",
|
||||
},
|
||||
}); err != nil {
|
||||
if err := adjustCoreForSealMigration(core, newSeal, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
@ -197,13 +192,16 @@ func TestSealMigration(t *testing.T) {
|
|||
cluster.Cores = nil
|
||||
}
|
||||
|
||||
// We should see stored barrier keys; after the next stanza, we shouldn't
|
||||
// We should see stored barrier keys; after the sixth test, we shouldn't
|
||||
if entry, err := phys.Get(ctx, vault.StoredBarrierKeysPath); err != nil || entry == nil {
|
||||
t.Fatalf("expected nil error and non-nil entry, got error %#v and entry %#v", err, entry)
|
||||
}
|
||||
|
||||
// Fifth: create an autoseal and activate migration. Verify it doesn't work
|
||||
// if disabled isn't set.
|
||||
altTestSeal := seal.NewTestSeal(nil)
|
||||
altTestSeal.Type = "test-alternate"
|
||||
altSeal := vault.NewAutoSeal(altTestSeal)
|
||||
|
||||
// Fifth: migrate from auto-seal to auto-seal
|
||||
{
|
||||
coreConfig.Seal = autoSeal
|
||||
cluster := vault.NewTestCluster(t, coreConfig, clusterConfig)
|
||||
|
@ -212,17 +210,45 @@ func TestSealMigration(t *testing.T) {
|
|||
|
||||
core := cluster.Cores[0].Core
|
||||
|
||||
serverConf := &server.Config{
|
||||
Seal: &server.Seal{
|
||||
Type: "test-auto",
|
||||
},
|
||||
if err := adjustCoreForSealMigration(core, altSeal, autoSeal); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := adjustCoreForSealMigration(ctx, core, coreConfig, shamirSeal, serverConf); err == nil {
|
||||
t.Fatal("expected error since disabled isn't set true")
|
||||
client := cluster.Cores[0].Client
|
||||
client.SetToken(rootToken)
|
||||
|
||||
var resp *api.SealStatusResponse
|
||||
unsealOpts := &api.UnsealOpts{}
|
||||
for _, key := range keys {
|
||||
unsealOpts.Key = key
|
||||
unsealOpts.Migrate = true
|
||||
resp, err = client.Sys().UnsealWithOptions(unsealOpts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp == nil {
|
||||
t.Fatal("expected response")
|
||||
}
|
||||
}
|
||||
serverConf.Seal.Disabled = true
|
||||
if err := adjustCoreForSealMigration(ctx, core, coreConfig, shamirSeal, serverConf); err != nil {
|
||||
if resp.Sealed {
|
||||
t.Fatalf("expected unsealed state; got %#v", *resp)
|
||||
}
|
||||
|
||||
cluster.Cleanup()
|
||||
cluster.Cores = nil
|
||||
}
|
||||
|
||||
// Sixth: create an Shamir seal and activate migration. Verify it doesn't work
|
||||
// if disabled isn't set.
|
||||
{
|
||||
coreConfig.Seal = altSeal
|
||||
cluster := vault.NewTestCluster(t, coreConfig, clusterConfig)
|
||||
cluster.Start()
|
||||
defer cluster.Cleanup()
|
||||
|
||||
core := cluster.Cores[0].Core
|
||||
|
||||
if err := adjustCoreForSealMigration(core, shamirSeal, altSeal); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
@ -259,7 +285,7 @@ func TestSealMigration(t *testing.T) {
|
|||
t.Fatalf("expected nil error and nil entry, got error %#v and entry %#v", err, entry)
|
||||
}
|
||||
|
||||
// Sixth: verify autoseal is off and the expected key shares work
|
||||
// Seventh: verify autoseal is off and the expected key shares work
|
||||
{
|
||||
coreConfig.Seal = shamirSeal
|
||||
cluster := vault.NewTestCluster(t, coreConfig, clusterConfig)
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/hashicorp/vault/helper/metricsutil"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
|
@ -21,6 +20,8 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/helper/metricsutil"
|
||||
|
||||
metrics "github.com/armon/go-metrics"
|
||||
"github.com/armon/go-metrics/circonus"
|
||||
"github.com/armon/go-metrics/datadog"
|
||||
|
@ -501,44 +502,61 @@ func (c *ServerCommand) Run(args []string) int {
|
|||
info["log level"] = logLevelString
|
||||
infoKeys = append(infoKeys, "log level")
|
||||
|
||||
sealType := vaultseal.Shamir
|
||||
if config.Seal != nil || os.Getenv("VAULT_SEAL_TYPE") != "" {
|
||||
if config.Seal == nil {
|
||||
sealType = os.Getenv("VAULT_SEAL_TYPE")
|
||||
} else {
|
||||
sealType = config.Seal.Type
|
||||
}
|
||||
}
|
||||
var barrierSeal vault.Seal
|
||||
var unwrapSeal vault.Seal
|
||||
|
||||
var seal vault.Seal
|
||||
var sealConfigError error
|
||||
if c.flagDevAutoSeal {
|
||||
seal = vault.NewAutoSeal(vaultseal.NewTestSeal(nil))
|
||||
barrierSeal = vault.NewAutoSeal(vaultseal.NewTestSeal(nil))
|
||||
} else {
|
||||
sealLogger := c.logger.Named(sealType)
|
||||
allLoggers = append(allLoggers, sealLogger)
|
||||
seal, sealConfigError = serverseal.ConfigureSeal(config, &infoKeys, &info, sealLogger, vault.NewDefaultSeal())
|
||||
if sealConfigError != nil {
|
||||
if !errwrap.ContainsType(sealConfigError, new(logical.KeyNotFoundError)) {
|
||||
// Handle the case where no seal is provided
|
||||
if len(config.Seals) == 0 {
|
||||
config.Seals = append(config.Seals, &server.Seal{Type: vaultseal.Shamir})
|
||||
}
|
||||
for _, configSeal := range config.Seals {
|
||||
sealType := vaultseal.Shamir
|
||||
if !configSeal.Disabled && os.Getenv("VAULT_SEAL_TYPE") != "" {
|
||||
sealType = os.Getenv("VAULT_SEAL_TYPE")
|
||||
} else {
|
||||
sealType = configSeal.Type
|
||||
}
|
||||
|
||||
var seal vault.Seal
|
||||
sealLogger := c.logger.Named(sealType)
|
||||
allLoggers = append(allLoggers, sealLogger)
|
||||
seal, sealConfigError = serverseal.ConfigureSeal(configSeal, &infoKeys, &info, sealLogger, vault.NewDefaultSeal())
|
||||
if sealConfigError != nil {
|
||||
if !errwrap.ContainsType(sealConfigError, new(logical.KeyNotFoundError)) {
|
||||
c.UI.Error(fmt.Sprintf(
|
||||
"Error parsing Seal configuration: %s", sealConfigError))
|
||||
return 1
|
||||
}
|
||||
}
|
||||
if seal == nil {
|
||||
c.UI.Error(fmt.Sprintf(
|
||||
"Error parsing Seal configuration: %s", sealConfigError))
|
||||
"After configuring seal nil returned, seal type was %s", sealType))
|
||||
return 1
|
||||
}
|
||||
|
||||
if configSeal.Disabled {
|
||||
unwrapSeal = seal
|
||||
} else {
|
||||
barrierSeal = seal
|
||||
}
|
||||
|
||||
// Ensure that the seal finalizer is called, even if using verify-only
|
||||
defer func() {
|
||||
err = seal.Finalize(context.Background())
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error finalizing seals: %v", err))
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that the seal finalizer is called, even if using verify-only
|
||||
defer func() {
|
||||
if seal != nil {
|
||||
err = seal.Finalize(context.Background())
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error finalizing seals: %v", err))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if seal == nil {
|
||||
c.UI.Error(fmt.Sprintf("Could not create seal! Most likely proper Seal configuration information was not set, but no error was generated."))
|
||||
if barrierSeal == nil {
|
||||
c.UI.Error(fmt.Sprintf("Could not create barrier seal! Most likely proper Seal configuration information was not set, but no error was generated."))
|
||||
return 1
|
||||
}
|
||||
|
||||
|
@ -546,7 +564,7 @@ func (c *ServerCommand) Run(args []string) int {
|
|||
Physical: backend,
|
||||
RedirectAddr: config.Storage.RedirectAddr,
|
||||
HAPhysical: nil,
|
||||
Seal: seal,
|
||||
Seal: barrierSeal,
|
||||
AuditBackends: c.AuditBackends,
|
||||
CredentialBackends: c.CredentialBackends,
|
||||
LogicalBackends: c.LogicalBackends,
|
||||
|
@ -937,7 +955,7 @@ CLUSTER_SYNTHESIS_COMPLETE:
|
|||
}))
|
||||
|
||||
// Before unsealing with stored keys, setup seal migration if needed
|
||||
if err := adjustCoreForSealMigration(context.Background(), core, coreConfig, seal, config); err != nil {
|
||||
if err := adjustCoreForSealMigration(core, barrierSeal, unwrapSeal); err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
@ -946,27 +964,29 @@ 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.
|
||||
go func() {
|
||||
for {
|
||||
err := core.UnsealWithStoredKeys(context.Background())
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if !core.IsInSealMigration() {
|
||||
go func() {
|
||||
for {
|
||||
err := core.UnsealWithStoredKeys(context.Background())
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if vault.IsFatalError(err) {
|
||||
c.logger.Error("error unsealing core", "error", err)
|
||||
return
|
||||
} else {
|
||||
c.logger.Warn("failed to unseal core", "error", err)
|
||||
}
|
||||
if vault.IsFatalError(err) {
|
||||
c.logger.Error("error unsealing core", "error", err)
|
||||
return
|
||||
} else {
|
||||
c.logger.Warn("failed to unseal core", "error", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-c.ShutdownCh:
|
||||
return
|
||||
case <-time.After(5 * time.Second):
|
||||
select {
|
||||
case <-c.ShutdownCh:
|
||||
return
|
||||
case <-time.After(5 * time.Second):
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}()
|
||||
}
|
||||
|
||||
// Perform service discovery registrations and initialization of
|
||||
// HTTP server after the verifyOnly check.
|
||||
|
@ -1366,9 +1386,8 @@ func (c *ServerCommand) enableDev(core *vault.Core, coreConfig *vault.CoreConfig
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Upgrade the default K/V store
|
||||
kvVer := "2"
|
||||
if c.flagDevKVV1 {
|
||||
if c.flagDevKVV1 || c.flagDevLeasedKV {
|
||||
kvVer = "1"
|
||||
}
|
||||
req := &logical.Request{
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
@ -29,7 +30,7 @@ type Config struct {
|
|||
Storage *Storage `hcl:"-"`
|
||||
HAStorage *Storage `hcl:"-"`
|
||||
|
||||
Seal *Seal `hcl:"-"`
|
||||
Seals []*Seal `hcl:"-"`
|
||||
|
||||
CacheSize int `hcl:"cache_size"`
|
||||
DisableCache bool `hcl:"-"`
|
||||
|
@ -276,9 +277,11 @@ func (c *Config) Merge(c2 *Config) *Config {
|
|||
result.HAStorage = c2.HAStorage
|
||||
}
|
||||
|
||||
result.Seal = c.Seal
|
||||
if c2.Seal != nil {
|
||||
result.Seal = c2.Seal
|
||||
for _, s := range c.Seals {
|
||||
result.Seals = append(result.Seals, s)
|
||||
}
|
||||
for _, s := range c2.Seals {
|
||||
result.Seals = append(result.Seals, s)
|
||||
}
|
||||
|
||||
result.Telemetry = c.Telemetry
|
||||
|
@ -558,13 +561,13 @@ func ParseConfig(d string, logger log.Logger) (*Config, error) {
|
|||
}
|
||||
|
||||
if o := list.Filter("hsm"); len(o.Items) > 0 {
|
||||
if err := parseSeal(&result, o, "hsm"); err != nil {
|
||||
if err := parseSeals(&result, o, "hsm"); err != nil {
|
||||
return nil, errwrap.Wrapf("error parsing 'hsm': {{err}}", err)
|
||||
}
|
||||
}
|
||||
|
||||
if o := list.Filter("seal"); len(o.Items) > 0 {
|
||||
if err := parseSeal(&result, o, "seal"); err != nil {
|
||||
if err := parseSeals(&result, o, "seal"); err != nil {
|
||||
return nil, errwrap.Wrapf("error parsing 'seal': {{err}}", err)
|
||||
}
|
||||
}
|
||||
|
@ -795,40 +798,46 @@ func parseHAStorage(result *Config, list *ast.ObjectList, name string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func parseSeal(result *Config, list *ast.ObjectList, blockName string) error {
|
||||
if len(list.Items) > 1 {
|
||||
return fmt.Errorf("only one %q block is permitted", blockName)
|
||||
func parseSeals(result *Config, list *ast.ObjectList, blockName string) error {
|
||||
if len(list.Items) > 2 {
|
||||
return fmt.Errorf("only two or less %q blocks are permitted", blockName)
|
||||
}
|
||||
|
||||
// Get our item
|
||||
item := list.Items[0]
|
||||
|
||||
key := blockName
|
||||
if len(item.Keys) > 0 {
|
||||
key = item.Keys[0].Token.Value().(string)
|
||||
}
|
||||
|
||||
var m map[string]string
|
||||
if err := hcl.DecodeObject(&m, item.Val); err != nil {
|
||||
return multierror.Prefix(err, fmt.Sprintf("%s.%s:", blockName, key))
|
||||
}
|
||||
|
||||
var disabled bool
|
||||
var err error
|
||||
if v, ok := m["disabled"]; ok {
|
||||
disabled, err = strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return multierror.Prefix(err, fmt.Sprintf("%s.%s:", blockName, key))
|
||||
seals := make([]*Seal, 0, len(list.Items))
|
||||
for _, item := range list.Items {
|
||||
key := "seal"
|
||||
if len(item.Keys) > 0 {
|
||||
key = item.Keys[0].Token.Value().(string)
|
||||
}
|
||||
delete(m, "disabled")
|
||||
|
||||
var m map[string]string
|
||||
if err := hcl.DecodeObject(&m, item.Val); err != nil {
|
||||
return multierror.Prefix(err, fmt.Sprintf("seal.%s:", key))
|
||||
}
|
||||
|
||||
var disabled bool
|
||||
var err error
|
||||
if v, ok := m["disabled"]; ok {
|
||||
disabled, err = strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return multierror.Prefix(err, fmt.Sprintf("%s.%s:", blockName, key))
|
||||
}
|
||||
delete(m, "disabled")
|
||||
}
|
||||
seals = append(seals, &Seal{
|
||||
Type: strings.ToLower(key),
|
||||
Disabled: disabled,
|
||||
Config: m,
|
||||
})
|
||||
}
|
||||
|
||||
result.Seal = &Seal{
|
||||
Type: strings.ToLower(key),
|
||||
Disabled: disabled,
|
||||
Config: m,
|
||||
if len(seals) == 2 &&
|
||||
(seals[0].Disabled && seals[1].Disabled || !seals[0].Disabled && !seals[1].Disabled) {
|
||||
return errors.New("seals: two seals provided but both are disabled or neither are disabled")
|
||||
}
|
||||
|
||||
result.Seals = seals
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ package seal
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
log "github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/command/server"
|
||||
|
@ -11,39 +10,33 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
ConfigureSeal func(*server.Config, *[]string, *map[string]string, log.Logger, vault.Seal) (vault.Seal, error) = configureSeal
|
||||
ConfigureSeal = configureSeal
|
||||
)
|
||||
|
||||
func configureSeal(config *server.Config, infoKeys *[]string, info *map[string]string, logger log.Logger, inseal vault.Seal) (outseal vault.Seal, err error) {
|
||||
if config.Seal != nil || os.Getenv("VAULT_SEAL_TYPE") != "" {
|
||||
if config.Seal == nil {
|
||||
config.Seal = &server.Seal{
|
||||
Type: os.Getenv("VAULT_SEAL_TYPE"),
|
||||
}
|
||||
}
|
||||
switch config.Seal.Type {
|
||||
case seal.AliCloudKMS:
|
||||
return configureAliCloudKMSSeal(config, infoKeys, info, logger, inseal)
|
||||
func configureSeal(configSeal *server.Seal, infoKeys *[]string, info *map[string]string, logger log.Logger, inseal vault.Seal) (outseal vault.Seal, err error) {
|
||||
switch configSeal.Type {
|
||||
case seal.AliCloudKMS:
|
||||
return configureAliCloudKMSSeal(configSeal, infoKeys, info, logger, inseal)
|
||||
|
||||
case seal.AWSKMS:
|
||||
return configureAWSKMSSeal(config, infoKeys, info, logger, inseal)
|
||||
case seal.AWSKMS:
|
||||
return configureAWSKMSSeal(configSeal, infoKeys, info, logger, inseal)
|
||||
|
||||
case seal.GCPCKMS:
|
||||
return configureGCPCKMSSeal(config, infoKeys, info, logger, inseal)
|
||||
case seal.GCPCKMS:
|
||||
return configureGCPCKMSSeal(configSeal, infoKeys, info, logger, inseal)
|
||||
|
||||
case seal.AzureKeyVault:
|
||||
return configureAzureKeyVaultSeal(config, infoKeys, info, logger, inseal)
|
||||
case seal.AzureKeyVault:
|
||||
return configureAzureKeyVaultSeal(configSeal, infoKeys, info, logger, inseal)
|
||||
|
||||
case seal.Transit:
|
||||
return configureTransitSeal(config, infoKeys, info, logger, inseal)
|
||||
case seal.Transit:
|
||||
return configureTransitSeal(configSeal, infoKeys, info, logger, inseal)
|
||||
|
||||
case seal.PKCS11:
|
||||
return nil, fmt.Errorf("Seal type 'pkcs11' requires the Vault Enterprise HSM binary")
|
||||
case seal.PKCS11:
|
||||
return nil, fmt.Errorf("Seal type 'pkcs11' requires the Vault Enterprise HSM binary")
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("Unknown seal type %q", config.Seal.Type)
|
||||
}
|
||||
case seal.Shamir:
|
||||
return inseal, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("Unknown seal type %q", configSeal.Type)
|
||||
}
|
||||
|
||||
return inseal, nil
|
||||
}
|
||||
|
|
|
@ -9,9 +9,9 @@ import (
|
|||
"github.com/hashicorp/vault/vault/seal/alicloudkms"
|
||||
)
|
||||
|
||||
func configureAliCloudKMSSeal(config *server.Config, infoKeys *[]string, info *map[string]string, logger log.Logger, inseal vault.Seal) (vault.Seal, error) {
|
||||
func configureAliCloudKMSSeal(configSeal *server.Seal, infoKeys *[]string, info *map[string]string, logger log.Logger, inseal vault.Seal) (vault.Seal, error) {
|
||||
kms := alicloudkms.NewSeal(logger)
|
||||
kmsInfo, err := kms.SetConfig(config.Seal.Config)
|
||||
kmsInfo, err := kms.SetConfig(configSeal.Config)
|
||||
if err != nil {
|
||||
// If the error is any other than logical.KeyNotFoundError, return the error
|
||||
if !errwrap.ContainsType(err, new(logical.KeyNotFoundError)) {
|
||||
|
@ -21,7 +21,7 @@ func configureAliCloudKMSSeal(config *server.Config, infoKeys *[]string, info *m
|
|||
autoseal := vault.NewAutoSeal(kms)
|
||||
if kmsInfo != nil {
|
||||
*infoKeys = append(*infoKeys, "Seal Type", "AliCloud KMS Region", "AliCloud KMS KeyID")
|
||||
(*info)["Seal Type"] = config.Seal.Type
|
||||
(*info)["Seal Type"] = configSeal.Type
|
||||
(*info)["AliCloud KMS Region"] = kmsInfo["region"]
|
||||
(*info)["AliCloud KMS KeyID"] = kmsInfo["kms_key_id"]
|
||||
if domain, ok := kmsInfo["domain"]; ok {
|
||||
|
|
|
@ -9,9 +9,9 @@ import (
|
|||
"github.com/hashicorp/vault/vault/seal/awskms"
|
||||
)
|
||||
|
||||
func configureAWSKMSSeal(config *server.Config, infoKeys *[]string, info *map[string]string, logger log.Logger, inseal vault.Seal) (vault.Seal, error) {
|
||||
func configureAWSKMSSeal(configSeal *server.Seal, infoKeys *[]string, info *map[string]string, logger log.Logger, inseal vault.Seal) (vault.Seal, error) {
|
||||
kms := awskms.NewSeal(logger)
|
||||
kmsInfo, err := kms.SetConfig(config.Seal.Config)
|
||||
kmsInfo, err := kms.SetConfig(configSeal.Config)
|
||||
if err != nil {
|
||||
// If the error is any other than logical.KeyNotFoundError, return the error
|
||||
if !errwrap.ContainsType(err, new(logical.KeyNotFoundError)) {
|
||||
|
@ -21,7 +21,7 @@ func configureAWSKMSSeal(config *server.Config, infoKeys *[]string, info *map[st
|
|||
autoseal := vault.NewAutoSeal(kms)
|
||||
if kmsInfo != nil {
|
||||
*infoKeys = append(*infoKeys, "Seal Type", "AWS KMS Region", "AWS KMS KeyID")
|
||||
(*info)["Seal Type"] = config.Seal.Type
|
||||
(*info)["Seal Type"] = configSeal.Type
|
||||
(*info)["AWS KMS Region"] = kmsInfo["region"]
|
||||
(*info)["AWS KMS KeyID"] = kmsInfo["kms_key_id"]
|
||||
if endpoint, ok := kmsInfo["endpoint"]; ok {
|
||||
|
|
|
@ -9,9 +9,9 @@ import (
|
|||
"github.com/hashicorp/vault/vault/seal/azurekeyvault"
|
||||
)
|
||||
|
||||
func configureAzureKeyVaultSeal(config *server.Config, infoKeys *[]string, info *map[string]string, logger log.Logger, inseal vault.Seal) (vault.Seal, error) {
|
||||
func configureAzureKeyVaultSeal(configSeal *server.Seal, infoKeys *[]string, info *map[string]string, logger log.Logger, inseal vault.Seal) (vault.Seal, error) {
|
||||
kv := azurekeyvault.NewSeal(logger)
|
||||
kvInfo, err := kv.SetConfig(config.Seal.Config)
|
||||
kvInfo, err := kv.SetConfig(configSeal.Config)
|
||||
if err != nil {
|
||||
// If the error is any other than logical.KeyNotFoundError, return the error
|
||||
if !errwrap.ContainsType(err, new(logical.KeyNotFoundError)) {
|
||||
|
@ -21,7 +21,7 @@ func configureAzureKeyVaultSeal(config *server.Config, infoKeys *[]string, info
|
|||
autoseal := vault.NewAutoSeal(kv)
|
||||
if kvInfo != nil {
|
||||
*infoKeys = append(*infoKeys, "Seal Type", "Azure Environment", "Azure Vault Name", "Azure Key Name")
|
||||
(*info)["Seal Type"] = config.Seal.Type
|
||||
(*info)["Seal Type"] = configSeal.Type
|
||||
(*info)["Azure Environment"] = kvInfo["environment"]
|
||||
(*info)["Azure Vault Name"] = kvInfo["vault_name"]
|
||||
(*info)["Azure Key Name"] = kvInfo["key_name"]
|
||||
|
|
|
@ -9,9 +9,9 @@ import (
|
|||
"github.com/hashicorp/vault/vault/seal/gcpckms"
|
||||
)
|
||||
|
||||
func configureGCPCKMSSeal(config *server.Config, infoKeys *[]string, info *map[string]string, logger log.Logger, inseal vault.Seal) (vault.Seal, error) {
|
||||
func configureGCPCKMSSeal(configSeal *server.Seal, infoKeys *[]string, info *map[string]string, logger log.Logger, inseal vault.Seal) (vault.Seal, error) {
|
||||
kms := gcpckms.NewSeal(logger)
|
||||
kmsInfo, err := kms.SetConfig(config.Seal.Config)
|
||||
kmsInfo, err := kms.SetConfig(configSeal.Config)
|
||||
if err != nil {
|
||||
// If the error is any other than logical.KeyNotFoundError, return the error
|
||||
if !errwrap.ContainsType(err, new(logical.KeyNotFoundError)) {
|
||||
|
@ -21,7 +21,7 @@ func configureGCPCKMSSeal(config *server.Config, infoKeys *[]string, info *map[s
|
|||
autoseal := vault.NewAutoSeal(kms)
|
||||
if kmsInfo != nil {
|
||||
*infoKeys = append(*infoKeys, "Seal Type", "GCP KMS Project", "GCP KMS Region", "GCP KMS Key Ring", "GCP KMS Crypto Key")
|
||||
(*info)["Seal Type"] = config.Seal.Type
|
||||
(*info)["Seal Type"] = configSeal.Type
|
||||
(*info)["GCP KMS Project"] = kmsInfo["project"]
|
||||
(*info)["GCP KMS Region"] = kmsInfo["region"]
|
||||
(*info)["GCP KMS Key Ring"] = kmsInfo["key_ring"]
|
||||
|
|
|
@ -9,9 +9,9 @@ import (
|
|||
"github.com/hashicorp/vault/vault/seal/transit"
|
||||
)
|
||||
|
||||
func configureTransitSeal(config *server.Config, infoKeys *[]string, info *map[string]string, logger log.Logger, inseal vault.Seal) (vault.Seal, error) {
|
||||
func configureTransitSeal(configSeal *server.Seal, infoKeys *[]string, info *map[string]string, logger log.Logger, inseal vault.Seal) (vault.Seal, error) {
|
||||
transitSeal := transit.NewSeal(logger)
|
||||
sealInfo, err := transitSeal.SetConfig(config.Seal.Config)
|
||||
sealInfo, err := transitSeal.SetConfig(configSeal.Config)
|
||||
if err != nil {
|
||||
// If the error is any other than logical.KeyNotFoundError, return the error
|
||||
if !errwrap.ContainsType(err, new(logical.KeyNotFoundError)) {
|
||||
|
@ -21,7 +21,7 @@ func configureTransitSeal(config *server.Config, infoKeys *[]string, info *map[s
|
|||
autoseal := vault.NewAutoSeal(transitSeal)
|
||||
if sealInfo != nil {
|
||||
*infoKeys = append(*infoKeys, "Seal Type", "Transit Address", "Transit Mount Path", "Transit Key Name")
|
||||
(*info)["Seal Type"] = config.Seal.Type
|
||||
(*info)["Seal Type"] = configSeal.Type
|
||||
(*info)["Transit Address"] = sealInfo["address"]
|
||||
(*info)["Transit Mount Path"] = sealInfo["mount_path"]
|
||||
(*info)["Transit Key Name"] = sealInfo["key_name"]
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/vault/command/server"
|
||||
"github.com/hashicorp/vault/vault"
|
||||
vaultseal "github.com/hashicorp/vault/vault/seal"
|
||||
"github.com/pkg/errors"
|
||||
|
@ -14,63 +13,78 @@ var (
|
|||
onEnterprise = false
|
||||
)
|
||||
|
||||
func adjustCoreForSealMigration(ctx context.Context, core *vault.Core, coreConfig *vault.CoreConfig, seal vault.Seal, config *server.Config) error {
|
||||
func adjustCoreForSealMigration(core *vault.Core, barrierSeal, unwrapSeal vault.Seal) error {
|
||||
existBarrierSealConfig, existRecoverySealConfig, err := core.PhysicalSealConfigs(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error checking for existing seal: %s", err)
|
||||
}
|
||||
var existSeal vault.Seal
|
||||
var newSeal vault.Seal
|
||||
if existBarrierSealConfig != nil && existBarrierSealConfig.Type != vaultseal.HSMAutoDeprecated &&
|
||||
(existBarrierSealConfig.Type != seal.BarrierType() ||
|
||||
config.Seal != nil && config.Seal.Disabled) {
|
||||
// If the existing seal is not Shamir, we're going to Shamir, which
|
||||
// means we require them setting "disabled" to true in their
|
||||
// configuration as a sanity check.
|
||||
if (config.Seal == nil || !config.Seal.Disabled) && existBarrierSealConfig.Type != vaultseal.Shamir {
|
||||
return errors.New(`Seal migration requires specifying "disabled" as "true" in the "seal" block of Vault's configuration file"`)
|
||||
}
|
||||
|
||||
// Conversely, if they are going from Shamir to auto, we want to
|
||||
// ensure disabled is *not* set
|
||||
if existBarrierSealConfig.Type == vaultseal.Shamir && config.Seal != nil && config.Seal.Disabled {
|
||||
coreConfig.Logger.Warn(`when not migrating, Vault's config should not specify "disabled" as "true" in the "seal" block of Vault's configuration file`)
|
||||
// If we don't have an existing config or if it's the deprecated auto seal
|
||||
// which needs an upgrade, skip out
|
||||
if existBarrierSealConfig == nil || existBarrierSealConfig.Type == vaultseal.HSMAutoDeprecated {
|
||||
return nil
|
||||
}
|
||||
|
||||
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 == barrierSeal.BarrierType() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if existBarrierSealConfig.Type != vaultseal.Shamir && existRecoverySealConfig == nil {
|
||||
return errors.New(`Recovery seal configuration not found for existing seal`)
|
||||
// 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 != vaultseal.Shamir && barrierSeal.BarrierType() != vaultseal.Shamir {
|
||||
return errors.New(`Trying to migrate from auto-seal to auto-seal but no "disabled" seal stanza found`)
|
||||
}
|
||||
|
||||
switch existBarrierSealConfig.Type {
|
||||
case vaultseal.Shamir:
|
||||
// The value reflected in config is what we're going to
|
||||
existSeal = vault.NewDefaultSeal()
|
||||
existSeal.SetCore(core)
|
||||
newSeal = seal
|
||||
newBarrierSealConfig := &vault.SealConfig{
|
||||
Type: newSeal.BarrierType(),
|
||||
SecretShares: 1,
|
||||
SecretThreshold: 1,
|
||||
StoredShares: 1,
|
||||
}
|
||||
newSeal.SetCachedBarrierConfig(newBarrierSealConfig)
|
||||
newSeal.SetCachedRecoveryConfig(existBarrierSealConfig)
|
||||
|
||||
default:
|
||||
if onEnterprise {
|
||||
return errors.New("Migrating from autoseal to Shamir seal is not supported on Vault Enterprise")
|
||||
}
|
||||
|
||||
// The disabled value reflected in config is what we're going from
|
||||
existSeal = coreConfig.Seal
|
||||
newSeal = vault.NewDefaultSeal()
|
||||
newSeal.SetCore(core)
|
||||
newSeal.SetCachedBarrierConfig(existRecoverySealConfig)
|
||||
} else {
|
||||
if unwrapSeal.BarrierType() == vaultseal.Shamir {
|
||||
return errors.New("Shamir seals cannot be set disabled (they should simply not be set)")
|
||||
}
|
||||
|
||||
core.SetSealsForMigration(existSeal, newSeal)
|
||||
}
|
||||
|
||||
var existSeal vault.Seal
|
||||
var newSeal vault.Seal
|
||||
|
||||
if existBarrierSealConfig.Type == barrierSeal.BarrierType() {
|
||||
// In this case our migration seal is set so we are using it
|
||||
// (potentially) for unwrapping. Set it on core for that purpose then
|
||||
// exit.
|
||||
core.SetSealsForMigration(nil, nil, unwrapSeal)
|
||||
return nil
|
||||
}
|
||||
|
||||
if existBarrierSealConfig.Type != vaultseal.Shamir && existRecoverySealConfig == nil {
|
||||
return errors.New(`Recovery seal configuration not found for existing seal`)
|
||||
}
|
||||
|
||||
switch existBarrierSealConfig.Type {
|
||||
case vaultseal.Shamir:
|
||||
// The value reflected in config is what we're going to
|
||||
existSeal = vault.NewDefaultSeal()
|
||||
newSeal = barrierSeal
|
||||
newBarrierSealConfig := &vault.SealConfig{
|
||||
Type: newSeal.BarrierType(),
|
||||
SecretShares: 1,
|
||||
SecretThreshold: 1,
|
||||
StoredShares: 1,
|
||||
}
|
||||
newSeal.SetCachedBarrierConfig(newBarrierSealConfig)
|
||||
newSeal.SetCachedRecoveryConfig(existBarrierSealConfig)
|
||||
|
||||
default:
|
||||
if onEnterprise && barrierSeal.BarrierType() == vaultseal.Shamir {
|
||||
return errors.New("Migrating from autoseal to Shamir seal is not currently supported on Vault Enterprise")
|
||||
}
|
||||
|
||||
// If we're not cominng from Shamir we expect the previous seal to be
|
||||
// in the config and disabled.
|
||||
existSeal = unwrapSeal
|
||||
newSeal = barrierSeal
|
||||
newSeal.SetCachedBarrierConfig(existRecoverySealConfig)
|
||||
}
|
||||
|
||||
core.SetSealsForMigration(existSeal, newSeal, unwrapSeal)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -179,6 +179,10 @@ type Core struct {
|
|||
// seal we're migrating *from*.
|
||||
migrationSeal Seal
|
||||
|
||||
// unwrapSeal is the seal to use on Enterprise to unwrap values wrapped
|
||||
// with the previous seal.
|
||||
unwrapSeal Seal
|
||||
|
||||
// barrier is the security barrier wrapping the physical backend
|
||||
barrier SecurityBarrier
|
||||
|
||||
|
@ -921,9 +925,12 @@ func (c *Core) unsealPart(ctx context.Context, seal Seal, key []byte, useRecover
|
|||
return nil, errwrap.Wrapf("unable to retrieve stored keys: {{err}}", err)
|
||||
}
|
||||
|
||||
if len(masterKeyShares) == 1 {
|
||||
switch len(masterKeyShares) {
|
||||
case 0:
|
||||
return nil, errors.New("seal returned no master key shares")
|
||||
case 1:
|
||||
masterKey = masterKeyShares[0]
|
||||
} else {
|
||||
default:
|
||||
masterKey, err = shamir.Combine(masterKeyShares)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("failed to compute master key: {{err}}", err)
|
||||
|
@ -942,10 +949,50 @@ func (c *Core) unsealPart(ctx context.Context, seal Seal, key []byte, useRecover
|
|||
}
|
||||
defer c.barrier.Seal()
|
||||
|
||||
// The seal used in this function will have been the migration seal,
|
||||
// and c.seal will be the opposite type, so there are two
|
||||
// possibilities: Shamir to auto, and auto to Shamir.
|
||||
if !seal.RecoveryKeySupported() {
|
||||
switch {
|
||||
case c.migrationSeal.RecoveryKeySupported() && c.seal.RecoveryKeySupported():
|
||||
// Set the recovery and barrier keys to be the same.
|
||||
recoveryKey, err := c.migrationSeal.RecoveryKey(ctx)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("error getting recovery key to set on new seal: {{err}}", err)
|
||||
}
|
||||
|
||||
if err := c.seal.SetRecoveryKey(ctx, recoveryKey); err != nil {
|
||||
return nil, errwrap.Wrapf("error setting new recovery key information during migrate: {{err}}", err)
|
||||
}
|
||||
|
||||
barrierKeys, err := c.migrationSeal.GetStoredKeys(ctx)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("error getting stored keys to set on new seal: {{err}}", err)
|
||||
}
|
||||
|
||||
if err := c.seal.SetStoredKeys(ctx, barrierKeys); err != nil {
|
||||
return nil, errwrap.Wrapf("error setting new barrier key information during migrate: {{err}}", err)
|
||||
}
|
||||
|
||||
case c.migrationSeal.RecoveryKeySupported():
|
||||
// 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 recoveryKey == nil {
|
||||
return nil, errors.New("did not get expected recovery information to set new seal during migration")
|
||||
}
|
||||
|
||||
// We have recovery keys; we're going to use them as the new
|
||||
// barrier key.
|
||||
if err := c.barrier.Rekey(ctx, recoveryKey); err != nil {
|
||||
return nil, errwrap.Wrapf("error rekeying barrier during migration: {{err}}", err)
|
||||
}
|
||||
|
||||
if err := c.barrier.Delete(ctx, StoredBarrierKeysPath); err != nil {
|
||||
// Don't actually exit here as successful deletion isn't critical
|
||||
c.logger.Error("error deleting stored barrier keys after migration; continuing anyways", "error", err)
|
||||
}
|
||||
|
||||
masterKey = recoveryKey
|
||||
|
||||
case c.seal.RecoveryKeySupported():
|
||||
// The new seal will have recovery keys; we set it to the existing
|
||||
// master key, so barrier key shares -> recovery key shares
|
||||
if err := c.seal.SetRecoveryKey(ctx, masterKey); err != nil {
|
||||
|
@ -970,25 +1017,9 @@ func (c *Core) unsealPart(ctx context.Context, seal Seal, key []byte, useRecover
|
|||
|
||||
// Return the new key so it can be used to unlock the barrier
|
||||
masterKey = newMasterKey
|
||||
} else {
|
||||
// In this case we have to ensure that the recovery information was
|
||||
// set properly.
|
||||
if recoveryKey == nil {
|
||||
return nil, errors.New("did not get expected recovery information to set new seal during migration")
|
||||
}
|
||||
|
||||
// Auto to Shamir. We have recovery keys; we're going to use them
|
||||
// as the new barrier key
|
||||
if err := c.barrier.Rekey(ctx, recoveryKey); err != nil {
|
||||
return nil, errwrap.Wrapf("error rekeying barrier during migration: {{err}}", err)
|
||||
}
|
||||
|
||||
if err := c.barrier.Delete(ctx, StoredBarrierKeysPath); err != nil {
|
||||
// Don't actually exit here as successful deletion isn't critical
|
||||
c.logger.Error("error deleting stored barrier keys after migration; continuing anyways", "error", err)
|
||||
}
|
||||
|
||||
masterKey = recoveryKey
|
||||
default:
|
||||
return nil, errors.New("unhandled migration case (shamir to shamir)")
|
||||
}
|
||||
|
||||
// At this point we've swapped things around and need to ensure we
|
||||
|
@ -1700,12 +1731,20 @@ func (c *Core) PhysicalSealConfigs(ctx context.Context) (*SealConfig, *SealConfi
|
|||
return barrierConf, recoveryConf, nil
|
||||
}
|
||||
|
||||
func (c *Core) SetSealsForMigration(migrationSeal, newSeal Seal) {
|
||||
func (c *Core) SetSealsForMigration(migrationSeal, newSeal, unwrapSeal Seal) {
|
||||
c.stateLock.Lock()
|
||||
defer c.stateLock.Unlock()
|
||||
c.migrationSeal = migrationSeal
|
||||
c.seal = newSeal
|
||||
c.logger.Warn("entering seal migration mode; Vault will not automatically unseal even if using an autoseal")
|
||||
c.unwrapSeal = unwrapSeal
|
||||
if c.unwrapSeal != nil {
|
||||
c.unwrapSeal.SetCore(c)
|
||||
}
|
||||
if newSeal != nil && migrationSeal != nil {
|
||||
c.migrationSeal = migrationSeal
|
||||
c.migrationSeal.SetCore(c)
|
||||
c.seal = newSeal
|
||||
c.seal.SetCore(c)
|
||||
c.logger.Warn("entering seal migration mode; Vault will not automatically unseal even if using an autoseal", "from_barrier_type", c.migrationSeal.BarrierType(), "to_barrier_type", c.seal.BarrierType())
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Core) IsInSealMigration() bool {
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
)
|
||||
|
||||
type TestSeal struct {
|
||||
Type string
|
||||
secret []byte
|
||||
}
|
||||
|
||||
|
@ -15,6 +16,7 @@ var _ Access = (*TestSeal)(nil)
|
|||
|
||||
func NewTestSeal(secret []byte) *TestSeal {
|
||||
return &TestSeal{
|
||||
Type: Test,
|
||||
secret: secret,
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +30,7 @@ func (t *TestSeal) Finalize(_ context.Context) error {
|
|||
}
|
||||
|
||||
func (t *TestSeal) SealType() string {
|
||||
return Test
|
||||
return t.Type
|
||||
}
|
||||
|
||||
func (t *TestSeal) KeyID() string {
|
||||
|
|
|
@ -303,30 +303,6 @@ func (d *autoSeal) RecoveryConfig(ctx context.Context) (*SealConfig, error) {
|
|||
return conf.Clone(), nil
|
||||
}
|
||||
|
||||
func (d *autoSeal) RecoveryKey(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)
|
||||
return nil, errwrap.Wrapf("failed to read recovery key: {{err}}", err)
|
||||
}
|
||||
if pe == nil {
|
||||
d.core.logger.Warn("autoseal: no recovery key found")
|
||||
return nil, fmt.Errorf("no recovery key found")
|
||||
}
|
||||
|
||||
blobInfo := &physical.EncryptedBlobInfo{}
|
||||
if err := proto.Unmarshal(pe.Value, blobInfo); err != nil {
|
||||
return nil, errwrap.Wrapf("failed to proto decode recovery keys: {{err}}", err)
|
||||
}
|
||||
|
||||
pt, err := d.Decrypt(ctx, blobInfo)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("failed to decrypt encrypted recovery keys: {{err}}", err)
|
||||
}
|
||||
|
||||
return pt, nil
|
||||
}
|
||||
|
||||
// SetRecoveryConfig writes the recovery configuration to the physical storage
|
||||
// and sets it as the seal's recoveryConfig.
|
||||
func (d *autoSeal) SetRecoveryConfig(ctx context.Context, conf *SealConfig) error {
|
||||
|
@ -377,7 +353,7 @@ func (d *autoSeal) VerifyRecoveryKey(ctx context.Context, key []byte) error {
|
|||
return fmt.Errorf("recovery key to verify is nil")
|
||||
}
|
||||
|
||||
pt, err := d.RecoveryKey(ctx)
|
||||
pt, err := d.getRecoveryKeyInternal(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -422,6 +398,34 @@ func (d *autoSeal) SetRecoveryKey(ctx context.Context, key []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (d *autoSeal) RecoveryKey(ctx context.Context) ([]byte, error) {
|
||||
return d.getRecoveryKeyInternal(ctx)
|
||||
}
|
||||
|
||||
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)
|
||||
return nil, errwrap.Wrapf("failed to read recovery key: {{err}}", err)
|
||||
}
|
||||
if pe == nil {
|
||||
d.core.logger.Warn("autoseal: no recovery key found")
|
||||
return nil, fmt.Errorf("no recovery key found")
|
||||
}
|
||||
|
||||
blobInfo := &physical.EncryptedBlobInfo{}
|
||||
if err := proto.Unmarshal(pe.Value, blobInfo); err != nil {
|
||||
return nil, errwrap.Wrapf("failed to proto decode stored keys: {{err}}", err)
|
||||
}
|
||||
|
||||
pt, err := d.Decrypt(ctx, blobInfo)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf("failed to decrypt encrypted stored keys: {{err}}", err)
|
||||
}
|
||||
|
||||
return pt, 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.
|
||||
|
|
Loading…
Reference in New Issue