VAULT-12833 Update prompts for the rekey command (#18892)

* update prompts for rekey command

* cleanup additional places with unseal/recovery keys
This commit is contained in:
miagilepner 2023-01-30 17:51:01 +01:00 committed by GitHub
parent b9bbc82078
commit 5d7a8aac2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 289 additions and 61 deletions

3
changelog/18892.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
cli: updated `vault operator rekey` prompts to describe recovery keys when `-target=recovery`
```

View File

@ -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"
@ -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
}
@ -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,
@ -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,
@ -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()
@ -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), 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.

View File

@ -20,6 +20,11 @@ var (
_ cli.CommandAutocomplete = (*OperatorRekeyCommand)(nil)
)
const (
keyTypeRecovery = "Recovery"
keyTypeUnseal = "Unseal"
)
type OperatorRekeyCommand struct {
*BaseCommand
@ -58,6 +63,9 @@ 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 require a
quorum of recovery keys in order to generate a new set of recovery keys.
Initialize a rekey:
$ vault operator rekey \
@ -112,7 +120,7 @@ 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{
@ -120,7 +128,7 @@ func (c *OperatorRekeyCommand) Flags() *FlagSets {
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{
@ -130,7 +138,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{
@ -150,7 +158,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{
@ -179,7 +187,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.",
})
@ -189,25 +197,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.",
})
@ -268,10 +276,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))
@ -295,25 +305,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("")
}
}
@ -358,7 +368,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) {
@ -376,6 +386,7 @@ func (c *OperatorRekeyCommand) provide(client *api.Client, key string) int {
}
}
case "recovery", "hsm":
keyTypeRequired = keyTypeRecovery
statusFn = func() (interface{}, error) {
return client.Sys().RekeyRecoveryKeyStatus()
}
@ -448,7 +459,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 {
@ -458,11 +469,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
@ -697,7 +708,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.",
@ -708,33 +719,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 unseal keys 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 recovery keys 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 unseal 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 recovery 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.",

View File

@ -9,6 +9,8 @@ import (
"strings"
"testing"
"github.com/hashicorp/vault/sdk/helper/roottoken"
"github.com/hashicorp/vault/api"
"github.com/mitchellh/cli"
)
@ -254,6 +256,83 @@ func TestOperatorRekeyCommand_Run(t *testing.T) {
}
})
t.Run("provide_arg_recovery_keys", func(t *testing.T) {
t.Parallel()
client, keys, closer := testVaultServerAutoUnseal(t)
defer closer()
// Initialize a rekey
status, err := client.Sys().RekeyRecoveryKeyInit(&api.RekeyInitRequest{
SecretShares: 1,
SecretThreshold: 1,
})
if err != nil {
t.Fatal(err)
}
nonce := status.Nonce
// Supply the first n-1 recovery keys
for _, key := range keys[:len(keys)-1] {
ui, cmd := testOperatorRekeyCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-nonce", nonce,
"-target", "recovery",
key,
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
}
ui, cmd := testOperatorRekeyCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-nonce", nonce,
"-target", "recovery",
keys[len(keys)-1], // the last recovery key
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
re := regexp.MustCompile(`Key 1: (.+)`)
output := ui.OutputWriter.String()
match := re.FindAllStringSubmatch(output, -1)
if len(match) < 1 || len(match[0]) < 2 {
t.Fatalf("bad match: %#v", match)
}
recoveryKey := match[0][1]
if strings.Contains(strings.ToLower(output), "unseal key") {
t.Fatalf(`output %s shouldn't contain "unseal key"`, output)
}
// verify that we can perform operations with the recovery key
// below we generate a root token using the recovery key
rootStatus, err := client.Sys().GenerateRootStatus()
if err != nil {
t.Fatal(err)
}
otp, err := roottoken.GenerateOTP(rootStatus.OTPLength)
if err != nil {
t.Fatal(err)
}
genRoot, err := client.Sys().GenerateRootInit(otp, "")
if err != nil {
t.Fatal(err)
}
r, err := client.Sys().GenerateRootUpdate(recoveryKey, genRoot.Nonce)
if err != nil {
t.Fatal(err)
}
if !r.Complete {
t.Fatal("expected root update to be complete")
}
})
t.Run("provide_arg", func(t *testing.T) {
t.Parallel()
@ -392,6 +471,94 @@ func TestOperatorRekeyCommand_Run(t *testing.T) {
}
})
t.Run("provide_stdin_recovery_keys", func(t *testing.T) {
t.Parallel()
client, keys, closer := testVaultServerAutoUnseal(t)
defer closer()
// Initialize a rekey
status, err := client.Sys().RekeyRecoveryKeyInit(&api.RekeyInitRequest{
SecretShares: 1,
SecretThreshold: 1,
})
if err != nil {
t.Fatal(err)
}
nonce := status.Nonce
for _, key := range keys[:len(keys)-1] {
stdinR, stdinW := io.Pipe()
go func() {
_, _ = stdinW.Write([]byte(key))
_ = stdinW.Close()
}()
ui, cmd := testOperatorRekeyCommand(t)
cmd.client = client
cmd.testStdin = stdinR
code := cmd.Run([]string{
"-target", "recovery",
"-nonce", nonce,
"-",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
}
stdinR, stdinW := io.Pipe()
go func() {
_, _ = stdinW.Write([]byte(keys[len(keys)-1])) // the last recovery key
_ = stdinW.Close()
}()
ui, cmd := testOperatorRekeyCommand(t)
cmd.client = client
cmd.testStdin = stdinR
code := cmd.Run([]string{
"-nonce", nonce,
"-target", "recovery",
"-",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
re := regexp.MustCompile(`Key 1: (.+)`)
output := ui.OutputWriter.String()
match := re.FindAllStringSubmatch(output, -1)
if len(match) < 1 || len(match[0]) < 2 {
t.Fatalf("bad match: %#v", match)
}
recoveryKey := match[0][1]
if strings.Contains(strings.ToLower(output), "unseal key") {
t.Fatalf(`output %s shouldn't contain "unseal key"`, output)
}
// verify that we can perform operations with the recovery key
// below we generate a root token using the recovery key
rootStatus, err := client.Sys().GenerateRootStatus()
if err != nil {
t.Fatal(err)
}
otp, err := roottoken.GenerateOTP(rootStatus.OTPLength)
if err != nil {
t.Fatal(err)
}
genRoot, err := client.Sys().GenerateRootInit(otp, "")
if err != nil {
t.Fatal(err)
}
r, err := client.Sys().GenerateRootUpdate(recoveryKey, genRoot.Nonce)
if err != nil {
t.Fatal(err)
}
if !r.Complete {
t.Fatal("expected root update to be complete")
}
})
t.Run("backup", func(t *testing.T) {
t.Parallel()