From d7f528a768d08ee3b99381276bbd6bb8d43c6bf6 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Wed, 28 Oct 2015 15:59:39 -0400 Subject: [PATCH] Add reset support to the unseal command. Reset clears the provided unseal keys, allowing the process to be begun again. Includes documentation and unit test changes. Fixes #695 --- CHANGELOG.md | 2 + api/sys_seal.go | 25 +++++---- command/unseal.go | 55 ++++++++++--------- http/sys_seal.go | 48 +++++++++++------ http/sys_seal_test.go | 59 +++++++++++++++++++++ vault/core.go | 11 ++++ website/source/docs/http/sys-unseal.html.md | 12 ++++- 7 files changed, 158 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9cd5d175c..e16cca0984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ IMPROVEMENTS: * api: API client now uses a 30 second timeout instead of indefinite [GH-681] * core: The physical storage read cache can now be disabled via "disable_cache" [GH-674] + * core: The unsealing process can now be reset midway through (this feature + was documented before, but not enabled) [GH-695] * core: Tokens can now renew themselves [GH-455] * core: Base64-encoded PGP keys can be used with the CLI for `init` and `rekey` operations [GH-653] diff --git a/api/sys_seal.go b/api/sys_seal.go index cfff0f6192..2a5ad4be28 100644 --- a/api/sys_seal.go +++ b/api/sys_seal.go @@ -2,15 +2,7 @@ package api func (c *Sys) SealStatus() (*SealStatusResponse, error) { r := c.c.NewRequest("GET", "/v1/sys/seal-status") - resp, err := c.c.RawRequest(r) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - var result SealStatusResponse - err = resp.DecodeJSON(&result) - return &result, err + return sealStatusRequest(c, r) } func (c *Sys) Seal() error { @@ -22,6 +14,17 @@ func (c *Sys) Seal() error { return err } +func (c *Sys) ResetUnsealProcess() (*SealStatusResponse, error) { + body := map[string]interface{}{"reset": true} + + r := c.c.NewRequest("PUT", "/v1/sys/unseal") + if err := r.SetJSONBody(body); err != nil { + return nil, err + } + + return sealStatusRequest(c, r) +} + func (c *Sys) Unseal(shard string) (*SealStatusResponse, error) { body := map[string]interface{}{"key": shard} @@ -30,6 +33,10 @@ func (c *Sys) Unseal(shard string) (*SealStatusResponse, error) { return nil, err } + return sealStatusRequest(c, r) +} + +func sealStatusRequest(c *Sys, r *Request) (*SealStatusResponse, error) { resp, err := c.c.RawRequest(r) if err != nil { return nil, err diff --git a/command/unseal.go b/command/unseal.go index c7eee8c694..f484f121b7 100644 --- a/command/unseal.go +++ b/command/unseal.go @@ -46,33 +46,36 @@ func (c *UnsealCommand) Run(args []string) int { } args = flags.Args() - - 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 + 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)) } - status, err := client.Sys().Unseal(strings.TrimSpace(value)) if err != nil { c.Ui.Error(fmt.Sprintf( - "Error attempting unseal: %s", err)) + "Error: %s", err)) return 1 } @@ -81,10 +84,10 @@ func (c *UnsealCommand) Run(args []string) int { "Key Shares: %d\n"+ "Key Threshold: %d\n"+ "Unseal Progress: %d", - status.Sealed, - status.N, - status.T, - status.Progress, + sealStatus.Sealed, + sealStatus.N, + sealStatus.T, + sealStatus.Progress, )) return 0 diff --git a/http/sys_seal.go b/http/sys_seal.go index f763087f40..d5ac76624f 100644 --- a/http/sys_seal.go +++ b/http/sys_seal.go @@ -50,30 +50,43 @@ func handleSysUnseal(core *vault.Core) http.Handler { respondError(w, http.StatusBadRequest, err) return } - if req.Key == "" { + if !req.Reset && req.Key == "" { respondError( w, http.StatusBadRequest, - errors.New("'key' must specified in request body as JSON")) + errors.New("'key' must specified in request body as JSON, or 'reset' set to true")) return } - // Decode the key, which is hex encoded - key, err := hex.DecodeString(req.Key) - if err != nil { - respondError( - w, http.StatusBadRequest, - errors.New("'key' must be a valid hex-string")) - return - } - - // Attempt the unseal - if _, err := core.Unseal(key); err != nil { - // Ignore ErrInvalidKey because its a user error that we - // mask away. We just show them the seal status. - if !errwrap.ContainsType(err, new(vault.ErrInvalidKey)) { + if req.Reset { + sealed, err := core.Sealed() + if err != nil { respondError(w, http.StatusInternalServerError, err) return } + if !sealed { + respondError(w, http.StatusBadRequest, errors.New("vault is unsealed")) + return + } + core.ResetUnsealProcess() + } else { + // Decode the key, which is hex encoded + key, err := hex.DecodeString(req.Key) + if err != nil { + respondError( + w, http.StatusBadRequest, + errors.New("'key' must be a valid hex-string")) + return + } + + // Attempt the unseal + if _, err := core.Unseal(key); err != nil { + // Ignore ErrInvalidKey because its a user error that we + // mask away. We just show them the seal status. + if !errwrap.ContainsType(err, new(vault.ErrInvalidKey)) { + respondError(w, http.StatusInternalServerError, err) + return + } + } } // Return the seal status @@ -126,5 +139,6 @@ type SealStatusResponse struct { } type UnsealRequest struct { - Key string + Key string + Reset bool } diff --git a/http/sys_seal_test.go b/http/sys_seal_test.go index d403ae1e70..676eeba1eb 100644 --- a/http/sys_seal_test.go +++ b/http/sys_seal_test.go @@ -129,3 +129,62 @@ func TestSysUnseal_badKey(t *testing.T) { t.Fatalf("bad: %#v", actual) } } + +func TestSysUnseal_Reset(t *testing.T) { + core := vault.TestCore(t) + ln, addr := TestServer(t, core) + defer ln.Close() + + thresh := 3 + resp := testHttpPut(t, "", addr+"/v1/sys/init", map[string]interface{}{ + "secret_shares": 5, + "secret_threshold": thresh, + }) + + var actual map[string]interface{} + testResponseStatus(t, resp, 200) + testResponseBody(t, resp, &actual) + keysRaw, ok := actual["keys"] + if !ok { + t.Fatalf("no keys: %#v", actual) + } + for i, key := range keysRaw.([]interface{}) { + if i > thresh-2 { + break + } + + resp := testHttpPut(t, "", addr+"/v1/sys/unseal", map[string]interface{}{ + "key": key.(string), + }) + + var actual map[string]interface{} + expected := map[string]interface{}{ + "sealed": true, + "t": float64(3), + "n": float64(5), + "progress": float64(i + 1), + } + testResponseStatus(t, resp, 200) + testResponseBody(t, resp, &actual) + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("\nexpected:\n%#v\nactual:\n%#v\n", expected, actual) + } + } + + resp = testHttpPut(t, "", addr+"/v1/sys/unseal", map[string]interface{}{ + "reset": true, + }) + + actual = map[string]interface{}{} + expected := map[string]interface{}{ + "sealed": true, + "t": float64(3), + "n": float64(5), + "progress": float64(0), + } + testResponseStatus(t, resp, 200) + testResponseBody(t, resp, &actual) + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("\nexpected:\n%#v\nactual:\n%#v\n", expected, actual) + } +} diff --git a/vault/core.go b/vault/core.go index f045ced3a6..47310a34ec 100644 --- a/vault/core.go +++ b/vault/core.go @@ -957,6 +957,17 @@ func (c *Core) SecretProgress() int { return len(c.unlockParts) } +// ResetUnsealProcess removes the current unlock parts from memory, to reset +// the unsealing process +func (c *Core) ResetUnsealProcess() { + c.stateLock.Lock() + defer c.stateLock.Unlock() + if !c.sealed { + return + } + c.unlockParts = nil +} + // Unseal is used to provide one of the key parts to unseal the Vault. // // They key given as a parameter will automatically be zerod after diff --git a/website/source/docs/http/sys-unseal.html.md b/website/source/docs/http/sys-unseal.html.md index 699db46148..94f21a644c 100644 --- a/website/source/docs/http/sys-unseal.html.md +++ b/website/source/docs/http/sys-unseal.html.md @@ -14,7 +14,9 @@ description: |- Enter a single master key share to progress the unsealing of the Vault. If the threshold number of master key shares is reached, Vault will attempt to unseal the Vault. Otherwise, this API must be - called multiple times until that threshold is met. + called multiple times until that threshold is met.

Either + the `key` or `reset` parameter must be provided; if both are provided, + `reset` takes precedence.
Method
@@ -25,9 +27,15 @@ description: |-
Returns