Recovery Mode (#7559)

* Initial work

* rework

* s/dr/recovery

* Add sys/raw support to recovery mode (#7577)

* Factor the raw paths out so they can be run with a SystemBackend.

# Conflicts:
#	vault/logical_system.go

* Add handleLogicalRecovery which is like handleLogical but is only
sufficient for use with the sys-raw endpoint in recovery mode.  No
authentication is done yet.

* Integrate with recovery-mode.  We now handle unauthenticated sys/raw
requests, albeit on path v1/raw instead v1/sys/raw.

* Use sys/raw instead raw during recovery.

* Don't bother persisting the recovery token.  Authenticate sys/raw
requests with it.

* RecoveryMode: Support generate-root for autounseals (#7591)

* Recovery: Abstract config creation and log settings

* Recovery mode integration test. (#7600)

* Recovery: Touch up (#7607)

* Recovery: Touch up

* revert the raw backend creation changes

* Added recovery operation token prefix

* Move RawBackend to its own file

* Update API path and hit it using CLI flag on generate-root

* Fix a panic triggered when handling a request that yields a nil response. (#7618)

* Improve integ test to actually make changes while in recovery mode and
verify they're still there after coming back in regular mode.

* Refuse to allow a second recovery token to be generated.

* Resize raft cluster to size 1 and start as leader (#7626)

* RecoveryMode: Setup raft cluster post unseal (#7635)

* Setup raft cluster post unseal in recovery mode

* Remove marking as unsealed as its not needed

* Address review comments

* Accept only one seal config in recovery mode as there is no scope for migration
This commit is contained in:
Vishal Nayak
2019-10-15 00:55:31 -04:00
committed by GitHub
parent 9de6e1bfd0
commit 2f4ae7f215
24 changed files with 1264 additions and 413 deletions

View File

@@ -6,6 +6,7 @@ import (
"encoding/base64"
"encoding/hex"
"fmt"
"go.uber.org/atomic"
"io"
"io/ioutil"
"net"
@@ -99,6 +100,7 @@ type ServerCommand struct {
flagConfigs []string
flagLogLevel string
flagLogFormat string
flagRecovery bool
flagDev bool
flagDevRootTokenID string
flagDevListenAddr string
@@ -197,6 +199,13 @@ func (c *ServerCommand) Flags() *FlagSets {
Usage: `Log format. Supported values are "standard" and "json".`,
})
f.BoolVar(&BoolVar{
Name: "recovery",
Target: &c.flagRecovery,
Usage: "Enable recovery mode. In this mode, Vault is used to perform recovery actions." +
"Using a recovery operation token, \"sys/raw\" API can be used to manipulate the storage.",
})
f = set.NewFlagSet("Dev Options")
f.BoolVar(&BoolVar{
@@ -365,6 +374,384 @@ func (c *ServerCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *ServerCommand) parseConfig() (*server.Config, error) {
// Load the configuration
var config *server.Config
for _, path := range c.flagConfigs {
current, err := server.LoadConfig(path)
if err != nil {
return nil, errwrap.Wrapf(fmt.Sprintf("error loading configuration from %s: {{err}}", path), err)
}
if config == nil {
config = current
} else {
config = config.Merge(current)
}
}
return config, nil
}
func (c *ServerCommand) runRecoveryMode() int {
config, err := c.parseConfig()
if err != nil {
c.UI.Error(err.Error())
return 1
}
// Ensure at least one config was found.
if config == nil {
c.UI.Output(wrapAtLength(
"No configuration files found. Please provide configurations with the " +
"-config flag. If you are supplying the path to a directory, please " +
"ensure the directory contains files with the .hcl or .json " +
"extension."))
return 1
}
level, logLevelString, logLevelWasNotSet, logFormat, err := c.processLogLevelAndFormat(config)
if err != nil {
c.UI.Error(err.Error())
return 1
}
c.logger = log.New(&log.LoggerOptions{
Output: c.logWriter,
Level: level,
// Note that if logFormat is either unspecified or standard, then
// the resulting logger's format will be standard.
JSONFormat: logFormat == logging.JSONFormat,
})
logLevelStr, err := c.adjustLogLevel(config, logLevelWasNotSet)
if err != nil {
c.UI.Error(err.Error())
return 1
}
if logLevelStr != "" {
logLevelString = logLevelStr
}
// create GRPC logger
namedGRPCLogFaker := c.logger.Named("grpclogfaker")
grpclog.SetLogger(&grpclogFaker{
logger: namedGRPCLogFaker,
log: os.Getenv("VAULT_GRPC_LOGGING") != "",
})
if config.Storage == nil {
c.UI.Output("A storage backend must be specified")
return 1
}
if config.DefaultMaxRequestDuration != 0 {
vault.DefaultMaxRequestDuration = config.DefaultMaxRequestDuration
}
proxyCfg := httpproxy.FromEnvironment()
c.logger.Info("proxy environment", "http_proxy", proxyCfg.HTTPProxy,
"https_proxy", proxyCfg.HTTPSProxy, "no_proxy", proxyCfg.NoProxy)
// Initialize the storage backend
factory, exists := c.PhysicalBackends[config.Storage.Type]
if !exists {
c.UI.Error(fmt.Sprintf("Unknown storage type %s", config.Storage.Type))
return 1
}
if config.Storage.Type == "raft" {
if envCA := os.Getenv("VAULT_CLUSTER_ADDR"); envCA != "" {
config.ClusterAddr = envCA
}
if len(config.ClusterAddr) == 0 {
c.UI.Error("Cluster address must be set when using raft storage")
return 1
}
}
namedStorageLogger := c.logger.Named("storage." + config.Storage.Type)
backend, err := factory(config.Storage.Config, namedStorageLogger)
if err != nil {
c.UI.Error(fmt.Sprintf("Error initializing storage of type %s: %s", config.Storage.Type, err))
return 1
}
infoKeys := make([]string, 0, 10)
info := make(map[string]string)
info["log level"] = logLevelString
infoKeys = append(infoKeys, "log level")
var barrierSeal vault.Seal
var sealConfigError error
if len(config.Seals) == 0 {
config.Seals = append(config.Seals, &server.Seal{Type: vaultseal.Shamir})
}
if len(config.Seals) > 1 {
c.UI.Error("Only one seal block is accepted in recovery mode")
return 1
}
configSeal := config.Seals[0]
sealType := vaultseal.Shamir
if !configSeal.Disabled && os.Getenv("VAULT_SEAL_TYPE") != "" {
sealType = os.Getenv("VAULT_SEAL_TYPE")
configSeal.Type = sealType
} else {
sealType = configSeal.Type
}
var seal vault.Seal
sealLogger := c.logger.Named(sealType)
seal, sealConfigError = serverseal.ConfigureSeal(configSeal, &infoKeys, &info, sealLogger, vault.NewDefaultSeal(shamirseal.NewSeal(c.logger.Named("shamir"))))
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(
"After configuring seal nil returned, seal type was %s", sealType))
return 1
}
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))
}
}()
coreConfig := &vault.CoreConfig{
Physical: backend,
StorageType: config.Storage.Type,
Seal: barrierSeal,
Logger: c.logger,
DisableMlock: config.DisableMlock,
RecoveryMode: c.flagRecovery,
ClusterAddr: config.ClusterAddr,
}
core, newCoreError := vault.NewCore(coreConfig)
if newCoreError != nil {
if vault.IsFatalError(newCoreError) {
c.UI.Error(fmt.Sprintf("Error initializing core: %s", newCoreError))
return 1
}
}
if err := core.InitializeRecovery(context.Background()); err != nil {
c.UI.Error(fmt.Sprintf("Error initializing core in recovery mode: %s", err))
return 1
}
// Compile server information for output later
infoKeys = append(infoKeys, "storage")
info["storage"] = config.Storage.Type
if coreConfig.ClusterAddr != "" {
info["cluster address"] = coreConfig.ClusterAddr
infoKeys = append(infoKeys, "cluster address")
}
// Initialize the listeners
lns := make([]ServerListener, 0, len(config.Listeners))
for _, lnConfig := range config.Listeners {
ln, _, _, err := server.NewListener(lnConfig.Type, lnConfig.Config, c.logWriter, c.UI)
if err != nil {
c.UI.Error(fmt.Sprintf("Error initializing listener of type %s: %s", lnConfig.Type, err))
return 1
}
lns = append(lns, ServerListener{
Listener: ln,
config: lnConfig.Config,
})
}
listenerCloseFunc := func() {
for _, ln := range lns {
ln.Listener.Close()
}
}
defer c.cleanupGuard.Do(listenerCloseFunc)
infoKeys = append(infoKeys, "version")
verInfo := version.GetVersion()
info["version"] = verInfo.FullVersionNumber(false)
if verInfo.Revision != "" {
info["version sha"] = strings.Trim(verInfo.Revision, "'")
infoKeys = append(infoKeys, "version sha")
}
infoKeys = append(infoKeys, "recovery mode")
info["recovery mode"] = "true"
// Server configuration output
padding := 24
sort.Strings(infoKeys)
c.UI.Output("==> Vault server configuration:\n")
for _, k := range infoKeys {
c.UI.Output(fmt.Sprintf(
"%s%s: %s",
strings.Repeat(" ", padding-len(k)),
strings.Title(k),
info[k]))
}
c.UI.Output("")
for _, ln := range lns {
handler := vaulthttp.Handler(&vault.HandlerProperties{
Core: core,
MaxRequestSize: ln.maxRequestSize,
MaxRequestDuration: ln.maxRequestDuration,
DisablePrintableCheck: config.DisablePrintableCheck,
RecoveryMode: c.flagRecovery,
RecoveryToken: atomic.NewString(""),
})
server := &http.Server{
Handler: handler,
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 30 * time.Second,
IdleTimeout: 5 * time.Minute,
ErrorLog: c.logger.StandardLogger(nil),
}
go server.Serve(ln.Listener)
}
if sealConfigError != nil {
init, err := core.Initialized(context.Background())
if err != nil {
c.UI.Error(fmt.Sprintf("Error checking if core is initialized: %v", err))
return 1
}
if init {
c.UI.Error("Vault is initialized but no Seal key could be loaded")
return 1
}
}
if newCoreError != nil {
c.UI.Warn(wrapAtLength(
"WARNING! A non-fatal error occurred during initialization. Please " +
"check the logs for more information."))
c.UI.Warn("")
}
if !c.flagCombineLogs {
c.UI.Output("==> Vault server started! Log data will stream in below:\n")
}
c.logGate.Flush()
for {
select {
case <-c.ShutdownCh:
c.UI.Output("==> Vault shutdown triggered")
c.cleanupGuard.Do(listenerCloseFunc)
if err := core.Shutdown(); err != nil {
c.UI.Error(fmt.Sprintf("Error with core shutdown: %s", err))
}
return 0
case <-c.SigUSR2Ch:
buf := make([]byte, 32*1024*1024)
n := runtime.Stack(buf[:], true)
c.logger.Info("goroutine trace", "stack", string(buf[:n]))
}
}
return 0
}
func (c *ServerCommand) adjustLogLevel(config *server.Config, logLevelWasNotSet bool) (string, error) {
var logLevelString string
if config.LogLevel != "" && logLevelWasNotSet {
configLogLevel := strings.ToLower(strings.TrimSpace(config.LogLevel))
logLevelString = configLogLevel
switch configLogLevel {
case "trace":
c.logger.SetLevel(log.Trace)
case "debug":
c.logger.SetLevel(log.Debug)
case "notice", "info", "":
c.logger.SetLevel(log.Info)
case "warn", "warning":
c.logger.SetLevel(log.Warn)
case "err", "error":
c.logger.SetLevel(log.Error)
default:
return "", fmt.Errorf("unknown log level: %s", config.LogLevel)
}
}
return logLevelString, nil
}
func (c *ServerCommand) processLogLevelAndFormat(config *server.Config) (log.Level, string, bool, logging.LogFormat, error) {
// Create a logger. We wrap it in a gated writer so that it doesn't
// start logging too early.
c.logGate = &gatedwriter.Writer{Writer: os.Stderr}
c.logWriter = c.logGate
if c.flagCombineLogs {
c.logWriter = os.Stdout
}
var level log.Level
var logLevelWasNotSet bool
logFormat := logging.UnspecifiedFormat
logLevelString := c.flagLogLevel
c.flagLogLevel = strings.ToLower(strings.TrimSpace(c.flagLogLevel))
switch c.flagLogLevel {
case notSetValue, "":
logLevelWasNotSet = true
logLevelString = "info"
level = log.Info
case "trace":
level = log.Trace
case "debug":
level = log.Debug
case "notice", "info":
level = log.Info
case "warn", "warning":
level = log.Warn
case "err", "error":
level = log.Error
default:
return level, logLevelString, logLevelWasNotSet, logFormat, fmt.Errorf("unknown log level: %s", c.flagLogLevel)
}
if c.flagLogFormat != notSetValue {
var err error
logFormat, err = logging.ParseLogFormat(c.flagLogFormat)
if err != nil {
return level, logLevelString, logLevelWasNotSet, logFormat, err
}
}
if logFormat == logging.UnspecifiedFormat {
logFormat = logging.ParseEnvLogFormat()
}
if logFormat == logging.UnspecifiedFormat {
var err error
logFormat, err = logging.ParseLogFormat(config.LogFormat)
if err != nil {
return level, logLevelString, logLevelWasNotSet, logFormat, err
}
}
return level, logLevelString, logLevelWasNotSet, logFormat, nil
}
func (c *ServerCommand) Run(args []string) int {
f := c.Flags()
@@ -373,6 +760,10 @@ func (c *ServerCommand) Run(args []string) int {
return 1
}
if c.flagRecovery {
return c.runRecoveryMode()
}
// Automatically enable dev mode if other dev flags are provided.
if c.flagDevHA || c.flagDevTransactional || c.flagDevLeasedKV || c.flagDevThreeNode || c.flagDevFourCluster || c.flagDevAutoSeal || c.flagDevKVV1 {
c.flagDev = true
@@ -413,18 +804,16 @@ func (c *ServerCommand) Run(args []string) int {
config.Listeners[0].Config["address"] = c.flagDevListenAddr
}
}
for _, path := range c.flagConfigs {
current, err := server.LoadConfig(path)
if err != nil {
c.UI.Error(fmt.Sprintf("Error loading configuration from %s: %s", path, err))
return 1
}
if config == nil {
config = current
} else {
config = config.Merge(current)
}
parsedConfig, err := c.parseConfig()
if err != nil {
c.UI.Error(err.Error())
return 1
}
if config == nil {
config = parsedConfig
} else {
config = config.Merge(parsedConfig)
}
// Ensure at least one config was found.
@@ -437,58 +826,12 @@ func (c *ServerCommand) Run(args []string) int {
return 1
}
// Create a logger. We wrap it in a gated writer so that it doesn't
// start logging too early.
c.logGate = &gatedwriter.Writer{Writer: os.Stderr}
c.logWriter = c.logGate
if c.flagCombineLogs {
c.logWriter = os.Stdout
}
var level log.Level
var logLevelWasNotSet bool
logLevelString := c.flagLogLevel
c.flagLogLevel = strings.ToLower(strings.TrimSpace(c.flagLogLevel))
switch c.flagLogLevel {
case notSetValue, "":
logLevelWasNotSet = true
logLevelString = "info"
level = log.Info
case "trace":
level = log.Trace
case "debug":
level = log.Debug
case "notice", "info":
level = log.Info
case "warn", "warning":
level = log.Warn
case "err", "error":
level = log.Error
default:
c.UI.Error(fmt.Sprintf("Unknown log level: %s", c.flagLogLevel))
level, logLevelString, logLevelWasNotSet, logFormat, err := c.processLogLevelAndFormat(config)
if err != nil {
c.UI.Error(err.Error())
return 1
}
logFormat := logging.UnspecifiedFormat
if c.flagLogFormat != notSetValue {
var err error
logFormat, err = logging.ParseLogFormat(c.flagLogFormat)
if err != nil {
c.UI.Error(err.Error())
return 1
}
}
if logFormat == logging.UnspecifiedFormat {
logFormat = logging.ParseEnvLogFormat()
}
if logFormat == logging.UnspecifiedFormat {
var err error
logFormat, err = logging.ParseLogFormat(config.LogFormat)
if err != nil {
c.UI.Error(err.Error())
return 1
}
}
if c.flagDevThreeNode || c.flagDevFourCluster {
c.logger = log.New(&log.LoggerOptions{
Mutex: &sync.Mutex{},
@@ -507,25 +850,13 @@ func (c *ServerCommand) Run(args []string) int {
allLoggers := []log.Logger{c.logger}
// adjust log level based on config setting
if config.LogLevel != "" && logLevelWasNotSet {
configLogLevel := strings.ToLower(strings.TrimSpace(config.LogLevel))
logLevelString = configLogLevel
switch configLogLevel {
case "trace":
c.logger.SetLevel(log.Trace)
case "debug":
c.logger.SetLevel(log.Debug)
case "notice", "info", "":
c.logger.SetLevel(log.Info)
case "warn", "warning":
c.logger.SetLevel(log.Warn)
case "err", "error":
c.logger.SetLevel(log.Error)
default:
c.UI.Error(fmt.Sprintf("Unknown log level: %s", config.LogLevel))
return 1
}
logLevelStr, err := c.adjustLogLevel(config, logLevelWasNotSet)
if err != nil {
c.UI.Error(err.Error())
return 1
}
if logLevelStr != "" {
logLevelString = logLevelStr
}
// create GRPC logger
@@ -580,7 +911,6 @@ func (c *ServerCommand) Run(args []string) int {
return 1
}
if config.Storage.Type == "raft" {
if envCA := os.Getenv("VAULT_CLUSTER_ADDR"); envCA != "" {
config.ClusterAddr = envCA
@@ -1066,6 +1396,9 @@ CLUSTER_SYNTHESIS_COMPLETE:
info["cgo"] = "enabled"
}
infoKeys = append(infoKeys, "recovery mode")
info["recovery mode"] = "false"
// Server configuration output
padding := 24
sort.Strings(infoKeys)
@@ -1263,6 +1596,7 @@ CLUSTER_SYNTHESIS_COMPLETE:
MaxRequestDuration: ln.maxRequestDuration,
DisablePrintableCheck: config.DisablePrintableCheck,
UnauthenticatedMetricsAccess: ln.unauthenticatedMetricsAccess,
RecoveryMode: c.flagRecovery,
})
// We perform validation on the config earlier, we can just cast here