Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

VAULT-12833 Update prompts for the rekey command #18892

Merged
merged 2 commits into from
Jan 30, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/18892.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
cli: updated `vault operator rekey` prompts to describe recovery keys when `-target=recovery`
```
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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if there's a better way to set up auto-unseal for testing

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depends on what you're trying to do. I think you made the right choice here. If we were actually exercising seal related more directly, I might point you at something that doesn't use a NewTestSeal, which is kind of a fake.

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() }
miagilepner marked this conversation as resolved.
Show resolved Hide resolved
}

// 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
miagilepner marked this conversation as resolved.
Show resolved Hide resolved
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.
miagilepner marked this conversation as resolved.
Show resolved Hide resolved

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 "+
miagilepner marked this conversation as resolved.
Show resolved Hide resolved
"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 "+
miagilepner marked this conversation as resolved.
Show resolved Hide resolved
"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