diff --git a/builtin/credential/app-id/backend_test.go b/builtin/credential/app-id/backend_test.go index 8cf7a510e..c5efd79d5 100644 --- a/builtin/credential/app-id/backend_test.go +++ b/builtin/credential/app-id/backend_test.go @@ -63,7 +63,7 @@ func TestBackend_upgradeToSalted(t *testing.T) { // Initialize the backend, this should do the automatic upgrade conf := &logical.BackendConfig{ - View: inm, + StorageView: inm, } backend, err := Factory(conf) if err != nil { diff --git a/http/handler.go b/http/handler.go index 6a93e8ce1..81bf6b3df 100644 --- a/http/handler.go +++ b/http/handler.go @@ -30,8 +30,8 @@ func Handler(core *vault.Core) http.Handler { mux.Handle("/v1/sys/policy", handleSysListPolicies(core)) mux.Handle("/v1/sys/policy/", handleSysPolicy(core)) mux.Handle("/v1/sys/renew/", proxySysRequest(core)) - mux.Handle("/v1/sys/revoke/", handleSysRevoke(core)) - mux.Handle("/v1/sys/revoke-prefix/", handleSysRevokePrefix(core)) + mux.Handle("/v1/sys/revoke/", proxySysRequest(core)) + mux.Handle("/v1/sys/revoke-prefix/", proxySysRequest(core)) mux.Handle("/v1/sys/auth", proxySysRequest(core)) mux.Handle("/v1/sys/auth/", proxySysRequest(core)) mux.Handle("/v1/sys/audit", handleSysListAudit(core)) diff --git a/http/handler_test.go b/http/handler_test.go index 935319958..8bb4ed98b 100644 --- a/http/handler_test.go +++ b/http/handler_test.go @@ -47,6 +47,14 @@ func TestSysMounts_headerAuth(t *testing.T) { "max_lease_ttl": float64(0), }, }, + "cubbyhole/": map[string]interface{}{ + "description": "per-token private secret storage", + "type": "cubbyhole", + "config": map[string]interface{}{ + "default_lease_ttl": float64(0), + "max_lease_ttl": float64(0), + }, + }, } testResponseStatus(t, resp, 200) testResponseBody(t, resp, &actual) diff --git a/http/sys_lease.go b/http/sys_lease.go deleted file mode 100644 index 1bffb08af..000000000 --- a/http/sys_lease.go +++ /dev/null @@ -1,79 +0,0 @@ -package http - -import ( - "net/http" - "strings" - - "github.com/hashicorp/vault/logical" - "github.com/hashicorp/vault/vault" -) - -func handleSysRevoke(core *vault.Core) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != "PUT" { - respondError(w, http.StatusMethodNotAllowed, nil) - return - } - - // Determine the path... - prefix := "/v1/sys/revoke/" - if !strings.HasPrefix(r.URL.Path, prefix) { - respondError(w, http.StatusNotFound, nil) - return - } - path := r.URL.Path[len(prefix):] - if path == "" { - respondError(w, http.StatusNotFound, nil) - return - } - - _, err := core.HandleRequest(requestAuth(r, &logical.Request{ - Operation: logical.WriteOperation, - Path: "sys/revoke/" + path, - Connection: getConnection(r), - })) - if err != nil { - respondError(w, http.StatusBadRequest, err) - return - } - - respondOk(w, nil) - }) -} - -func handleSysRevokePrefix(core *vault.Core) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != "PUT" { - respondError(w, http.StatusMethodNotAllowed, nil) - return - } - - // Determine the path... - prefix := "/v1/sys/revoke-prefix/" - if !strings.HasPrefix(r.URL.Path, prefix) { - respondError(w, http.StatusNotFound, nil) - return - } - path := r.URL.Path[len(prefix):] - if path == "" { - respondError(w, http.StatusNotFound, nil) - return - } - - _, err := core.HandleRequest(requestAuth(r, &logical.Request{ - Operation: logical.WriteOperation, - Path: "sys/revoke-prefix/" + path, - Connection: getConnection(r), - })) - if err != nil { - respondError(w, http.StatusBadRequest, err) - return - } - - respondOk(w, nil) - }) -} - -type RenewRequest struct { - Increment int `json:"increment"` -} diff --git a/http/sys_mount_test.go b/http/sys_mount_test.go index 42742f6c1..d258d00f3 100644 --- a/http/sys_mount_test.go +++ b/http/sys_mount_test.go @@ -35,6 +35,14 @@ func TestSysMounts(t *testing.T) { "max_lease_ttl": float64(0), }, }, + "cubbyhole/": map[string]interface{}{ + "description": "per-token private secret storage", + "type": "cubbyhole", + "config": map[string]interface{}{ + "default_lease_ttl": float64(0), + "max_lease_ttl": float64(0), + }, + }, } testResponseStatus(t, resp, 200) testResponseBody(t, resp, &actual) @@ -83,6 +91,14 @@ func TestSysMount(t *testing.T) { "max_lease_ttl": float64(0), }, }, + "cubbyhole/": map[string]interface{}{ + "description": "per-token private secret storage", + "type": "cubbyhole", + "config": map[string]interface{}{ + "default_lease_ttl": float64(0), + "max_lease_ttl": float64(0), + }, + }, } testResponseStatus(t, resp, 200) testResponseBody(t, resp, &actual) @@ -153,6 +169,14 @@ func TestSysRemount(t *testing.T) { "max_lease_ttl": float64(0), }, }, + "cubbyhole/": map[string]interface{}{ + "description": "per-token private secret storage", + "type": "cubbyhole", + "config": map[string]interface{}{ + "default_lease_ttl": float64(0), + "max_lease_ttl": float64(0), + }, + }, } testResponseStatus(t, resp, 200) testResponseBody(t, resp, &actual) @@ -196,6 +220,14 @@ func TestSysUnmount(t *testing.T) { "max_lease_ttl": float64(0), }, }, + "cubbyhole/": map[string]interface{}{ + "description": "per-token private secret storage", + "type": "cubbyhole", + "config": map[string]interface{}{ + "default_lease_ttl": float64(0), + "max_lease_ttl": float64(0), + }, + }, } testResponseStatus(t, resp, 200) testResponseBody(t, resp, &actual) @@ -244,6 +276,14 @@ func TestSysTuneMount(t *testing.T) { "max_lease_ttl": float64(0), }, }, + "cubbyhole/": map[string]interface{}{ + "description": "per-token private secret storage", + "type": "cubbyhole", + "config": map[string]interface{}{ + "default_lease_ttl": float64(0), + "max_lease_ttl": float64(0), + }, + }, } testResponseStatus(t, resp, 200) testResponseBody(t, resp, &actual) @@ -313,6 +353,14 @@ func TestSysTuneMount(t *testing.T) { "max_lease_ttl": float64(0), }, }, + "cubbyhole/": map[string]interface{}{ + "description": "per-token private secret storage", + "type": "cubbyhole", + "config": map[string]interface{}{ + "default_lease_ttl": float64(0), + "max_lease_ttl": float64(0), + }, + }, } testResponseStatus(t, resp, 200) diff --git a/logical/framework/backend.go b/logical/framework/backend.go index b29cc2a6e..eb2b5e72b 100644 --- a/logical/framework/backend.go +++ b/logical/framework/backend.go @@ -53,7 +53,7 @@ type Backend struct { // Clean is called on unload to clean up e.g any existing connections // to the backend, if required. - Clean CleanupFunc + Clean CleanupFunc // AuthRenew is the callback to call when a RenewRequest for an // authentication comes in. By default, renewal won't be allowed. @@ -159,6 +159,7 @@ func (b *Backend) Cleanup() { b.Clean() } } + // Logger can be used to get the logger. If no logger has been set, // the logs will be discarded. func (b *Backend) Logger() *log.Logger { diff --git a/vault/auth.go b/vault/auth.go index 8898d7d33..e259d0928 100644 --- a/vault/auth.go +++ b/vault/auth.go @@ -291,6 +291,7 @@ func (c *Core) newCredentialBackend( StorageView: view, Logger: c.logger, Config: conf, + System: sysView, } b, err := f(config) diff --git a/vault/core.go b/vault/core.go index 8517aa1e6..30779c989 100644 --- a/vault/core.go +++ b/vault/core.go @@ -351,6 +351,7 @@ func NewCore(conf *CoreConfig) (*Core, error) { logicalBackends[k] = f } logicalBackends["generic"] = PassthroughBackendFactory + logicalBackends["cubbyhole"] = CubbyholeBackendFactory logicalBackends["system"] = func(config *logical.BackendConfig) (logical.Backend, error) { return NewSystemBackend(c, config), nil } diff --git a/vault/expiration.go b/vault/expiration.go index 702821157..3bf86d5cf 100644 --- a/vault/expiration.go +++ b/vault/expiration.go @@ -477,6 +477,7 @@ func (m *ExpirationManager) revokeEntry(le *leaseEntry) error { if err := m.tokenStore.RevokeTree(le.Auth.ClientToken); err != nil { return fmt.Errorf("failed to revoke token: %v", err) } + return nil } diff --git a/vault/logical_cubbyhole.go b/vault/logical_cubbyhole.go new file mode 100644 index 000000000..d6948dced --- /dev/null +++ b/vault/logical_cubbyhole.go @@ -0,0 +1,194 @@ +package vault + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +// CubbyholeBackendFactory constructs a new cubbyhole backend +func CubbyholeBackendFactory(conf *logical.BackendConfig) (logical.Backend, error) { + var b CubbyholeBackend + b.Backend = &framework.Backend{ + Help: strings.TrimSpace(cubbyholeHelp), + + Paths: []*framework.Path{ + &framework.Path{ + Pattern: ".*", + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.handleRead, + logical.WriteOperation: b.handleWrite, + logical.DeleteOperation: b.handleDelete, + logical.ListOperation: b.handleList, + }, + + HelpSynopsis: strings.TrimSpace(cubbyholeHelpSynopsis), + HelpDescription: strings.TrimSpace(cubbyholeHelpDescription), + }, + }, + } + + if conf == nil { + return nil, fmt.Errorf("Configuation passed into backend is nil") + } + b.Backend.Setup(conf) + + return b, nil +} + +// CubbyholeBackend is used for storing secrets directly into the physical +// backend. The secrets are encrypted in the durable storage. +// This differs from generic in that every token has its own private +// storage view. The view is removed when the token expires. +type CubbyholeBackend struct { + *framework.Backend +} + +func (b *CubbyholeBackend) revoke(saltedToken string, storageView logical.Storage) error { + if saltedToken == "" { + return fmt.Errorf("[ERR] cubbyhole: client token empty during revocation") + } + // Delete the entire tree in a stupid fashion for the moment + // to avoid changing the Storage interface + keys, err := storageView.List(saltedToken + "/") + if err != nil { + return err + } + + errors := []string{} + for _, key := range keys { + err = storageView.Delete(saltedToken + "/" + key) + if err != nil { + errors = append(errors, err.Error()) + } + } + + if len(errors) != 0 { + return fmt.Errorf("[ERR] cubbyhole: errors were encountered when deleting the tree for token %s: %s", saltedToken, strings.Join(errors, "; ")) + } + + return nil +} + +func (b *CubbyholeBackend) handleRead( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + if req.ClientToken == "" { + return nil, fmt.Errorf("[ERR] cubbyhole read: Client token empty") + } + + // Read the path + out, err := req.Storage.Get(req.ClientToken + "/" + req.Path) + if err != nil { + return nil, fmt.Errorf("read failed: %v", err) + } + + // Fast-path the no data case + if out == nil { + return nil, nil + } + + // Decode the data + var rawData map[string]interface{} + if err := json.Unmarshal(out.Value, &rawData); err != nil { + return nil, fmt.Errorf("json decoding failed: %v", err) + } + + // Generate the response + resp := &logical.Response{ + Data: rawData, + } + + return resp, nil +} + +func (b *CubbyholeBackend) handleWrite( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + if req.ClientToken == "" { + return nil, fmt.Errorf("[ERR] cubbyhole write: Client token empty") + } + // Check that some fields are given + if len(req.Data) == 0 { + return nil, fmt.Errorf("missing data fields") + } + + // JSON encode the data + buf, err := json.Marshal(req.Data) + if err != nil { + return nil, fmt.Errorf("json encoding failed: %v", err) + } + + // Write out a new key + entry := &logical.StorageEntry{ + Key: req.ClientToken + "/" + req.Path, + Value: buf, + } + if err := req.Storage.Put(entry); err != nil { + return nil, fmt.Errorf("failed to write: %v", err) + } + + return nil, nil +} + +func (b *CubbyholeBackend) handleDelete( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + if req.ClientToken == "" { + return nil, fmt.Errorf("[ERR] cubbyhole delete: Client token empty") + } + // Delete the key at the request path + if err := req.Storage.Delete(req.ClientToken + "/" + req.Path); err != nil { + return nil, err + } + + return nil, nil +} + +func (b *CubbyholeBackend) handleList( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + if req.ClientToken == "" { + return nil, fmt.Errorf("[ERR] cubbyhole list: Client token empty") + } + // List the keys at the prefix given by the request + keys, err := req.Storage.List(req.ClientToken + "/" + req.Path) + if err != nil { + return nil, err + } + + strippedKeys := []string{} + for _, key := range keys { + strippedKeys = append(strippedKeys, strings.TrimPrefix(key, req.ClientToken+"/")) + } + + // Generate the response + return logical.ListResponse(strippedKeys), nil +} + +const cubbyholeHelp = ` +The cubbyhole backend reads and writes arbitrary secrets to the backend. +The secrets are encrypted/decrypted by Vault: they are never stored +unencrypted in the backend and the backend never has an opportunity to +see the unencrypted value. + +This backend differs from the 'generic' backend in that it is namespaced +per-token. Tokens can only read and write their own values, with no +sharing possible (per-token cubbyholes). This can be useful for implementing +certain authentication workflows, as well as "scratch" areas for individual +clients. When the token is revoked, the entire set of stored values for that +token is also removed. +` + +const cubbyholeHelpSynopsis = ` +Pass-through secret storage to a token-specific cubbyhole in the storage +backend, allowing you to read/write arbitrary data into secret storage. +` + +const cubbyholeHelpDescription = ` +The cubbyhole backend reads and writes arbitrary data into secret storage, +encrypting it along the way. + +The view into the cubbyhole storage space is different for each token; it is +a per-token cubbyhole. When the token is revoked all values are removed. +` diff --git a/vault/logical_cubbyhole_test.go b/vault/logical_cubbyhole_test.go new file mode 100644 index 000000000..b38de1378 --- /dev/null +++ b/vault/logical_cubbyhole_test.go @@ -0,0 +1,256 @@ +package vault + +import ( + "reflect" + "sort" + "testing" + "time" + + "github.com/hashicorp/vault/helper/uuid" + "github.com/hashicorp/vault/logical" +) + +func TestCubbyholeBackend_RootPaths(t *testing.T) { + b := testCubbyholeBackend() + root := b.SpecialPaths() + if root != nil { + t.Fatalf("unexpected: %v", root) + } +} + +func TestCubbyholeBackend_Write(t *testing.T) { + b := testCubbyholeBackend() + req := logical.TestRequest(t, logical.WriteOperation, "foo") + clientToken := uuid.GenerateUUID() + req.ClientToken = clientToken + storage := req.Storage + req.Data["raw"] = "test" + + resp, err := b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil { + t.Fatalf("bad: %v", resp) + } + + req = logical.TestRequest(t, logical.ReadOperation, "foo") + req.Storage = storage + req.ClientToken = clientToken + _, err = b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } +} + +func TestCubbyholeBackend_Read(t *testing.T) { + b := testCubbyholeBackend() + req := logical.TestRequest(t, logical.WriteOperation, "foo") + req.Data["raw"] = "test" + storage := req.Storage + clientToken := uuid.GenerateUUID() + req.ClientToken = clientToken + + if _, err := b.HandleRequest(req); err != nil { + t.Fatalf("err: %v", err) + } + + req = logical.TestRequest(t, logical.ReadOperation, "foo") + req.Storage = storage + req.ClientToken = clientToken + + resp, err := b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + + expected := &logical.Response{ + Data: map[string]interface{}{ + "raw": "test", + }, + } + + if !reflect.DeepEqual(resp, expected) { + t.Fatalf("bad response.\n\nexpected: %#v\n\nGot: %#v", expected, resp) + } +} + +func TestCubbyholeBackend_Delete(t *testing.T) { + b := testCubbyholeBackend() + req := logical.TestRequest(t, logical.WriteOperation, "foo") + req.Data["raw"] = "test" + storage := req.Storage + clientToken := uuid.GenerateUUID() + req.ClientToken = clientToken + + if _, err := b.HandleRequest(req); err != nil { + t.Fatalf("err: %v", err) + } + + req = logical.TestRequest(t, logical.DeleteOperation, "foo") + req.Storage = storage + req.ClientToken = clientToken + resp, err := b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil { + t.Fatalf("bad: %v", resp) + } + + req = logical.TestRequest(t, logical.ReadOperation, "foo") + req.Storage = storage + req.ClientToken = clientToken + resp, err = b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil { + t.Fatalf("bad: %v", resp) + } +} + +func TestCubbyholeBackend_List(t *testing.T) { + b := testCubbyholeBackend() + req := logical.TestRequest(t, logical.WriteOperation, "foo") + clientToken := uuid.GenerateUUID() + req.Data["raw"] = "test" + req.ClientToken = clientToken + storage := req.Storage + + if _, err := b.HandleRequest(req); err != nil { + t.Fatalf("err: %v", err) + } + + req = logical.TestRequest(t, logical.WriteOperation, "bar") + req.Data["raw"] = "baz" + req.ClientToken = clientToken + req.Storage = storage + + if _, err := b.HandleRequest(req); err != nil { + t.Fatalf("err: %v", err) + } + + req = logical.TestRequest(t, logical.ListOperation, "") + req.Storage = storage + req.ClientToken = clientToken + resp, err := b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + + expKeys := []string{"foo", "bar"} + respKeys := resp.Data["keys"].([]string) + sort.Strings(expKeys) + sort.Strings(respKeys) + if !reflect.DeepEqual(respKeys, expKeys) { + t.Fatalf("bad response.\n\nexpected: %#v\n\nGot: %#v", expKeys, respKeys) + } +} + +func TestCubbyholeIsolation(t *testing.T) { + b := testCubbyholeBackend() + + clientTokenA := uuid.GenerateUUID() + clientTokenB := uuid.GenerateUUID() + var storageA logical.Storage + var storageB logical.Storage + + // Populate and test A entries + req := logical.TestRequest(t, logical.WriteOperation, "foo") + req.ClientToken = clientTokenA + storageA = req.Storage + req.Data["raw"] = "test" + + resp, err := b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil { + t.Fatalf("bad: %v", resp) + } + + req = logical.TestRequest(t, logical.ReadOperation, "foo") + req.Storage = storageA + req.ClientToken = clientTokenA + resp, err = b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + + expected := &logical.Response{ + Data: map[string]interface{}{ + "raw": "test", + }, + } + + if !reflect.DeepEqual(resp, expected) { + t.Fatalf("bad response.\n\nexpected: %#v\n\nGot: %#v", expected, resp) + } + + // Populate and test B entries + req = logical.TestRequest(t, logical.WriteOperation, "bar") + req.ClientToken = clientTokenB + storageB = req.Storage + req.Data["raw"] = "baz" + + resp, err = b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil { + t.Fatalf("bad: %v", resp) + } + + req = logical.TestRequest(t, logical.ReadOperation, "bar") + req.Storage = storageB + req.ClientToken = clientTokenB + resp, err = b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + + expected = &logical.Response{ + Data: map[string]interface{}{ + "raw": "baz", + }, + } + + if !reflect.DeepEqual(resp, expected) { + t.Fatalf("bad response.\n\nexpected: %#v\n\nGot: %#v", expected, resp) + } + + // We shouldn't be able to read A from B and vice versa + req = logical.TestRequest(t, logical.ReadOperation, "foo") + req.Storage = storageB + req.ClientToken = clientTokenB + resp, err = b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil { + t.Fatalf("err: was able to read from other user's cubbyhole") + } + + req = logical.TestRequest(t, logical.ReadOperation, "bar") + req.Storage = storageA + req.ClientToken = clientTokenA + resp, err = b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil { + t.Fatalf("err: was able to read from other user's cubbyhole") + } +} + +func testCubbyholeBackend() logical.Backend { + b, _ := CubbyholeBackendFactory(&logical.BackendConfig{ + Logger: nil, + System: logical.StaticSystemView{ + DefaultLeaseTTLVal: time.Hour * 24, + MaxLeaseTTLVal: time.Hour * 24 * 30, + }, + }) + return b +} diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index 7d79a88ba..08ddef704 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -59,6 +59,14 @@ func TestSystemBackend_mounts(t *testing.T) { "max_lease_ttl": resp.Data["sys/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(time.Duration), }, }, + "cubbyhole/": map[string]interface{}{ + "description": "per-token private secret storage", + "type": "cubbyhole", + "config": map[string]interface{}{ + "default_lease_ttl": resp.Data["cubbyhole/"].(map[string]interface{})["config"].(map[string]interface{})["default_lease_ttl"].(time.Duration), + "max_lease_ttl": resp.Data["cubbyhole/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(time.Duration), + }, + }, } if !reflect.DeepEqual(resp.Data, exp) { t.Fatalf("Got:\n%#v\nExpected:\n%#v", resp.Data, exp) diff --git a/vault/mount.go b/vault/mount.go index a96b8251a..8f5449cfc 100644 --- a/vault/mount.go +++ b/vault/mount.go @@ -37,6 +37,14 @@ var ( "audit/", "auth/", "sys/", + "cubbyhole/", + } + + // singletonMounts can only exist in one location and are + // loaded by default. These are types, not paths. + singletonMounts = []string{ + "cubbyhole", + "system", } ) @@ -158,6 +166,13 @@ func (c *Core) mount(me *MountEntry) error { } } + // Do not allow more than one instance of a singleton mount + for _, p := range singletonMounts { + if me.Type == p { + return logical.CodedError(403, fmt.Sprintf("Cannot mount more than one instance of '%s'", me.Type)) + } + } + // Verify there is no conflicting mount if match := c.router.MatchingMount(me.Path); match != "" { return logical.CodedError(409, fmt.Sprintf("existing mount at %s", match)) @@ -206,11 +221,18 @@ func (c *Core) unmount(path string) error { } // Verify exact match of the route - match := c.router.MatchingMount(path) - if match == "" || path != match { + match := c.router.MatchingMountEntry(path) + if match == nil || path != match.Path { return fmt.Errorf("no matching mount") } + // Do not allow singleton mounts to be removed + for _, p := range singletonMounts { + if match.Type == p { + return logical.CodedError(403, fmt.Sprintf("Cannot unmount backend of type '%s'", match.Type)) + } + } + // Store the view for this backend view := c.router.MatchingView(path) @@ -301,11 +323,18 @@ func (c *Core) remount(src, dst string) error { } // Verify exact match of the route - match := c.router.MatchingMount(src) - if match == "" || src != match { + match := c.router.MatchingMountEntry(src) + if match == nil || src != match.Path { return fmt.Errorf("no matching mount at '%s'", src) } + // Do not allow singleton mounts to be removed + for _, p := range singletonMounts { + if match.Type == p { + return logical.CodedError(403, fmt.Sprintf("Cannot remount backend of type '%s'", match.Type)) + } + } + if match := c.router.MatchingMount(dst); match != "" { return fmt.Errorf("existing mount at '%s'", match) } @@ -489,10 +518,7 @@ func (c *Core) newLogicalBackend(t string, sysView logical.SystemView, view logi StorageView: view, Logger: c.logger, Config: conf, - System: &logical.StaticSystemView{ - DefaultLeaseTTLVal: c.defaultLeaseTTL, - MaxLeaseTTLVal: c.maxLeaseTTL, - }, + System: sysView, } b, err := f(config) @@ -521,6 +547,12 @@ func defaultMountTable() *MountTable { Description: "generic secret storage", UUID: uuid.GenerateUUID(), } + cubbyholeMount := &MountEntry{ + Path: "cubbyhole/", + Type: "cubbyhole", + Description: "per-token private secret storage", + UUID: uuid.GenerateUUID(), + } sysMount := &MountEntry{ Path: "sys/", Type: "system", @@ -528,6 +560,7 @@ func defaultMountTable() *MountTable { UUID: uuid.GenerateUUID(), } table.Entries = append(table.Entries, genericMount) + table.Entries = append(table.Entries, cubbyholeMount) table.Entries = append(table.Entries, sysMount) return table } diff --git a/vault/mount_test.go b/vault/mount_test.go index 683282b42..30bbc7573 100644 --- a/vault/mount_test.go +++ b/vault/mount_test.go @@ -321,7 +321,7 @@ func TestDefaultMountTable(t *testing.T) { } func verifyDefaultTable(t *testing.T, table *MountTable) { - if len(table.Entries) != 2 { + if len(table.Entries) != 3 { t.Fatalf("bad: %v", table.Entries) } for idx, entry := range table.Entries { @@ -334,6 +334,13 @@ func verifyDefaultTable(t *testing.T, table *MountTable) { t.Fatalf("bad: %v", entry) } case 1: + if entry.Path != "cubbyhole/" { + t.Fatalf("bad: %v", entry) + } + if entry.Type != "cubbyhole" { + t.Fatalf("bad: %v", entry) + } + case 2: if entry.Path != "sys/" { t.Fatalf("bad: %v", entry) } diff --git a/vault/router.go b/vault/router.go index be601a6d0..8be6ed3fd 100644 --- a/vault/router.go +++ b/vault/router.go @@ -14,8 +14,11 @@ import ( // Router is used to do prefix based routing of a request to a logical backend type Router struct { - l sync.RWMutex - root *radix.Tree + l sync.RWMutex + root *radix.Tree + cubbyholeEntry *routeEntry + cubbyholeDestroyFunc func(string, logical.Storage) error + tokenStoreSalt *salt.Salt } // NewRouter returns a new router @@ -68,9 +71,19 @@ func (r *Router) Mount(backend logical.Backend, prefix string, mountEntry *Mount loginPaths: pathsToRadix(paths.Unauthenticated), } r.root.Insert(prefix, re) + + if mountEntry.Type == "cubbyhole" { + r.cubbyholeEntry = re + be := backend.(CubbyholeBackend) + r.cubbyholeDestroyFunc = be.revoke + } return nil } +func (r *Router) destroyCubbyhole(saltedID string) error { + return r.cubbyholeDestroyFunc(r.cubbyholeEntry.SaltID(saltedID), r.cubbyholeEntry.view) +} + // Unmount is used to remove a logical backend from a given prefix func (r *Router) Unmount(prefix string) error { r.l.Lock() @@ -214,7 +227,13 @@ func (r *Router) Route(req *logical.Request) (*logical.Response, error) { // Hash the request token unless this is the token backend clientToken := req.ClientToken - if !strings.HasPrefix(original, "auth/token/") { + switch { + case strings.HasPrefix(original, "auth/token/"): + case strings.HasPrefix(original, "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)) + default: req.ClientToken = re.SaltID(req.ClientToken) } diff --git a/vault/token_store.go b/vault/token_store.go index b84752304..5c1393ea1 100644 --- a/vault/token_store.go +++ b/vault/token_store.go @@ -44,6 +44,8 @@ type TokenStore struct { salt *salt.Salt expiration *ExpirationManager + + router *Router } // NewTokenStore is used to construct a token store that is @@ -54,7 +56,8 @@ func NewTokenStore(c *Core, config *logical.BackendConfig) (*TokenStore, error) // Initialize the store t := &TokenStore{ - view: view, + view: view, + router: c.router, } // Setup the salt @@ -63,6 +66,7 @@ func NewTokenStore(c *Core, config *logical.BackendConfig) (*TokenStore, error) return nil, err } t.salt = salt + t.router.tokenStoreSalt = salt // Setup the framework endpoints t.Backend = &framework.Backend{ @@ -366,6 +370,7 @@ func (ts *TokenStore) Revoke(id string) error { if id == "" { return fmt.Errorf("cannot revoke blank token") } + return ts.revokeSalted(ts.SaltID(id)) } @@ -398,6 +403,13 @@ func (ts *TokenStore) revokeSalted(saltedId string) error { return err } } + + // Destroy the cubby space + err = ts.router.destroyCubbyhole(saltedId) + if err != nil { + return err + } + return nil }