From 60deff1bad6ad4d0cd0daed9d65c2cf18c4b616d Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Wed, 28 Sep 2016 21:01:28 -0700 Subject: [PATCH] Wrapping enhancements (#1927) --- api/client.go | 20 +- api/logical.go | 57 ++- command/rekey_test.go | 5 +- command/unwrap.go | 26 +- command/util.go | 3 +- http/handler.go | 39 +- http/logical.go | 71 ++-- http/sys_wrapping_test.go | 350 ++++++++++++++++++ meta/meta.go | 3 +- vault/core.go | 26 +- vault/core_test.go | 5 +- vault/logical_system.go | 314 +++++++++++++++- vault/logical_system_test.go | 23 +- vault/policy_store.go | 75 ++-- vault/policy_store_test.go | 4 +- vault/request_handling.go | 119 ++++-- vault/router.go | 18 +- vault/testing.go | 2 - vault/token_store.go | 11 +- vault/token_store_test.go | 2 +- .../docs/http/sys-wrapping-lookup.html.md | 54 +++ .../docs/http/sys-wrapping-rewrap.html.md | 60 +++ .../docs/http/sys-wrapping-unwrap.html.md | 56 +++ .../docs/http/sys-wrapping-wrap.html.md | 59 +++ website/source/layouts/http.erb | 18 + 25 files changed, 1277 insertions(+), 143 deletions(-) create mode 100644 http/sys_wrapping_test.go create mode 100644 website/source/docs/http/sys-wrapping-lookup.html.md create mode 100644 website/source/docs/http/sys-wrapping-rewrap.html.md create mode 100644 website/source/docs/http/sys-wrapping-unwrap.html.md create mode 100644 website/source/docs/http/sys-wrapping-wrap.html.md diff --git a/api/client.go b/api/client.go index e548eb9646..4aee40c0a7 100644 --- a/api/client.go +++ b/api/client.go @@ -327,17 +327,19 @@ func (c *Client) NewRequest(method, path string) *Request { Params: make(map[string][]string), } + var lookupPath string + switch { + case strings.HasPrefix(path, "/v1/"): + lookupPath = strings.TrimPrefix(path, "/v1/") + case strings.HasPrefix(path, "v1/"): + lookupPath = strings.TrimPrefix(path, "v1/") + default: + lookupPath = path + } if c.wrappingLookupFunc != nil { - var lookupPath string - switch { - case strings.HasPrefix(path, "/v1/"): - lookupPath = strings.TrimPrefix(path, "/v1/") - case strings.HasPrefix(path, "v1/"): - lookupPath = strings.TrimPrefix(path, "v1/") - default: - lookupPath = path - } req.WrapTTL = c.wrappingLookupFunc(method, lookupPath) + } else { + req.WrapTTL = DefaultWrappingLookupFunc(method, lookupPath) } return req diff --git a/api/logical.go b/api/logical.go index e6ff875ce8..f1cea7ddf7 100644 --- a/api/logical.go +++ b/api/logical.go @@ -3,6 +3,8 @@ package api import ( "bytes" "fmt" + "net/http" + "os" "github.com/hashicorp/vault/helper/jsonutil" ) @@ -11,6 +13,26 @@ const ( wrappedResponseLocation = "cubbyhole/response" ) +var ( + // The default TTL that will be used with `sys/wrapping/wrap`, can be + // changed + DefaultWrappingTTL = "5m" + + // The default function used if no other function is set, which honors the + // env var and wraps `sys/wrapping/wrap` + DefaultWrappingLookupFunc = func(operation, path string) string { + if os.Getenv(EnvVaultWrapTTL) != "" { + return os.Getenv(EnvVaultWrapTTL) + } + + if (operation == "PUT" || operation == "POST") && path == "sys/wrapping/wrap" { + return DefaultWrappingTTL + } + + return "" + } +) + // Logical is used to perform logical backend operations on Vault. type Logical struct { c *Client @@ -96,10 +118,39 @@ func (c *Logical) Delete(path string) (*Secret, error) { } func (c *Logical) Unwrap(wrappingToken string) (*Secret, error) { - origToken := c.c.Token() - defer c.c.SetToken(origToken) + var data map[string]interface{} + if wrappingToken != "" { + data = map[string]interface{}{ + "token": wrappingToken, + } + } - c.c.SetToken(wrappingToken) + r := c.c.NewRequest("PUT", "/v1/sys/wrapping/unwrap") + if err := r.SetJSONBody(data); err != nil { + return nil, err + } + + resp, err := c.c.RawRequest(r) + if resp != nil { + defer resp.Body.Close() + } + if err != nil && resp.StatusCode != 404 { + return nil, err + } + + switch resp.StatusCode { + case http.StatusOK: // New method is supported + return ParseSecret(resp.Body) + case http.StatusNotFound: // Fall back to old method + default: + return nil, nil + } + + if wrappingToken == "" { + origToken := c.c.Token() + defer c.c.SetToken(origToken) + c.c.SetToken(wrappingToken) + } secret, err := c.Read(wrappedResponseLocation) if err != nil { diff --git a/command/rekey_test.go b/command/rekey_test.go index e3335cb581..f312e4774a 100644 --- a/command/rekey_test.go +++ b/command/rekey_test.go @@ -177,7 +177,10 @@ func TestRekey_init_pgp(t *testing.T) { MaxLeaseTTLVal: time.Hour * 24 * 32, }, } - sysBackend := vault.NewSystemBackend(core, bc) + sysBackend, err := vault.NewSystemBackend(core, bc) + if err != nil { + t.Fatal(err) + } ui := new(cli.MockUi) c := &RekeyCommand{ diff --git a/command/unwrap.go b/command/unwrap.go index e664d98ad0..24e4e53509 100644 --- a/command/unwrap.go +++ b/command/unwrap.go @@ -30,18 +30,22 @@ func (c *UnwrapCommand) Run(args []string) int { return 1 } - args = flags.Args() - if len(args) != 1 || len(args[0]) == 0 { - c.Ui.Error("Unwrap expects one argument: the ID of the wrapping token") - flags.Usage() - return 1 - } + var tokenID string - tokenID := args[0] - _, err = uuid.ParseUUID(tokenID) - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Given token could not be parsed as a UUID: %s", err)) + args = flags.Args() + switch len(args) { + case 0: + case 1: + tokenID = args[0] + _, err = uuid.ParseUUID(tokenID) + if err != nil { + c.Ui.Error(fmt.Sprintf( + "Given token could not be parsed as a UUID: %v", err)) + return 1 + } + default: + c.Ui.Error("Unwrap expects zero or one argument (the ID of the wrapping token)") + flags.Usage() return 1 } diff --git a/command/util.go b/command/util.go index 6b50aa6a05..990416d9f4 100644 --- a/command/util.go +++ b/command/util.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "reflect" + "time" "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/command/token" @@ -55,7 +56,7 @@ func PrintRawField(ui cli.Ui, secret *api.Secret, field string) int { case "wrapping_token_ttl": val = secret.WrapInfo.TTL case "wrapping_token_creation_time": - val = secret.WrapInfo.CreationTime.String() + val = secret.WrapInfo.CreationTime.Format(time.RFC3339Nano) case "wrapped_accessor": val = secret.WrapInfo.WrappedAccessor default: diff --git a/http/handler.go b/http/handler.go index c1388e9f44..4ad99216d5 100644 --- a/http/handler.go +++ b/http/handler.go @@ -48,7 +48,10 @@ func Handler(core *vault.Core) http.Handler { mux.Handle("/v1/sys/rekey/update", handleRequestForwarding(core, handleSysRekeyUpdate(core, false))) mux.Handle("/v1/sys/rekey-recovery-key/init", handleRequestForwarding(core, handleSysRekeyInit(core, true))) mux.Handle("/v1/sys/rekey-recovery-key/update", handleRequestForwarding(core, handleSysRekeyUpdate(core, true))) - mux.Handle("/v1/sys/capabilities-self", handleRequestForwarding(core, handleLogical(core, true, sysCapabilitiesSelfCallback))) + mux.Handle("/v1/sys/wrapping/lookup", handleRequestForwarding(core, handleLogical(core, false, wrappingVerificationFunc))) + mux.Handle("/v1/sys/wrapping/rewrap", handleRequestForwarding(core, handleLogical(core, false, wrappingVerificationFunc))) + mux.Handle("/v1/sys/wrapping/unwrap", handleRequestForwarding(core, handleLogical(core, false, wrappingVerificationFunc))) + mux.Handle("/v1/sys/capabilities-self", handleRequestForwarding(core, handleLogical(core, true, nil))) mux.Handle("/v1/sys/", handleRequestForwarding(core, handleLogical(core, true, nil))) mux.Handle("/v1/", handleRequestForwarding(core, handleLogical(core, false, nil))) @@ -58,15 +61,35 @@ func Handler(core *vault.Core) http.Handler { return handler } -// ClientToken is required in the handler of sys/capabilities-self endpoint in -// system backend. But the ClientToken gets obfuscated before the request gets -// forwarded to any logical backend. So, setting the ClientToken in the data -// field for this request. -func sysCapabilitiesSelfCallback(req *logical.Request) error { - if req == nil || req.Data == nil { +// A lookup on a token that is about to expire returns nil, which means by the +// time we can validate a wrapping token lookup will return nil since it will +// be revoked after the call. So we have to do the validation here. +func wrappingVerificationFunc(core *vault.Core, req *logical.Request) error { + if req == nil { return fmt.Errorf("invalid request") } - req.Data["token"] = req.ClientToken + + var token string + if req.Data != nil && req.Data["token"] != nil { + if tokenStr, ok := req.Data["token"].(string); !ok { + return fmt.Errorf("could not decode token in request body") + } else if tokenStr == "" { + return fmt.Errorf("empty token in request body") + } else { + token = tokenStr + } + } else { + token = req.ClientToken + } + + valid, err := core.ValidateWrappingToken(token) + if err != nil { + return fmt.Errorf("error validating wrapping token: %v", err) + } + if !valid { + return fmt.Errorf("wrapping token is not valid or does not exist") + } + return nil } diff --git a/http/logical.go b/http/logical.go index 1646e7fdef..ed9be41b7c 100644 --- a/http/logical.go +++ b/http/logical.go @@ -14,7 +14,7 @@ import ( "github.com/hashicorp/vault/vault" ) -type PrepareRequestFunc func(req *logical.Request) error +type PrepareRequestFunc func(*vault.Core, *logical.Request) error func buildLogicalRequest(w http.ResponseWriter, r *http.Request) (*logical.Request, int, error) { // Determine the path... @@ -99,8 +99,8 @@ func handleLogical(core *vault.Core, dataOnly bool, prepareRequestCallback Prepa // will have a callback registered to do the needed operations, so // invoke it before proceeding. if prepareRequestCallback != nil { - if err := prepareRequestCallback(req); err != nil { - respondError(w, http.StatusInternalServerError, err) + if err := prepareRequestCallback(core, req); err != nil { + respondError(w, http.StatusBadRequest, err) return } } @@ -160,8 +160,8 @@ func respondLogical(w http.ResponseWriter, r *http.Request, req *logical.Request } // Check if this is a raw response - if _, ok := resp.Data[logical.HTTPContentType]; ok { - respondRaw(w, r, req.Path, resp) + if _, ok := resp.Data[logical.HTTPStatusCode]; ok { + respondRaw(w, r, resp) return } @@ -197,51 +197,68 @@ func respondLogical(w http.ResponseWriter, r *http.Request, req *logical.Request // respondRaw is used when the response is using HTTPContentType and HTTPRawBody // to change the default response handling. This is only used for specific things like // returning the CRL information on the PKI backends. -func respondRaw(w http.ResponseWriter, r *http.Request, path string, resp *logical.Response) { +func respondRaw(w http.ResponseWriter, r *http.Request, resp *logical.Response) { + retErr := func(w http.ResponseWriter, err string) { + w.Header().Set("X-Vault-Raw-Error", err) + w.WriteHeader(http.StatusInternalServerError) + w.Write(nil) + } + // Ensure this is never a secret or auth response if resp.Secret != nil || resp.Auth != nil { - respondError(w, http.StatusInternalServerError, nil) + retErr(w, "raw responses cannot contain secrets or auth") return } // Get the status code statusRaw, ok := resp.Data[logical.HTTPStatusCode] if !ok { - respondError(w, http.StatusInternalServerError, nil) + retErr(w, "no status code given") return } status, ok := statusRaw.(int) if !ok { - respondError(w, http.StatusInternalServerError, nil) + retErr(w, "cannot decode status code") return } - // Get the header + nonEmpty := status != http.StatusNoContent + + var contentType string + var body []byte + + // Get the content type header; don't require it if the body is empty contentTypeRaw, ok := resp.Data[logical.HTTPContentType] - if !ok { - respondError(w, http.StatusInternalServerError, nil) + if !ok && !nonEmpty { + retErr(w, "no content type given") return } - contentType, ok := contentTypeRaw.(string) - if !ok { - respondError(w, http.StatusInternalServerError, nil) - return + if ok { + contentType, ok = contentTypeRaw.(string) + if !ok { + retErr(w, "cannot decode content type") + return + } } - // Get the body - bodyRaw, ok := resp.Data[logical.HTTPRawBody] - if !ok { - respondError(w, http.StatusInternalServerError, nil) - return - } - body, ok := bodyRaw.([]byte) - if !ok { - respondError(w, http.StatusInternalServerError, nil) - return + if nonEmpty { + // Get the body + bodyRaw, ok := resp.Data[logical.HTTPRawBody] + if !ok { + retErr(w, "no body given") + return + } + body, ok = bodyRaw.([]byte) + if !ok { + retErr(w, "cannot decode body") + return + } } // Write the response - w.Header().Set("Content-Type", contentType) + if contentType != "" { + w.Header().Set("Content-Type", contentType) + } w.WriteHeader(status) w.Write(body) } diff --git a/http/sys_wrapping_test.go b/http/sys_wrapping_test.go new file mode 100644 index 0000000000..4bfeac110b --- /dev/null +++ b/http/sys_wrapping_test.go @@ -0,0 +1,350 @@ +package http + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" + "testing" + "time" + + cleanhttp "github.com/hashicorp/go-cleanhttp" + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/helper/jsonutil" + "github.com/hashicorp/vault/vault" +) + +// Test wrapping functionality +func TestHTTP_Wrapping(t *testing.T) { + handler1 := http.NewServeMux() + handler2 := http.NewServeMux() + handler3 := http.NewServeMux() + + coreConfig := &vault.CoreConfig{} + + // Chicken-and-egg: Handler needs a core. So we create handlers first, then + // add routes chained to a Handler-created handler. + cores := vault.TestCluster(t, []http.Handler{handler1, handler2, handler3}, coreConfig, true) + for _, core := range cores { + defer core.CloseListeners() + } + handler1.Handle("/", Handler(cores[0].Core)) + handler2.Handle("/", Handler(cores[1].Core)) + handler3.Handle("/", Handler(cores[2].Core)) + + // make it easy to get access to the active + core := cores[0].Core + vault.TestWaitActive(t, core) + + root := cores[0].Root + + transport := cleanhttp.DefaultTransport() + transport.TLSClientConfig = cores[0].TLSConfig + httpClient := &http.Client{ + Transport: transport, + } + addr := fmt.Sprintf("https://127.0.0.1:%d", cores[0].Listeners[0].Address.Port) + config := api.DefaultConfig() + config.Address = addr + config.HttpClient = httpClient + client, err := api.NewClient(config) + if err != nil { + t.Fatal(err) + } + client.SetToken(root) + + // Write a value that we will use with wrapping for lookup + _, err = client.Logical().Write("secret/foo", map[string]interface{}{ + "zip": "zap", + }) + if err != nil { + t.Fatal(err) + } + + // Set a wrapping lookup function for reads on that path + client.SetWrappingLookupFunc(func(operation, path string) string { + if operation == "GET" && path == "secret/foo" { + return "5m" + } + + return api.DefaultWrappingLookupFunc(operation, path) + }) + + // First test: basic things that should fail, lookup edition + // Root token isn't a wrapping token + _, err = client.Logical().Write("sys/wrapping/lookup", nil) + if err == nil { + t.Fatal("expected error") + } + // Not supplied + _, err = client.Logical().Write("sys/wrapping/lookup", map[string]interface{}{ + "foo": "bar", + }) + if err == nil { + t.Fatal("expected error") + } + // Nonexistent token isn't a wrapping token + _, err = client.Logical().Write("sys/wrapping/lookup", map[string]interface{}{ + "token": "bar", + }) + if err == nil { + t.Fatal("expected error") + } + + // Second: basic things that should fail, unwrap edition + // Root token isn't a wrapping token + _, err = client.Logical().Unwrap(root) + if err == nil { + t.Fatal("expected error") + } + // Root token isn't a wrapping token + _, err = client.Logical().Write("sys/wrapping/unwrap", nil) + if err == nil { + t.Fatal("expected error") + } + // Not supplied + _, err = client.Logical().Write("sys/wrapping/unwrap", map[string]interface{}{ + "foo": "bar", + }) + if err == nil { + t.Fatal("expected error") + } + // Nonexistent token isn't a wrapping token + _, err = client.Logical().Write("sys/wrapping/unwrap", map[string]interface{}{ + "token": "bar", + }) + if err == nil { + t.Fatal("expected error") + } + + // + // Test lookup + // + + // Create a wrapping token + secret, err := client.Logical().Read("secret/foo") + if err != nil { + t.Fatal(err) + } + if secret == nil || secret.WrapInfo == nil { + t.Fatal("secret or wrap info is nil") + } + wrapInfo := secret.WrapInfo + + // Test this twice to ensure no ill effect to the wrapping token as a result of the lookup + for i := 0; i < 2; i++ { + secret, err = client.Logical().Write("sys/wrapping/lookup", map[string]interface{}{ + "token": wrapInfo.Token, + }) + if secret == nil || secret.Data == nil { + t.Fatal("secret or secret data is nil") + } + creationTTL, _ := secret.Data["creation_ttl"].(json.Number).Int64() + if int(creationTTL) != wrapInfo.TTL { + t.Fatalf("mistmatched ttls: %d vs %d", creationTTL, wrapInfo.TTL) + } + if secret.Data["creation_time"].(string) != wrapInfo.CreationTime.Format(time.RFC3339Nano) { + t.Fatalf("mistmatched creation times: %d vs %d", secret.Data["creation_time"].(string), wrapInfo.CreationTime.Format(time.RFC3339Nano)) + } + } + + // + // Test unwrap + // + + // Create a wrapping token + secret, err = client.Logical().Read("secret/foo") + if err != nil { + t.Fatal(err) + } + if secret == nil || secret.WrapInfo == nil { + t.Fatal("secret or wrap info is nil") + } + wrapInfo = secret.WrapInfo + + // Test unwrap via the client token + client.SetToken(wrapInfo.Token) + secret, err = client.Logical().Write("sys/wrapping/unwrap", nil) + if secret == nil || secret.Data == nil { + t.Fatal("secret or secret data is nil") + } + ret1 := secret + // Should be expired and fail + _, err = client.Logical().Write("sys/wrapping/unwrap", nil) + if err == nil { + t.Fatal("expected err") + } + + // Create a wrapping token + client.SetToken(root) + secret, err = client.Logical().Read("secret/foo") + if err != nil { + t.Fatal(err) + } + if secret == nil || secret.WrapInfo == nil { + t.Fatal("secret or wrap info is nil") + } + wrapInfo = secret.WrapInfo + + // Test as a separate token + secret, err = client.Logical().Write("sys/wrapping/unwrap", map[string]interface{}{ + "token": wrapInfo.Token, + }) + ret2 := secret + // Should be expired and fail + _, err = client.Logical().Write("sys/wrapping/unwrap", map[string]interface{}{ + "token": wrapInfo.Token, + }) + if err == nil { + t.Fatal("expected err") + } + + // Create a wrapping token + secret, err = client.Logical().Read("secret/foo") + if err != nil { + t.Fatal(err) + } + if secret == nil || secret.WrapInfo == nil { + t.Fatal("secret or wrap info is nil") + } + wrapInfo = secret.WrapInfo + + // Read response directly + client.SetToken(wrapInfo.Token) + secret, err = client.Logical().Read("cubbyhole/response") + ret3 := secret + // Should be expired and fail + _, err = client.Logical().Write("cubbyhole/response", nil) + if err == nil { + t.Fatal("expected err") + } + + // Create a wrapping token + client.SetToken(root) + secret, err = client.Logical().Read("secret/foo") + if err != nil { + t.Fatal(err) + } + if secret == nil || secret.WrapInfo == nil { + t.Fatal("secret or wrap info is nil") + } + wrapInfo = secret.WrapInfo + + // Read via Unwrap method + secret, err = client.Logical().Unwrap(wrapInfo.Token) + ret4 := secret + // Should be expired and fail + _, err = client.Logical().Unwrap(wrapInfo.Token) + if err == nil { + t.Fatal("expected err") + } + + if !reflect.DeepEqual(ret1.Data, map[string]interface{}{ + "zip": "zap", + }) { + t.Fatalf("ret1 data did not match expected: %#v", ret1.Data) + } + if !reflect.DeepEqual(ret2.Data, map[string]interface{}{ + "zip": "zap", + }) { + t.Fatalf("ret2 data did not match expected: %#v", ret2.Data) + } + var ret3Secret api.Secret + err = jsonutil.DecodeJSON([]byte(ret3.Data["response"].(string)), &ret3Secret) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(ret3Secret.Data, map[string]interface{}{ + "zip": "zap", + }) { + t.Fatalf("ret3 data did not match expected: %#v", ret3Secret.Data) + } + if !reflect.DeepEqual(ret4.Data, map[string]interface{}{ + "zip": "zap", + }) { + t.Fatalf("ret4 data did not match expected: %#v", ret4.Data) + } + + // + // Custom wrapping + // + + client.SetToken(root) + data := map[string]interface{}{ + "zip": "zap", + "three": json.Number("2"), + } + + // Don't set a request TTL on that path, should fail + client.SetWrappingLookupFunc(func(operation, path string) string { + return "" + }) + secret, err = client.Logical().Write("sys/wrapping/wrap", data) + if err == nil { + t.Fatal("expected error") + } + + // Re-set the lookup function + client.SetWrappingLookupFunc(func(operation, path string) string { + if operation == "GET" && path == "secret/foo" { + return "5m" + } + + return api.DefaultWrappingLookupFunc(operation, path) + }) + secret, err = client.Logical().Write("sys/wrapping/wrap", data) + if err != nil { + t.Fatal(err) + } + secret, err = client.Logical().Unwrap(secret.WrapInfo.Token) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(data, secret.Data) { + t.Fatal("custom wrap did not match expected: %#v", secret.Data) + } + + // + // Test rewrap + // + + // Create a wrapping token + secret, err = client.Logical().Read("secret/foo") + if err != nil { + t.Fatal(err) + } + if secret == nil || secret.WrapInfo == nil { + t.Fatal("secret or wrap info is nil") + } + wrapInfo = secret.WrapInfo + + // Test rewrapping + secret, err = client.Logical().Write("sys/wrapping/rewrap", map[string]interface{}{ + "token": wrapInfo.Token, + }) + // Should be expired and fail + _, err = client.Logical().Write("sys/wrapping/unwrap", map[string]interface{}{ + "token": wrapInfo.Token, + }) + if err == nil { + t.Fatal("expected err") + } + + // Attempt unwrapping the rewrapped token + wrapToken := secret.WrapInfo.Token + secret, err = client.Logical().Unwrap(wrapToken) + if err != nil { + t.Fatal(err) + } + // Should be expired and fail + _, err = client.Logical().Unwrap(wrapToken) + if err == nil { + t.Fatal("expected err") + } + + if !reflect.DeepEqual(secret.Data, map[string]interface{}{ + "zip": "zap", + }) { + t.Fatalf("secret data did not match expected: %#v", secret.Data) + } +} diff --git a/meta/meta.go b/meta/meta.go index ca123a0d21..3ff422e2f8 100644 --- a/meta/meta.go +++ b/meta/meta.go @@ -4,7 +4,6 @@ import ( "bufio" "flag" "io" - "os" "github.com/hashicorp/errwrap" "github.com/hashicorp/vault/api" @@ -51,7 +50,7 @@ func (m *Meta) DefaultWrappingLookupFunc(operation, path string) string { return m.flagWrapTTL } - return os.Getenv(api.EnvVaultWrapTTL) + return api.DefaultWrappingLookupFunc(operation, path) } // Client returns the API client to a Vault server given the configured diff --git a/vault/core.go b/vault/core.go index 7395e00f37..99c4fd5f81 100644 --- a/vault/core.go +++ b/vault/core.go @@ -426,7 +426,7 @@ func NewCore(conf *CoreConfig) (*Core, error) { } logicalBackends["cubbyhole"] = CubbyholeBackendFactory logicalBackends["system"] = func(config *logical.BackendConfig) (logical.Backend, error) { - return NewSystemBackend(c, config), nil + return NewSystemBackend(c, config) } c.logicalBackends = logicalBackends @@ -1516,3 +1516,27 @@ func (c *Core) BarrierKeyLength() (min, max int) { max += shamir.ShareOverhead return } + +func (c *Core) ValidateWrappingToken(token string) (bool, error) { + if token == "" { + return false, fmt.Errorf("token is empty") + } + + te, err := c.tokenStore.Lookup(token) + if err != nil { + return false, err + } + if te == nil { + return false, nil + } + + if len(te.Policies) != 1 { + return false, nil + } + + if te.Policies[0] != responseWrappingPolicyName { + return false, nil + } + + return true, nil +} diff --git a/vault/core_test.go b/vault/core_test.go index f73e2d2476..c52bc7b34f 100644 --- a/vault/core_test.go +++ b/vault/core_test.go @@ -1914,7 +1914,10 @@ path "secret/*" { } // Renew the lease - req = logical.TestRequest(t, logical.UpdateOperation, "sys/renew/"+resp.Secret.LeaseID) + req = logical.TestRequest(t, logical.UpdateOperation, "sys/renew") + req.Data = map[string]interface{}{ + "lease_id": resp.Secret.LeaseID, + } req.ClientToken = lresp.Auth.ClientToken _, err = c.HandleRequest(req) if err != nil { diff --git a/vault/logical_system.go b/vault/logical_system.go index 8d9601ef9f..c64f168622 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -3,6 +3,7 @@ package vault import ( "encoding/base64" "encoding/hex" + "encoding/json" "fmt" "strings" "sync" @@ -22,7 +23,7 @@ var ( } ) -func NewSystemBackend(core *Core, config *logical.BackendConfig) logical.Backend { +func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backend, error) { b := &SystemBackend{ Core: core, } @@ -540,12 +541,72 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) logical.Backend HelpSynopsis: strings.TrimSpace(sysHelp["rotate"][0]), HelpDescription: strings.TrimSpace(sysHelp["rotate"][1]), }, + + &framework.Path{ + Pattern: "wrapping/wrap$", + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.handleWrappingWrap, + }, + + HelpSynopsis: strings.TrimSpace(sysHelp["wrap"][0]), + HelpDescription: strings.TrimSpace(sysHelp["wrap"][1]), + }, + + &framework.Path{ + Pattern: "wrapping/unwrap$", + + Fields: map[string]*framework.FieldSchema{ + "token": &framework.FieldSchema{ + Type: framework.TypeString, + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.handleWrappingUnwrap, + }, + + HelpSynopsis: strings.TrimSpace(sysHelp["unwrap"][0]), + HelpDescription: strings.TrimSpace(sysHelp["unwrap"][1]), + }, + + &framework.Path{ + Pattern: "wrapping/lookup$", + + Fields: map[string]*framework.FieldSchema{ + "token": &framework.FieldSchema{ + Type: framework.TypeString, + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.handleWrappingLookup, + }, + + HelpSynopsis: strings.TrimSpace(sysHelp["wraplookup"][0]), + HelpDescription: strings.TrimSpace(sysHelp["wraplookup"][1]), + }, + + &framework.Path{ + Pattern: "wrapping/rewrap$", + + Fields: map[string]*framework.FieldSchema{ + "token": &framework.FieldSchema{ + Type: framework.TypeString, + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.handleWrappingRewrap, + }, + + HelpSynopsis: strings.TrimSpace(sysHelp["rewrap"][0]), + HelpDescription: strings.TrimSpace(sysHelp["rewrap"][1]), + }, }, } - b.Backend.Setup(config) - - return b.Backend + return b.Backend.Setup(config) } // SystemBackend implements logical.Backend and is used to interact with @@ -558,7 +619,11 @@ type SystemBackend struct { // handleCapabilitiesreturns the ACL capabilities of the token for a given path func (b *SystemBackend) handleCapabilities(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - capabilities, err := b.Core.Capabilities(d.Get("token").(string), d.Get("path").(string)) + token := d.Get("token").(string) + if token == "" { + token = req.ClientToken + } + capabilities, err := b.Core.Capabilities(token, d.Get("path").(string)) if err != nil { return nil, err } @@ -660,6 +725,7 @@ func (b *SystemBackend) handleRekeyDelete( return nil, nil } + func (b *SystemBackend) handleRekeyDeleteBarrier( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { return b.handleRekeyDelete(req, data, false) @@ -1406,6 +1472,221 @@ func (b *SystemBackend) handleRotate( return nil, nil } +func (b *SystemBackend) handleWrappingWrap( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + if req.WrapTTL == 0 { + return logical.ErrorResponse("endpoint requires response wrapping to be used"), logical.ErrInvalidRequest + } + + return &logical.Response{ + Data: data.Raw, + }, nil +} + +func (b *SystemBackend) handleWrappingUnwrap( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + // If a third party is unwrapping (rather than the calling token being the + // wrapping token) we detect this so that we can revoke the original + // wrapping token after reading it + var thirdParty bool + + token := data.Get("token").(string) + if token != "" { + thirdParty = true + } else { + token = req.ClientToken + } + + if thirdParty { + // Use the token to decrement the use count to avoid a second operation on the token. + _, err := b.Core.tokenStore.UseTokenByID(token) + if err != nil { + return nil, fmt.Errorf("error decrementing wrapping token's use-count: %v", err) + } + + defer b.Core.tokenStore.Revoke(token) + } + + cubbyReq := &logical.Request{ + Operation: logical.ReadOperation, + Path: "cubbyhole/response", + ClientToken: token, + } + cubbyResp, err := b.Core.router.Route(cubbyReq) + if err != nil { + return nil, fmt.Errorf("error looking up wrapping information: %v", err) + } + if cubbyResp == nil { + return logical.ErrorResponse("no information found; wrapping token may be from a previous Vault version"), nil + } + if cubbyResp != nil && cubbyResp.IsError() { + return cubbyResp, nil + } + if cubbyResp.Data == nil { + return logical.ErrorResponse("wrapping information was nil; wrapping token may be from a previous Vault version"), nil + } + + responseRaw := cubbyResp.Data["response"] + if responseRaw == nil { + return nil, fmt.Errorf("no response found inside the cubbyhole") + } + response, ok := responseRaw.(string) + if !ok { + return nil, fmt.Errorf("could not decode response inside the cubbyhole") + } + + resp := &logical.Response{ + Data: map[string]interface{}{}, + } + if len(response) == 0 { + resp.Data[logical.HTTPStatusCode] = 204 + } else { + resp.Data[logical.HTTPStatusCode] = 200 + resp.Data[logical.HTTPRawBody] = []byte(response) + resp.Data[logical.HTTPContentType] = "application/json" + } + + return resp, nil +} + +func (b *SystemBackend) handleWrappingLookup( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + token := data.Get("token").(string) + + if token == "" { + return logical.ErrorResponse("missing \"token\" value in input"), logical.ErrInvalidRequest + } + + cubbyReq := &logical.Request{ + Operation: logical.ReadOperation, + Path: "cubbyhole/wrapinfo", + ClientToken: token, + } + cubbyResp, err := b.Core.router.Route(cubbyReq) + if err != nil { + return nil, fmt.Errorf("error looking up wrapping information: %v", err) + } + if cubbyResp == nil { + return logical.ErrorResponse("no information found; wrapping token may be from a previous Vault version"), nil + } + if cubbyResp != nil && cubbyResp.IsError() { + return cubbyResp, nil + } + if cubbyResp.Data == nil { + return logical.ErrorResponse("wrapping information was nil; wrapping token may be from a previous Vault version"), nil + } + + creationTTLRaw := cubbyResp.Data["creation_ttl"] + creationTime := cubbyResp.Data["creation_time"] + + resp := &logical.Response{ + Data: map[string]interface{}{}, + } + if creationTTLRaw != nil { + creationTTL, err := creationTTLRaw.(json.Number).Int64() + if err != nil { + return nil, fmt.Errorf("error reading creation_ttl value from wrapping information: %v", err) + } + resp.Data["creation_ttl"] = time.Duration(creationTTL).Seconds() + } + if creationTime != nil { + // This was JSON marshaled so it's already a string in RFC3339 format + resp.Data["creation_time"] = cubbyResp.Data["creation_time"] + } + + return resp, nil +} + +func (b *SystemBackend) handleWrappingRewrap( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + // If a third party is rewrapping (rather than the calling token being the + // wrapping token) we detect this so that we can revoke the original + // wrapping token after reading it. Right now wrapped tokens can't unwrap + // themselves, but in case we change it, this will be ready to do the right + // thing. + var thirdParty bool + + token := data.Get("token").(string) + if token != "" { + thirdParty = true + } else { + token = req.ClientToken + } + + if thirdParty { + // Use the token to decrement the use count to avoid a second operation on the token. + _, err := b.Core.tokenStore.UseTokenByID(token) + if err != nil { + return nil, fmt.Errorf("error decrementing wrapping token's use-count: %v", err) + } + defer b.Core.tokenStore.Revoke(token) + } + + // Fetch the original TTL + cubbyReq := &logical.Request{ + Operation: logical.ReadOperation, + Path: "cubbyhole/wrapinfo", + ClientToken: token, + } + cubbyResp, err := b.Core.router.Route(cubbyReq) + if err != nil { + return nil, fmt.Errorf("error looking up wrapping information: %v", err) + } + if cubbyResp == nil { + return logical.ErrorResponse("no information found; wrapping token may be from a previous Vault version"), nil + } + if cubbyResp != nil && cubbyResp.IsError() { + return cubbyResp, nil + } + if cubbyResp.Data == nil { + return logical.ErrorResponse("wrapping information was nil; wrapping token may be from a previous Vault version"), nil + } + + // Set the creation TTL on the request + creationTTLRaw := cubbyResp.Data["creation_ttl"] + if creationTTLRaw == nil { + return nil, fmt.Errorf("creation_ttl value in wrapping information was nil") + } + creationTTL, err := cubbyResp.Data["creation_ttl"].(json.Number).Int64() + if err != nil { + return nil, fmt.Errorf("error reading creation_ttl value from wrapping information: %v", err) + } + + // Fetch the original response and return it as the data for the new response + cubbyReq = &logical.Request{ + Operation: logical.ReadOperation, + Path: "cubbyhole/response", + ClientToken: token, + } + cubbyResp, err = b.Core.router.Route(cubbyReq) + if err != nil { + return nil, fmt.Errorf("error looking up response: %v", err) + } + if cubbyResp == nil { + return logical.ErrorResponse("no information found; wrapping token may be from a previous Vault version"), nil + } + if cubbyResp != nil && cubbyResp.IsError() { + return cubbyResp, nil + } + if cubbyResp.Data == nil { + return logical.ErrorResponse("wrapping information was nil; wrapping token may be from a previous Vault version"), nil + } + + response := cubbyResp.Data["response"] + if response == nil { + return nil, fmt.Errorf("no response found inside the cubbyhole") + } + + // Return response in "response"; wrapping code will detect the rewrap and + // slot in instead of nesting + req.WrapTTL = time.Duration(creationTTL) + return &logical.Response{ + Data: map[string]interface{}{ + "response": response, + }, + }, nil +} + func sanitizeMountPath(path string) string { if !strings.HasSuffix(path, "/") { path += "/" @@ -1798,4 +2079,27 @@ Enable a new audit backend or disable an existing backend. `When there is no access to the token, token accessor can be used to fetch the token's capabilities on a given path.`, }, + + "wrap": { + "Response-wraps an arbitrary JSON object.", + `Round trips the given input data into a response-wrapped token.`, + }, + + "unwrap": { + "Unwraps a response-wrapped token.", + `Unwraps a response-wrapped token. Unlike simply reading from cubbyhole/response, + this provides additional validation on the token, and rather than a JSON-escaped + string, the returned response is the exact same as the contained wrapped response.`, + }, + + "wraplookup": { + "Looks up the properties of a response-wrapped token.", + `Returns the creation TTL and creation time of a response-wrapped token.`, + }, + + "rewrap": { + "Rotates a response-wrapped token.", + `Rotates a response-wrapped token; the output is a new token with the same + response wrapped inside and the same creation TTL. The original token is revoked.`, + }, } diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index 14f2986428..d6bca56f65 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -509,14 +509,18 @@ func TestSystemBackend_revokePrefixAuth(t *testing.T) { MaxLeaseTTLVal: time.Hour * 24 * 32, }, } - b := NewSystemBackend(core, bc) + b, err := NewSystemBackend(core, bc) + if err != nil { + t.Fatal(err) + } + exp := ts.expiration te := &TokenEntry{ ID: "foo", Path: "auth/github/login/bar", } - err := ts.create(te) + err = ts.create(te) if err != nil { t.Fatal(err) } @@ -1038,7 +1042,13 @@ func testSystemBackend(t *testing.T) logical.Backend { MaxLeaseTTLVal: time.Hour * 24 * 32, }, } - return NewSystemBackend(c, bc) + + b, err := NewSystemBackend(c, bc) + if err != nil { + t.Fatal(err) + } + + return b } func testCoreSystemBackend(t *testing.T) (*Core, logical.Backend, string) { @@ -1050,5 +1060,10 @@ func testCoreSystemBackend(t *testing.T) (*Core, logical.Backend, string) { MaxLeaseTTLVal: time.Hour * 24 * 32, }, } - return c, NewSystemBackend(c, bc), root + + b, err := NewSystemBackend(c, bc) + if err != nil { + t.Fatal(err) + } + return c, b, root } diff --git a/vault/policy_store.go b/vault/policy_store.go index 3768f9120a..46cfdf7087 100644 --- a/vault/policy_store.go +++ b/vault/policy_store.go @@ -19,48 +19,75 @@ const ( // policyCacheSize is the number of policies that are kept cached policyCacheSize = 1024 - // cubbyholeResponseWrappingPolicyName is the name of the fixed policy - cubbyholeResponseWrappingPolicyName = "response-wrapping" + // responseWrappingPolicyName is the name of the fixed policy + responseWrappingPolicyName = "response-wrapping" - // cubbyholeResponseWrappingPolicy is the policy that ensures cubbyhole - // response wrapping can always succeed - cubbyholeResponseWrappingPolicy = ` + // responseWrappingPolicy is the policy that ensures cubbyhole response + // wrapping can always succeed. Note that sys/wrapping/lookup isn't + // contained here because using it would revoke the token anyways, so there + // isn't much point. + responseWrappingPolicy = ` path "cubbyhole/response" { capabilities = ["create", "read"] } + +path "sys/wrapping/unwrap" { + capabilities = ["update"] +} ` // defaultPolicy is the "default" policy defaultPolicy = ` +# Allow tokens to look up their own properties path "auth/token/lookup-self" { capabilities = ["read"] } +# Allow tokens to renew themselves path "auth/token/renew-self" { capabilities = ["update"] } +# Allow tokens to revoke themselves path "auth/token/revoke-self" { capabilities = ["update"] } -path "cubbyhole/*" { - capabilities = ["create", "read", "update", "delete", "list"] -} - -path "cubbyhole" { - capabilities = ["list"] -} - +# Allow a token to look up its own capabilities on a path path "sys/capabilities-self" { capabilities = ["update"] } +# Allow a token to renew a lease via lease_id in the request body path "sys/renew" { capabilities = ["update"] } -path "sys/renew/*" { +# Allow a token to manage its own cubbyhole +path "cubbyhole/*" { + capabilities = ["create", "read", "update", "delete", "list"] +} + +# Allow a token to list its cubbyhole (not covered by the splat above) +path "cubbyhole" { + capabilities = ["list"] +} + +# Allow a token to wrap arbitrary values in a response-wrapping token +path "sys/wrapping/wrap" { + capabilities = ["update"] +} + +# Allow a token to look up the creation time and TTL of a given +# response-wrapping token +path "sys/wrapping/lookup" { + capabilities = ["update"] +} + +# Allow a token to unwrap a response-wrapping token. This is a convenience to +# avoid client token swapping since this is also part of the response wrapping +# policy. +path "sys/wrapping/unwrap" { capabilities = ["update"] } ` @@ -69,10 +96,10 @@ path "sys/renew/*" { var ( immutablePolicies = []string{ "root", - cubbyholeResponseWrappingPolicyName, + responseWrappingPolicyName, } nonAssignablePolicies = []string{ - cubbyholeResponseWrappingPolicyName, + responseWrappingPolicyName, } ) @@ -125,12 +152,12 @@ func (c *Core) setupPolicyStore() error { } // Ensure that the cubbyhole response wrapping policy exists - policy, err = c.policyStore.GetPolicy(cubbyholeResponseWrappingPolicyName) + policy, err = c.policyStore.GetPolicy(responseWrappingPolicyName) if err != nil { return errwrap.Wrapf("error fetching response-wrapping policy from store: {{err}}", err) } - if policy == nil || policy.Raw != cubbyholeResponseWrappingPolicy { - err := c.policyStore.createCubbyholeResponseWrappingPolicy() + if policy == nil || policy.Raw != responseWrappingPolicy { + err := c.policyStore.createResponseWrappingPolicy() if err != nil { return err } @@ -324,16 +351,16 @@ func (ps *PolicyStore) createDefaultPolicy() error { return ps.setPolicyInternal(policy) } -func (ps *PolicyStore) createCubbyholeResponseWrappingPolicy() error { - policy, err := Parse(cubbyholeResponseWrappingPolicy) +func (ps *PolicyStore) createResponseWrappingPolicy() error { + policy, err := Parse(responseWrappingPolicy) if err != nil { - return errwrap.Wrapf(fmt.Sprintf("error parsing %s policy: {{err}}", cubbyholeResponseWrappingPolicyName), err) + return errwrap.Wrapf(fmt.Sprintf("error parsing %s policy: {{err}}", responseWrappingPolicyName), err) } if policy == nil { - return fmt.Errorf("parsing %s policy resulted in nil policy", cubbyholeResponseWrappingPolicyName) + return fmt.Errorf("parsing %s policy resulted in nil policy", responseWrappingPolicyName) } - policy.Name = cubbyholeResponseWrappingPolicyName + policy.Name = responseWrappingPolicyName return ps.setPolicyInternal(policy) } diff --git a/vault/policy_store_test.go b/vault/policy_store_test.go index 4a0656e23b..dafca34c3f 100644 --- a/vault/policy_store_test.go +++ b/vault/policy_store_test.go @@ -147,8 +147,8 @@ func TestPolicyStore_Predefined(t *testing.T) { if err != nil { t.Fatalf("err: %v", err) } - if pCubby.Raw != cubbyholeResponseWrappingPolicy { - t.Fatalf("bad: expected\n%s\ngot\n%s\n", cubbyholeResponseWrappingPolicy, pCubby.Raw) + if pCubby.Raw != responseWrappingPolicy { + t.Fatalf("bad: expected\n%s\ngot\n%s\n", responseWrappingPolicy, pCubby.Raw) } pRoot, err := core.policyStore.GetPolicy("root") if err != nil { diff --git a/vault/request_handling.go b/vault/request_handling.go index e9c7b7ca96..8f36e58e88 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -2,6 +2,7 @@ package vault import ( "encoding/json" + "fmt" "strings" "time" @@ -53,15 +54,30 @@ func (c *Core) HandleRequest(req *logical.Request) (resp *logical.Response, err } // We are wrapping if there is anything to wrap (not a nil response) and a - // TTL was specified for the token - wrapping := resp != nil && resp.WrapInfo != nil && resp.WrapInfo.TTL != 0 + // TTL was specified for the token. Errors on a call should be returned to + // the caller, so wrapping is turned off if an error is hit and the error + // is logged to the audit log. + wrapping := resp != nil && + err == nil && + !resp.IsError() && + resp.WrapInfo != nil && + resp.WrapInfo.TTL != 0 if wrapping { - cubbyResp, err := c.wrapInCubbyhole(req, resp) + cubbyResp, cubbyErr := c.wrapInCubbyhole(req, resp) // If not successful, returns either an error response from the - // cubbyhole backend or an error; if either is set, return - if cubbyResp != nil || err != nil { - return cubbyResp, err + // cubbyhole backend or an error; if either is set, set resp and err to + // those and continue so that that's what we audit log. Otherwise + // finish the wrapping and audit log that. + if cubbyResp != nil || cubbyErr != nil { + resp = cubbyResp + err = cubbyErr + } else { + wrappingResp := &logical.Response{ + WrapInfo: resp.WrapInfo, + } + wrappingResp.CloneWarnings(resp) + resp = wrappingResp } } @@ -71,16 +87,6 @@ func (c *Core) HandleRequest(req *logical.Request) (resp *logical.Response, err return nil, ErrInternalError } - // If we are wrapping, now is when we create a new response object with the - // wrapped information, since the original response has been audit logged - if wrapping { - wrappingResp := &logical.Response{ - WrapInfo: resp.WrapInfo, - } - wrappingResp.CloneWarnings(resp) - resp = wrappingResp - } - return } @@ -244,6 +250,13 @@ func (c *Core) handleRequest(req *logical.Request) (retResp *logical.Response, r } } + if resp != nil && + req.Path == "cubbyhole/response" && + len(te.Policies) == 1 && + te.Policies[0] == responseWrappingPolicyName { + resp.AddWarning("Please use sys/wrapping/unwrap to unwrap responses, as it provides additional security checks.") + } + // Return the response and error if err != nil { retErr = multierror.Append(retErr, err) @@ -333,6 +346,13 @@ func (c *Core) handleLoginRequest(req *logical.Request) (*logical.Response, *log te.Policies = policyutil.SanitizePolicies(te.Policies, true) + // Prevent internal policies from being assigned to tokens + for _, policy := range te.Policies { + if strutil.StrListContains(nonAssignablePolicies, policy) { + return logical.ErrorResponse(fmt.Sprintf("cannot assign policy %q", policy)), nil, logical.ErrInvalidRequest + } + } + if err := c.tokenStore.create(&te); err != nil { c.logger.Error("core: failed to create token", "error", err) return nil, auth, ErrInternalError @@ -404,31 +424,39 @@ func (c *Core) wrapInCubbyhole(req *logical.Request, resp *logical.Response) (*l resp.WrapInfo.WrappedAccessor = resp.Auth.Accessor } - httpResponse := logical.SanitizeResponse(resp) - - // Add the unique identifier of the original request to the response - httpResponse.RequestID = req.ID - - // Because of the way that JSON encodes (likely just in Go) we actually get - // mixed-up values for ints if we simply put this object in the response - // and encode the whole thing; so instead we marshal it first, then store - // the string response. This actually ends up making it easier on the - // client side, too, as it becomes a straight read-string-pass-to-unmarshal - // operation. - - marshaledResponse, err := json.Marshal(httpResponse) - if err != nil { - c.logger.Error("core: failed to marshal wrapped response", "error", err) - return nil, ErrInternalError - } - cubbyReq := &logical.Request{ Operation: logical.CreateOperation, Path: "cubbyhole/response", ClientToken: te.ID, - Data: map[string]interface{}{ + } + + // During a rewrap, store the original response, don't wrap it again. + if req.Path == "sys/wrapping/rewrap" { + cubbyReq.Data = map[string]interface{}{ + "response": resp.Data["response"], + } + } else { + httpResponse := logical.SanitizeResponse(resp) + + // Add the unique identifier of the original request to the response + httpResponse.RequestID = req.ID + + // Because of the way that JSON encodes (likely just in Go) we actually get + // mixed-up values for ints if we simply put this object in the response + // and encode the whole thing; so instead we marshal it first, then store + // the string response. This actually ends up making it easier on the + // client side, too, as it becomes a straight read-string-pass-to-unmarshal + // operation. + + marshaledResponse, err := json.Marshal(httpResponse) + if err != nil { + c.logger.Error("core: failed to marshal wrapped response", "error", err) + return nil, ErrInternalError + } + + cubbyReq.Data = map[string]interface{}{ "response": string(marshaledResponse), - }, + } } cubbyResp, err := c.router.Route(cubbyReq) @@ -444,6 +472,25 @@ func (c *Core) wrapInCubbyhole(req *logical.Request, resp *logical.Response) (*l return cubbyResp, nil } + // Store info for lookup + cubbyReq.Path = "cubbyhole/wrapinfo" + cubbyReq.Data = map[string]interface{}{ + "creation_ttl": resp.WrapInfo.TTL, + "creation_time": creationTime, + } + cubbyResp, err = c.router.Route(cubbyReq) + if err != nil { + // Revoke since it's not yet being tracked for expiration + c.tokenStore.Revoke(te.ID) + c.logger.Error("core: failed to store wrapping information", "error", err) + return nil, ErrInternalError + } + if cubbyResp != nil && cubbyResp.IsError() { + c.tokenStore.Revoke(te.ID) + c.logger.Error("core: failed to store wrapping information", "error", cubbyResp.Data["error"]) + return cubbyResp, nil + } + auth := &logical.Auth{ ClientToken: te.ID, Policies: []string{"response-wrapping"}, diff --git a/vault/router.go b/vault/router.go index d1c0746762..0d18383b77 100644 --- a/vault/router.go +++ b/vault/router.go @@ -223,7 +223,7 @@ func (r *Router) routeCommon(req *logical.Request, existenceCheck bool) (*logica } // Adjust the path to exclude the routing prefix - original := req.Path + originalPath := req.Path req.Path = strings.TrimPrefix(req.Path, mount) req.MountPoint = mount if req.Path == "/" { @@ -236,8 +236,9 @@ func (r *Router) routeCommon(req *logical.Request, existenceCheck bool) (*logica // Hash the request token unless this is the token backend clientToken := req.ClientToken switch { - case strings.HasPrefix(original, "auth/token/"): - case strings.HasPrefix(original, "cubbyhole/"): + case strings.HasPrefix(originalPath, "auth/token/"): + case strings.HasPrefix(originalPath, "sys/"): + case strings.HasPrefix(originalPath, "cubbyhole/"): // In order for the token store to revoke later, we need to have the same // salted ID, so we double-salt what's going to the cubbyhole backend req.ClientToken = re.SaltID(r.tokenStoreSalt.SaltID(req.ClientToken)) @@ -251,14 +252,23 @@ func (r *Router) routeCommon(req *logical.Request, existenceCheck bool) (*logica // Cache the identifier of the request originalReqID := req.ID + // Cache the wrap TTL of the request + originalWrapTTL := req.WrapTTL + // Reset the request before returning defer func() { - req.Path = original + req.Path = originalPath req.MountPoint = "" req.Connection = originalConn req.ID = originalReqID req.Storage = nil req.ClientToken = clientToken + + // Only the rewrap endpoint is allowed to declare a wrap TTL on a + // request that did not come from the client + if req.Path != "sys/wrapping/rewrap" { + req.WrapTTL = originalWrapTTL + } }() // Invoke the backend diff --git a/vault/testing.go b/vault/testing.go index 24235051eb..e3a5903ba5 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -591,8 +591,6 @@ func TestCluster(t *testing.T, handlers []http.Handler, base *CoreConfig, unseal DisableMlock: true, } - coreConfig.LogicalBackends["generic"] = PassthroughBackendFactory - if base != nil { // Used to set something non-working to test fallback switch base.ClusterAddr { diff --git a/vault/token_store.go b/vault/token_store.go index 1e143b96e9..0fc954f500 100644 --- a/vault/token_store.go +++ b/vault/token_store.go @@ -763,6 +763,15 @@ func (ts *TokenStore) UseToken(te *TokenEntry) (*TokenEntry, error) { return te, nil } +func (ts *TokenStore) UseTokenByID(id string) (*TokenEntry, error) { + te, err := ts.Lookup(id) + if err != nil { + return te, err + } + + return ts.UseToken(te) +} + // Lookup is used to find a token given its ID. It acquires a read lock, then calls lookupSalted. func (ts *TokenStore) Lookup(id string) (*TokenEntry, error) { defer metrics.MeasureSince([]string{"token", "lookup"}, time.Now()) @@ -1259,7 +1268,7 @@ func (ts *TokenStore) handleCreateCommon( // Prevent internal policies from being assigned to tokens for _, policy := range te.Policies { if strutil.StrListContains(nonAssignablePolicies, policy) { - return logical.ErrorResponse(fmt.Sprintf("cannot assign %s policy", policy)), nil + return logical.ErrorResponse(fmt.Sprintf("cannot assign policy %q", policy)), nil } } diff --git a/vault/token_store_test.go b/vault/token_store_test.go index 52b9282559..2a10907ed2 100644 --- a/vault/token_store_test.go +++ b/vault/token_store_test.go @@ -766,7 +766,7 @@ func TestTokenStore_HandleRequest_NonAssignable(t *testing.T) { t.Fatalf("err: %v %v", err, resp) } - req.Data["policies"] = []string{"default", "foo", cubbyholeResponseWrappingPolicyName} + req.Data["policies"] = []string{"default", "foo", responseWrappingPolicyName} resp, err = ts.HandleRequest(req) if err != nil { diff --git a/website/source/docs/http/sys-wrapping-lookup.html.md b/website/source/docs/http/sys-wrapping-lookup.html.md new file mode 100644 index 0000000000..4feadd76b1 --- /dev/null +++ b/website/source/docs/http/sys-wrapping-lookup.html.md @@ -0,0 +1,54 @@ +--- +layout: "http" +page_title: "HTTP API: /sys/wrapping/lookup" +sidebar_current: "docs-http-wrapping-lookup" +description: |- + The '/sys/wrapping/lookup' endpoint returns wrapping token properties +--- + +# /sys/wrapping/lookup + +## POST + +
+
Description
+
+ Looks up wrapping properties for the given token. +
+ +
Method
+
POST
+ +
URL
+
`/sys/wrapping/lookup`
+ +
Parameters
+
+
    +
  • + token + required + The wrapping token ID. +
  • +
+
+ +
Returns
+
+ + ```javascript + { + "request_id": "481320f5-fdf8-885d-8050-65fa767fd19b", + "lease_id": "", + "lease_duration": 0, + "renewable": false, + "data": { + "creation_time": "2016-09-28T14:16:13.07103516-04:00", + "creation_ttl": 300 + }, + "warnings": null + } + ``` + +
+
diff --git a/website/source/docs/http/sys-wrapping-rewrap.html.md b/website/source/docs/http/sys-wrapping-rewrap.html.md new file mode 100644 index 0000000000..94e532ed5e --- /dev/null +++ b/website/source/docs/http/sys-wrapping-rewrap.html.md @@ -0,0 +1,60 @@ +--- +layout: "http" +page_title: "HTTP API: /sys/wrapping/rewrap" +sidebar_current: "docs-http-wrapping-rewrap" +description: |- + The '/sys/wrapping/rewrap' endpoint can be used to rotate a wrapping token and refresh its TTL +--- + +# /sys/wrapping/rewrap + +## POST + +
+
Description
+
+ Rewraps a response-wrapped token; the new token will use the same creation + TTL as the original token and contain the same response. The old token will + be invalidated. This can be used for long-term storage of a secret in a + response-wrapped token when rotation is a requirement. +
+ +
Method
+
POST
+ +
URL
+
`/sys/wrapping/rewrap`
+ +
Parameters
+
+
    +
  • + token + required + The wrapping token ID. +
  • +
+
+ +
Returns
+
+ + ```javascript + { + "request_id": "", + "lease_id": "", + "lease_duration": 0, + "renewable": false, + "data": null, + "warnings": null, + "wrap_info": { + "token": "3b6f1193-0707-ac17-284d-e41032e74d1f", + "ttl": 300, + "creation_time": "2016-09-28T14:22:26.486186607-04:00", + "wrapped_accessor": "" + } + } + ``` + +
+
diff --git a/website/source/docs/http/sys-wrapping-unwrap.html.md b/website/source/docs/http/sys-wrapping-unwrap.html.md new file mode 100644 index 0000000000..d201000724 --- /dev/null +++ b/website/source/docs/http/sys-wrapping-unwrap.html.md @@ -0,0 +1,56 @@ +--- +layout: "http" +page_title: "HTTP API: /sys/wrapping/unwrap" +sidebar_current: "docs-http-wrapping-unwrap" +description: |- + The '/sys/wrapping/unwrap' endpoint unwraps a wrapped response +--- + +# /sys/wrapping/unwrap + +## POST + +
+
Description
+
+ Returns the original response inside the given wrapping token. Unlike + simply reading `cubbyhole/response`, this endpoint provides additional + validation checks on the token, and returns the original value on the wire + rather than a JSON string representation of it. +
+ +
Method
+
POST
+ +
URL
+
`/sys/wrapping/unwrap`
+ +
Parameters
+
+
    +
  • + token + required + The wrapping token ID. +
  • +
+
+ +
Returns
+
+ + ```javascript + { + "request_id": "8e33c808-f86c-cff8-f30a-fbb3ac22c4a8", + "lease_id": "", + "lease_duration": 2592000, + "renewable": false, + "data": { + "zip": "zap" + }, + "warnings": null + } + ``` + +
+
diff --git a/website/source/docs/http/sys-wrapping-wrap.html.md b/website/source/docs/http/sys-wrapping-wrap.html.md new file mode 100644 index 0000000000..fd71bb136a --- /dev/null +++ b/website/source/docs/http/sys-wrapping-wrap.html.md @@ -0,0 +1,59 @@ +--- +layout: "http" +page_title: "HTTP API: /sys/wrapping/wrap" +sidebar_current: "docs-http-wrapping-wrap" +description: |- + The '/sys/wrapping/wrap' endpoint wraps the given values in a response-wrapped token +--- + +# /sys/wrapping/wrap + +## POST + +
+
Description
+
+ Wraps the given user-supplied data inside a response-wrapped token. +
+ +
Method
+
POST
+ +
URL
+
`/sys/wrapping/wrap`
+ +
Parameters
+
+
    +
  • + [any] + optional + Parameters should be supplied as keys/values in a JSON object. The + exact set of given parameters will be contained in the wrapped + response. +
  • +
+
+ +
Returns
+
+ + ```javascript + { + "request_id": "", + "lease_id": "", + "lease_duration": 0, + "renewable": false, + "data": null, + "warnings": null, + "wrap_info": { + "token": "fb79b9d3-d94e-9eb6-4919-c559311133d6", + "ttl": 300, + "creation_time": "2016-09-28T14:41:00.56961496-04:00", + "wrapped_accessor": "" + } + } + ``` + +
+
diff --git a/website/source/layouts/http.erb b/website/source/layouts/http.erb index 3659e8e501..93f7ef83dd 100644 --- a/website/source/layouts/http.erb +++ b/website/source/layouts/http.erb @@ -117,6 +117,24 @@ + > + Response Wrapping + + + > High Availability