Skip to content

Commit

Permalink
update prompts for rekey command
Browse files Browse the repository at this point in the history
  • Loading branch information
miagilepner committed Jan 27, 2023
1 parent 196e164 commit 9ffd602
Show file tree
Hide file tree
Showing 3 changed files with 287 additions and 61 deletions.
44 changes: 34 additions & 10 deletions command/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/sdk/physical/inmem"
"github.com/hashicorp/vault/vault"
"github.com/hashicorp/vault/vault/seal"
"github.com/mitchellh/cli"

auditFile "github.com/hashicorp/vault/builtin/audit/file"
Expand Down Expand Up @@ -70,7 +71,7 @@ func testVaultServer(tb testing.TB) (*api.Client, func()) {
func testVaultServerWithKVVersion(tb testing.TB, kvVersion string) (*api.Client, func()) {
tb.Helper()

client, _, closer := testVaultServerUnsealWithKVVersion(tb, kvVersion)
client, _, closer := testVaultServerUnsealWithKVVersionWithSeal(tb, kvVersion, nil)
return client, closer
}

Expand All @@ -89,13 +90,24 @@ func testVaultServerAllBackends(tb testing.TB) (*api.Client, func()) {
return client, closer
}

// testVaultServerAutoUnseal creates a test vault cluster and sets it up with auto unseal
// the function returns a client, the recovery keys, and a closer function
func testVaultServerAutoUnseal(tb testing.TB) (*api.Client, []string, func()) {
testSeal := seal.NewTestSeal(nil)
autoSeal, err := vault.NewAutoSeal(testSeal)
if err != nil {
tb.Fatal("unable to create autoseal", err)
}
return testVaultServerUnsealWithKVVersionWithSeal(tb, "1", autoSeal)
}

// testVaultServerUnseal creates a test vault cluster and returns a configured
// API client, list of unseal keys (as strings), and a closer function.
func testVaultServerUnseal(tb testing.TB) (*api.Client, []string, func()) {
return testVaultServerUnsealWithKVVersion(tb, "1")
return testVaultServerUnsealWithKVVersionWithSeal(tb, "1", nil)
}

func testVaultServerUnsealWithKVVersion(tb testing.TB, kvVersion string) (*api.Client, []string, func()) {
func testVaultServerUnsealWithKVVersionWithSeal(tb testing.TB, kvVersion string, seal vault.Seal) (*api.Client, []string, func()) {
tb.Helper()
logger := log.NewInterceptLogger(&log.LoggerOptions{
Output: log.DefaultOutput,
Expand All @@ -111,6 +123,7 @@ func testVaultServerUnsealWithKVVersion(tb testing.TB, kvVersion string) (*api.C
AuditBackends: defaultVaultAuditBackends,
LogicalBackends: defaultVaultLogicalBackends,
BuiltinRegistry: builtinplugins.Registry,
Seal: seal,
}, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
NumCores: 1,
Expand Down Expand Up @@ -144,7 +157,8 @@ func testVaultServerCoreConfig(tb testing.TB, coreConfig *vault.CoreConfig) (*ap
}

// testVaultServerCoreConfig creates a new vault cluster with the given core
// configuration. This is a lower-level test helper.
// configuration. This is a lower-level test helper. If the seal config supports recovery keys, then
// recovery keys are returned. Otherwise, unseal keys are returned
func testVaultServerCoreConfigWithOpts(tb testing.TB, coreConfig *vault.CoreConfig, opts *vault.TestClusterOptions) (*api.Client, []string, func()) {
tb.Helper()

Expand All @@ -159,14 +173,24 @@ func testVaultServerCoreConfigWithOpts(tb testing.TB, coreConfig *vault.CoreConf
client := cluster.Cores[0].Client
client.SetToken(cluster.RootToken)

// Convert the unseal keys to base64 encoded, since these are how the user
// will get them.
unsealKeys := make([]string, len(cluster.BarrierKeys))
for i := range unsealKeys {
unsealKeys[i] = base64.StdEncoding.EncodeToString(cluster.BarrierKeys[i])
var keys [][]byte
if coreConfig.Seal != nil && coreConfig.Seal.RecoveryKeySupported() {
keys = cluster.RecoveryKeys
} else {
keys = cluster.BarrierKeys
}

return client, unsealKeys, func() { defer cluster.Cleanup() }
return client, encodeKeys(keys), func() { defer cluster.Cleanup() }
}

// Convert the unseal keys to base64 encoded, since these are how the user
// will get them.
func encodeKeys(rawKeys [][]byte) []string {
keys := make([]string, len(rawKeys))
for i := range rawKeys {
keys[i] = base64.StdEncoding.EncodeToString(rawKeys[i])
}
return keys
}

// testVaultServerUninit creates an uninitialized server.
Expand Down
138 changes: 87 additions & 51 deletions command/operator_rekey.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ var (
_ cli.CommandAutocomplete = (*OperatorRekeyCommand)(nil)
)

const (
keyTypeRecovery = "Recovery"
keyTypeUnseal = "Unseal"
)

type OperatorRekeyCommand struct {
*BaseCommand

Expand Down Expand Up @@ -58,6 +63,11 @@ Usage: vault operator rekey [options] [KEY]
the command. If key is specified as "-", the command will read from stdin. If
a TTY is available, the command will prompt for text.
If the flag -target=recovery is supplied, then this operation will
generate a new set of recovery keys rather than unseal keys. These recovery
keys provide an authorization mechanism for actions that require a quorum. The
recovery keys will not be able to unseal vault.
Initialize a rekey:
$ vault operator rekey \
Expand Down Expand Up @@ -112,15 +122,15 @@ func (c *OperatorRekeyCommand) Flags() *FlagSets {
Target: &c.flagCancel,
Default: false,
Usage: "Reset the rekeying progress. This will discard any submitted " +
"unseal keys or configuration.",
"unseal keys, recovery keys, or configuration.",
})

f.BoolVar(&BoolVar{
Name: "status",
Target: &c.flagStatus,
Default: false,
Usage: "Print the status of the current attempt without providing an " +
"unseal key.",
"unseal or recovery key.",
})

f.IntVar(&IntVar{
Expand All @@ -130,7 +140,7 @@ func (c *OperatorRekeyCommand) Flags() *FlagSets {
Default: 5,
Completion: complete.PredictAnything,
Usage: "Number of key shares to split the generated root key into. " +
"This is the number of \"unseal keys\" to generate.",
"This is the number of \"unseal keys\" or \"recovery keys\" to generate.",
})

f.IntVar(&IntVar{
Expand All @@ -150,7 +160,7 @@ func (c *OperatorRekeyCommand) Flags() *FlagSets {
EnvVar: "",
Completion: complete.PredictAnything,
Usage: "Nonce value provided at initialization. The same nonce value " +
"must be provided with each unseal key.",
"must be provided with each unseal or recovery key.",
})

f.StringVar(&StringVar{
Expand Down Expand Up @@ -179,7 +189,7 @@ func (c *OperatorRekeyCommand) Flags() *FlagSets {
Usage: "Comma-separated list of paths to files on disk containing " +
"public PGP keys OR a comma-separated list of Keybase usernames using " +
"the format \"keybase:<username>\". When supplied, the generated " +
"unseal keys will be encrypted and base64-encoded in the order " +
"unseal or recovery keys will be encrypted and base64-encoded in the order " +
"specified in this list.",
})

Expand All @@ -189,25 +199,25 @@ func (c *OperatorRekeyCommand) Flags() *FlagSets {
Name: "backup",
Target: &c.flagBackup,
Default: false,
Usage: "Store a backup of the current PGP encrypted unseal keys in " +
Usage: "Store a backup of the current PGP encrypted unseal or recovery keys in " +
"Vault's core. The encrypted values can be recovered in the event of " +
"failure or discarded after success. See the -backup-delete and " +
"-backup-retrieve options for more information. This option only " +
"applies when the existing unseal keys were PGP encrypted.",
"applies when the existing unseal or recovery keys were PGP encrypted.",
})

f.BoolVar(&BoolVar{
Name: "backup-delete",
Target: &c.flagBackupDelete,
Default: false,
Usage: "Delete any stored backup unseal keys.",
Usage: "Delete any stored backup unseal or recovery keys.",
})

f.BoolVar(&BoolVar{
Name: "backup-retrieve",
Target: &c.flagBackupRetrieve,
Default: false,
Usage: "Retrieve the backed-up unseal keys. This option is only available " +
Usage: "Retrieve the backed-up unseal or recovery keys. This option is only available " +
"if the PGP keys were provided and the backup has not been deleted.",
})

Expand Down Expand Up @@ -268,10 +278,12 @@ func (c *OperatorRekeyCommand) Run(args []string) int {
func (c *OperatorRekeyCommand) init(client *api.Client) int {
// Handle the different API requests
var fn func(*api.RekeyInitRequest) (*api.RekeyStatusResponse, error)
keyTypeRequired := keyTypeUnseal
switch strings.ToLower(strings.TrimSpace(c.flagTarget)) {
case "barrier":
fn = client.Sys().RekeyInit
case "recovery", "hsm":
keyTypeRequired = keyTypeRecovery
fn = client.Sys().RekeyRecoveryKeyInit
default:
c.UI.Error(fmt.Sprintf("Unknown target: %s", c.flagTarget))
Expand All @@ -295,25 +307,25 @@ func (c *OperatorRekeyCommand) init(client *api.Client) int {
if len(c.flagPGPKeys) == 0 {
if Format(c.UI) == "table" {
c.UI.Warn(wrapAtLength(
"WARNING! If you lose the keys after they are returned, there is no " +
"recovery. Consider canceling this operation and re-initializing " +
"with the -pgp-keys flag to protect the returned unseal keys along " +
"with -backup to allow recovery of the encrypted keys in case of " +
"emergency. You can delete the stored keys later using the -delete " +
"flag."))
fmt.Sprintf("WARNING! If you lose the keys after they are returned, there is no "+
"recovery. Consider canceling this operation and re-initializing "+
"with the -pgp-keys flag to protect the returned %s keys along "+
"with -backup to allow recovery of the encrypted keys in case of "+
"emergency. You can delete the stored keys later using the -delete "+
"flag.", strings.ToLower(keyTypeRequired))))
c.UI.Output("")
}
}
if len(c.flagPGPKeys) > 0 && !c.flagBackup {
if Format(c.UI) == "table" {
c.UI.Warn(wrapAtLength(
"WARNING! You are using PGP keys for encrypted the resulting unseal " +
"keys, but you did not enable the option to backup the keys to " +
"Vault's core. If you lose the encrypted keys after they are " +
"returned, you will not be able to recover them. Consider canceling " +
"this operation and re-running with -backup to allow recovery of the " +
"encrypted unseal keys in case of emergency. You can delete the " +
"stored keys later using the -delete flag."))
fmt.Sprintf("WARNING! You are using PGP keys for encrypted the resulting %s "+
"keys, but you did not enable the option to backup the keys to "+
"Vault's core. If you lose the encrypted keys after they are "+
"returned, you will not be able to recover them. Consider canceling "+
"this operation and re-running with -backup to allow recovery of the "+
"encrypted unseal keys in case of emergency. You can delete the "+
"stored keys later using the -delete flag.", strings.ToLower(keyTypeRequired))))
c.UI.Output("")
}
}
Expand Down Expand Up @@ -358,7 +370,7 @@ func (c *OperatorRekeyCommand) cancel(client *api.Client) int {
func (c *OperatorRekeyCommand) provide(client *api.Client, key string) int {
var statusFn func() (interface{}, error)
var updateFn func(string, string) (interface{}, error)

keyTypeRequired := keyTypeUnseal
switch strings.ToLower(strings.TrimSpace(c.flagTarget)) {
case "barrier":
statusFn = func() (interface{}, error) {
Expand All @@ -376,6 +388,7 @@ func (c *OperatorRekeyCommand) provide(client *api.Client, key string) int {
}
}
case "recovery", "hsm":
keyTypeRequired = keyTypeRecovery
statusFn = func() (interface{}, error) {
return client.Sys().RekeyRecoveryKeyStatus()
}
Expand Down Expand Up @@ -448,7 +461,7 @@ func (c *OperatorRekeyCommand) provide(client *api.Client, key string) int {
// Nonce value is not required if we are prompting via the terminal
w := getWriterFromUI(c.UI)
fmt.Fprintf(w, "Rekey operation nonce: %s\n", nonce)
fmt.Fprintf(w, "Unseal Key (will be hidden): ")
fmt.Fprintf(w, "%s Key (will be hidden): ", keyTypeRequired)
key, err = password.Read(os.Stdin)
fmt.Fprintf(w, "\n")
if err != nil {
Expand All @@ -458,11 +471,11 @@ func (c *OperatorRekeyCommand) provide(client *api.Client, key string) int {
}

c.UI.Error(wrapAtLength(fmt.Sprintf("An error occurred attempting to "+
"ask for the unseal key. The raw error message is shown below, but "+
"ask for the %s key. The raw error message is shown below, but "+
"usually this is because you attempted to pipe a value into the "+
"command or you are executing outside of a terminal (tty). If you "+
"want to pipe the value, pass \"-\" as the argument to read from "+
"stdin. The raw error was: %s", err)))
"stdin. The raw error was: %s", strings.ToLower(keyTypeRequired), err)))
return 1
}
default: // Supplied directly as an arg
Expand Down Expand Up @@ -697,7 +710,7 @@ func (c *OperatorRekeyCommand) printUnsealKeys(client *api.Client, status *api.R
)))
case "recovery", "hsm":
c.UI.Output(wrapAtLength(fmt.Sprintf(
"The encrypted unseal keys are backed up to \"core/recovery-keys-backup\" " +
"The encrypted recovery keys are backed up to \"core/recovery-keys-backup\" " +
"in the storage backend. Remove these keys at any time using " +
"\"vault operator rekey -backup-delete -target=recovery\". Vault does not automatically " +
"remove these keys.",
Expand All @@ -708,33 +721,56 @@ func (c *OperatorRekeyCommand) printUnsealKeys(client *api.Client, status *api.R
switch status.VerificationRequired {
case false:
c.UI.Output("")
c.UI.Output(wrapAtLength(fmt.Sprintf(
"Vault rekeyed with %d key shares and a key threshold of %d. Please "+
"securely distribute the key shares printed above. When Vault is "+
"re-sealed, restarted, or stopped, you must supply at least %d of "+
"these keys to unseal it before it can start servicing requests.",
status.N,
status.T,
status.T)))
switch strings.ToLower(strings.TrimSpace(c.flagTarget)) {
case "barrier":
c.UI.Output(wrapAtLength(fmt.Sprintf(
"Vault rekeyed with %d key shares and a key threshold of %d. Please "+
"securely distribute the key shares printed above. When Vault is "+
"re-sealed, restarted, or stopped, you must supply at least %d of "+
"these keys to unseal it before it can start servicing requests.",
status.N,
status.T,
status.T)))
case "recovery", "hsm":
c.UI.Output(wrapAtLength(fmt.Sprintf(
"Vault rekeyed with %d key shares and a key threshold of %d. Please "+
"securely distribute the key shares printed above.",
status.N,
status.T)))
}

default:
c.UI.Output("")
c.UI.Output(wrapAtLength(fmt.Sprintf(
"Vault has created a new key, split into %d key shares and a key threshold "+
"of %d. These will not be active until after verification is complete. "+
"Please securely distribute the key shares printed above. When Vault "+
"is re-sealed, restarted, or stopped, you must supply at least %d of "+
"these keys to unseal it before it can start servicing requests.",
status.N,
status.T,
status.T)))
var warningText string
switch strings.ToLower(strings.TrimSpace(c.flagTarget)) {
case "barrier":
c.UI.Output(wrapAtLength(fmt.Sprintf(
"Vault has created a new key, split into %d key shares and a key threshold "+
"of %d. These will not be active until after verification is complete. "+
"Please securely distribute the key shares printed above. When Vault "+
"is re-sealed, restarted, or stopped, you must supply at least %d of "+
"these keys to unseal it before it can start servicing requests.",
status.N,
status.T,
status.T)))
warningText = "unseal"
case "recovery", "hsm":
c.UI.Output(wrapAtLength(fmt.Sprintf(
"Vault has created a new key, split into %d key shares and a key threshold "+
"of %d. These will not be active until after verification is complete. "+
"Please securely distribute the key shares printed above.",
status.N,
status.T)))
warningText = "authenticate with"

}
c.UI.Output("")
c.UI.Warn(wrapAtLength(
"Again, these key shares are _not_ valid until verification is performed. " +
"Do not lose or discard your current key shares until after verification " +
"is complete or you will be unable to unseal Vault. If you cancel the " +
"rekey process or seal Vault before verification is complete the new " +
"shares will be discarded and the current shares will remain valid.",
))
c.UI.Warn(wrapAtLength(fmt.Sprintf(
"Again, these key shares are _not_ valid until verification is performed. "+
"Do not lose or discard your current key shares until after verification "+
"is complete or you will be unable to %s Vault. If you cancel the "+
"rekey process or seal Vault before verification is complete the new "+
"shares will be discarded and the current shares will remain valid.", warningText)))
c.UI.Output("")
c.UI.Warn(wrapAtLength(
"The current verification status, including initial nonce, is shown below.",
Expand Down
Loading

0 comments on commit 9ffd602

Please sign in to comment.