From 80c3d4f31967eb07fcd8683214c4529deb02ddfc Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:05:27 -0400 Subject: [PATCH] update unseal command --- command/unseal.go | 230 +++++++++++++++++++++++------------------ command/unseal_test.go | 172 ++++++++++++++++++++---------- 2 files changed, 251 insertions(+), 151 deletions(-) diff --git a/command/unseal.go b/command/unseal.go index 2dfb9476de..495bf6e6c1 100644 --- a/command/unseal.go +++ b/command/unseal.go @@ -2,98 +2,27 @@ package command import ( "fmt" + "io" "os" "strings" + "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/helper/password" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*UnsealCommand)(nil) +var _ cli.CommandAutocomplete = (*UnsealCommand)(nil) + // UnsealCommand is a Command that unseals the vault. type UnsealCommand struct { - meta.Meta + *BaseCommand - // Key can be used to pre-seed the key. If it is set, it will not - // be asked with the `password` helper. - Key string -} + flagReset bool -func (c *UnsealCommand) Run(args []string) int { - var reset bool - flags := c.Meta.FlagSet("unseal", meta.FlagSetDefault) - flags.BoolVar(&reset, "reset", false, "") - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { - return 1 - } - - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) - return 2 - } - - sealStatus, err := client.Sys().SealStatus() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error checking seal status: %s", err)) - return 2 - } - - if !sealStatus.Sealed { - c.Ui.Output("Vault is already unsealed.") - return 0 - } - - args = flags.Args() - if reset { - sealStatus, err = client.Sys().ResetUnsealProcess() - } else { - value := c.Key - if len(args) > 0 { - value = args[0] - } - if value == "" { - fmt.Printf("Key (will be hidden): ") - value, err = password.Read(os.Stdin) - fmt.Printf("\n") - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error attempting to ask for password. The raw error message\n"+ - "is shown below, but the most common reason for this error is\n"+ - "that you attempted to pipe a value into unseal or you're\n"+ - "executing `vault unseal` from outside of a terminal.\n\n"+ - "You should use `vault unseal` from a terminal for maximum\n"+ - "security. If this isn't an option, the unseal key can be passed\n"+ - "in using the first parameter.\n\n"+ - "Raw error: %s", err)) - return 1 - } - } - sealStatus, err = client.Sys().Unseal(strings.TrimSpace(value)) - } - - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error: %s", err)) - return 1 - } - - c.Ui.Output(fmt.Sprintf( - "Sealed: %v\n"+ - "Key Shares: %d\n"+ - "Key Threshold: %d\n"+ - "Unseal Progress: %d\n"+ - "Unseal Nonce: %v", - sealStatus.Sealed, - sealStatus.N, - sealStatus.T, - sealStatus.Progress, - sealStatus.Nonce, - )) - - return 0 + testOutput io.Writer // for tests } func (c *UnsealCommand) Synopsis() string { @@ -102,27 +31,132 @@ func (c *UnsealCommand) Synopsis() string { func (c *UnsealCommand) Help() string { helpText := ` -Usage: vault unseal [options] [key] +Usage: vault unseal [options] [KEY] - Unseal the vault by entering a portion of the master key. Once all - portions are entered, the vault will be unsealed. + Provide a portion of the master key to unseal a Vault server. Vault starts + in a sealed state. It cannot perform operations until it is unsealed. This + command accepts a portion of the master key (an "unseal key"). - Every Vault server initially starts as sealed. It cannot perform any - operation except unsealing until it is sealed. Secrets cannot be accessed - in any way until the vault is unsealed. This command allows you to enter - a portion of the master key to unseal the vault. + The unseal key can be supplied as an argument to the command, but this is + not recommended as the unseal key will be available in your history: - The unseal key can be specified via the command line, but this is - not recommended. The key may then live in your terminal history. This - only exists to assist in scripting. + $ vault unseal IXyR0OJnSFobekZMMCKCoVEpT7wI6l+USMzE3IcyDyo= -General Options: -` + meta.GeneralOptionsUsage() + ` -Unseal Options: + Instead, run the command with no arguments and it will prompt for the key: - -reset Reset the unsealing process by throwing away - prior keys in process to unseal the vault. + $ vault unseal + Key (will be hidden): IXyR0OJnSFobekZMMCKCoVEpT7wI6l+USMzE3IcyDyo= + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() -` return strings.TrimSpace(helpText) } + +func (c *UnsealCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP) + + f := set.NewFlagSet("Command Options") + + f.BoolVar(&BoolVar{ + Name: "reset", + Aliases: []string{}, + Target: &c.flagReset, + Default: false, + EnvVar: "", + Completion: complete.PredictNothing, + Usage: "Discard any previously entered keys to the unseal process.", + }) + + return set +} + +func (c *UnsealCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultFiles() +} + +func (c *UnsealCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *UnsealCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + unsealKey := "" + + args = f.Args() + switch len(args) { + case 0: + // We will prompt for the unsealKey later + case 1: + unsealKey = strings.TrimSpace(args[0]) + default: + c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + if c.flagReset { + status, err := client.Sys().ResetUnsealProcess() + if err != nil { + c.UI.Error(fmt.Sprintf("Error resetting unseal process: %s", err)) + return 2 + } + c.prettySealStatus(status) + return 0 + } + + if unsealKey == "" { + // Override the output + writer := (io.Writer)(os.Stdout) + if c.testOutput != nil { + writer = c.testOutput + } + + fmt.Fprintf(writer, "Key (will be hidden): ") + value, err := password.Read(os.Stdin) + fmt.Fprintf(writer, "\n") + if err != nil { + c.UI.Error(wrapAtLength(fmt.Sprintf("An error occurred attempting to "+ + "ask for an unseal key. The raw error message is shown below, but "+ + "usually this is because you attempted to pipe a value into the "+ + "unseal command or you are executing outside of a terminal (tty). "+ + "You should run the unseal command from a terminal for maximum "+ + "security. If this is not an option, the unseal can be provided as "+ + "the first argument to the unseal command. The raw error "+ + "was:\n\n%s", err))) + return 1 + } + unsealKey = strings.TrimSpace(value) + } + + status, err := client.Sys().Unseal(unsealKey) + if err != nil { + c.UI.Error(fmt.Sprintf("Error unsealing: %s", err)) + return 2 + } + + c.prettySealStatus(status) + return 0 +} + +func (c *UnsealCommand) prettySealStatus(status *api.SealStatusResponse) { + c.UI.Output(fmt.Sprintf("Sealed: %t", status.Sealed)) + c.UI.Output(fmt.Sprintf("Key Shares: %d", status.N)) + c.UI.Output(fmt.Sprintf("Key Threshold: %d", status.T)) + c.UI.Output(fmt.Sprintf("Unseal Progress: %d", status.Progress)) + if status.Nonce != "" { + c.UI.Output(fmt.Sprintf("Unseal Nonce: %s", status.Nonce)) + } +} diff --git a/command/unseal_test.go b/command/unseal_test.go index 699fdd8fb7..bc8d2e8d22 100644 --- a/command/unseal_test.go +++ b/command/unseal_test.go @@ -1,72 +1,138 @@ package command import ( - "encoding/hex" + "fmt" + "io/ioutil" + "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestUnseal(t *testing.T) { - core := vault.TestCore(t) - keys, _ := vault.TestCoreInit(t, core) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testUnsealCommand(tb testing.TB) (*cli.MockUi, *UnsealCommand) { + tb.Helper() - ui := new(cli.MockUi) - - for _, key := range keys { - c := &UnsealCommand{ - Key: hex.EncodeToString(key), - Meta: meta.Meta{ - Ui: ui, - }, - } - - args := []string{"-address", addr} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - } - - sealed, err := core.Sealed() - if err != nil { - t.Fatalf("err: %s", err) - } - if sealed { - t.Fatal("should not be sealed") + ui := cli.NewMockUi() + return ui, &UnsealCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, } } -func TestUnseal_arg(t *testing.T) { - core := vault.TestCore(t) - keys, _ := vault.TestCoreInit(t, core) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func TestUnsealCommand_Run(t *testing.T) { + t.Parallel() - ui := new(cli.MockUi) + t.Run("error_non_terminal", func(t *testing.T) { + t.Parallel() - for _, key := range keys { - c := &UnsealCommand{ - Meta: meta.Meta{ - Ui: ui, - }, + ui, cmd := testUnsealCommand(t) + cmd.testOutput = ioutil.Discard + + code := cmd.Run(nil) + if exp := 1; code != exp { + t.Errorf("expected %d to be %d", code, exp) } - args := []string{"-address", addr, hex.EncodeToString(key)} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + expected := "is not a terminal" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) } - } + }) - sealed, err := core.Sealed() - if err != nil { - t.Fatalf("err: %s", err) - } - if sealed { - t.Fatal("should not be sealed") - } + t.Run("reset", func(t *testing.T) { + t.Parallel() + + client, keys, closer := testVaultServerUnseal(t) + defer closer() + + // Seal so we can unseal + if err := client.Sys().Seal(); err != nil { + t.Fatal(err) + } + + // Enter an unseal key + if _, err := client.Sys().Unseal(keys[0]); err != nil { + t.Fatal(err) + } + + ui, cmd := testUnsealCommand(t) + cmd.client = client + cmd.testOutput = ioutil.Discard + + // Reset and check output + code := cmd.Run([]string{ + "-reset", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + expected := "Unseal Progress: 0" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("full", func(t *testing.T) { + t.Parallel() + + client, keys, closer := testVaultServerUnseal(t) + defer closer() + + // Seal so we can unseal + if err := client.Sys().Seal(); err != nil { + t.Fatal(err) + } + + for i, key := range keys { + ui, cmd := testUnsealCommand(t) + cmd.client = client + cmd.testOutput = ioutil.Discard + + // Reset and check output + code := cmd.Run([]string{ + key, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + expected := fmt.Sprintf("Unseal Progress: %d", (i+1)%3) // 1, 2, 0 + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testUnsealCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "abcd", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error unsealing: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testUnsealCommand(t) + assertNoTabs(t, cmd) + }) }