open-vault/vault/mount.go
Jeff Mitchell 77e7379ab5 Implement the cubbyhole backend
In order to implement this efficiently, I have introduced the concept of
"singleton" backends -- currently, 'sys' and 'cubbyhole'. There isn't
much reason to allow sys to be mounted at multiple places, and there
isn't much reason you'd need multiple per-token storage areas. By
restricting it to just one, I can store that particular mount instead of
iterating through them in order to call the appropriate revoke function.

Additionally, because revocation on the backend needs to be triggered by
the token store, the token store's salt is kept in the router and
client tokens going to the cubbyhole backend are double-salted by the
router. This allows the token store to drive when revocation happens
using its salted tokens.
2015-09-15 13:50:37 -04:00

567 lines
15 KiB
Go

package vault
import (
"crypto/sha1"
"encoding/json"
"errors"
"fmt"
"strings"
"sync"
"time"
"github.com/hashicorp/vault/helper/uuid"
"github.com/hashicorp/vault/logical"
)
const (
// coreMountConfigPath is used to store the mount configuration.
// Mounts are protected within the Vault itself, which means they
// can only be viewed or modified after an unseal.
coreMountConfigPath = "core/mounts"
// backendBarrierPrefix is the prefix to the UUID used in the
// barrier view for the backends.
backendBarrierPrefix = "logical/"
// systemBarrierPrefix is the prefix used for the
// system logical backend.
systemBarrierPrefix = "sys/"
)
var (
// loadMountsFailed if loadMounts encounters an error
errLoadMountsFailed = errors.New("failed to setup mount table")
// protectedMounts cannot be remounted
protectedMounts = []string{
"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",
}
)
// MountTable is used to represent the internal mount table
type MountTable struct {
// This lock should be held whenever modifying the Entries field.
sync.RWMutex
Entries []*MountEntry `json:"entries"`
}
// ShallowClone returns a copy of the mount table that
// keeps the MountEntry locations, so as not to invalidate
// other locations holding pointers. Care needs to be taken
// if modifying entries rather than modifying the table itself
func (t *MountTable) ShallowClone() *MountTable {
mt := &MountTable{
Entries: make([]*MountEntry, len(t.Entries)),
}
for i, e := range t.Entries {
mt.Entries[i] = e
}
return mt
}
// Hash is used to generate a hash value for the mount table
func (t *MountTable) Hash() ([]byte, error) {
buf, err := json.Marshal(t)
if err != nil {
return nil, err
}
hash := sha1.Sum(buf)
return hash[:], nil
}
// Find is used to lookup an entry
func (t *MountTable) Find(path string) *MountEntry {
n := len(t.Entries)
for i := 0; i < n; i++ {
if t.Entries[i].Path == path {
return t.Entries[i]
}
}
return nil
}
// SetTaint is used to set the taint on given entry
func (t *MountTable) SetTaint(path string, value bool) bool {
n := len(t.Entries)
for i := 0; i < n; i++ {
if t.Entries[i].Path == path {
t.Entries[i].Tainted = value
return true
}
}
return false
}
// Remove is used to remove a given path entry
func (t *MountTable) Remove(path string) bool {
n := len(t.Entries)
for i := 0; i < n; i++ {
if t.Entries[i].Path == path {
t.Entries[i], t.Entries[n-1] = t.Entries[n-1], nil
t.Entries = t.Entries[:n-1]
return true
}
}
return false
}
// MountEntry is used to represent a mount table entry
type MountEntry struct {
Path string `json:"path"` // Mount Path
Type string `json:"type"` // Logical backend Type
Description string `json:"description"` // User-provided description
UUID string `json:"uuid"` // Barrier view UUID
Config MountConfig `json:"config"` // Configuration related to this mount (but not backend-derived)
Options map[string]string `json:"options"` // Backend options
Tainted bool `json:"tainted,omitempty"` // Set as a Write-Ahead flag for unmount/remount
}
// MountConfig is used to hold settable options
type MountConfig struct {
DefaultLeaseTTL time.Duration `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` // Override for global default
MaxLeaseTTL time.Duration `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` // Override for global default
}
// Returns a deep copy of the mount entry
func (e *MountEntry) Clone() *MountEntry {
optClone := make(map[string]string)
for k, v := range e.Options {
optClone[k] = v
}
return &MountEntry{
Path: e.Path,
Type: e.Type,
Description: e.Description,
UUID: e.UUID,
Config: e.Config,
Options: optClone,
}
}
// Mount is used to mount a new backend to the mount table.
func (c *Core) mount(me *MountEntry) error {
c.mounts.Lock()
defer c.mounts.Unlock()
// Ensure we end the path in a slash
if !strings.HasSuffix(me.Path, "/") {
me.Path += "/"
}
// Prevent protected paths from being mounted
for _, p := range protectedMounts {
if strings.HasPrefix(me.Path, p) {
return logical.CodedError(403, fmt.Sprintf("cannot mount '%s'", me.Path))
}
}
// 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))
}
// Generate a new UUID and view
me.UUID = uuid.GenerateUUID()
view := NewBarrierView(c.barrier, backendBarrierPrefix+me.UUID+"/")
backend, err := c.newLogicalBackend(me.Type, c.mountEntrySysView(me), view, nil)
if err != nil {
return err
}
// Update the mount table
newTable := c.mounts.ShallowClone()
newTable.Entries = append(newTable.Entries, me)
if err := c.persistMounts(newTable); err != nil {
return errors.New("failed to update mount table")
}
c.mounts = newTable
// Mount the backend
if err := c.router.Mount(backend, me.Path, me, view); err != nil {
return err
}
c.logger.Printf("[INFO] core: mounted '%s' type: %s", me.Path, me.Type)
return nil
}
// Unmount is used to unmount a path.
func (c *Core) unmount(path string) error {
c.mounts.Lock()
defer c.mounts.Unlock()
// Ensure we end the path in a slash
if !strings.HasSuffix(path, "/") {
path += "/"
}
// Prevent protected paths from being unmounted
for _, p := range protectedMounts {
if strings.HasPrefix(path, p) {
return fmt.Errorf("cannot unmount '%s'", path)
}
}
// Verify exact match of the route
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)
// Mark the entry as tainted
if err := c.taintMountEntry(path); err != nil {
return err
}
// Taint the router path to prevent routing
if err := c.router.Taint(path); err != nil {
return err
}
// Invoke the rollback manager a final time
if err := c.rollback.Rollback(path); err != nil {
return err
}
// Revoke all the dynamic keys
if err := c.expiration.RevokePrefix(path); err != nil {
return err
}
// Unmount the backend entirely
if err := c.router.Unmount(path); err != nil {
return err
}
// Clear the data in the view
if err := ClearView(view); err != nil {
return err
}
// Remove the mount table entry
if err := c.removeMountEntry(path); err != nil {
return err
}
c.logger.Printf("[INFO] core: unmounted '%s'", path)
return nil
}
// removeMountEntry is used to remove an entry from the mount table
func (c *Core) removeMountEntry(path string) error {
// Remove the entry from the mount table
newTable := c.mounts.ShallowClone()
newTable.Remove(path)
// Update the mount table
if err := c.persistMounts(newTable); err != nil {
return errors.New("failed to update mount table")
}
c.mounts = newTable
return nil
}
// taintMountEntry is used to mark an entry in the mount table as tainted
func (c *Core) taintMountEntry(path string) error {
// Remove the entry from the mount table
newTable := c.mounts.ShallowClone()
newTable.SetTaint(path, true)
// Update the mount table
if err := c.persistMounts(newTable); err != nil {
return errors.New("failed to update mount table")
}
c.mounts = newTable
return nil
}
// Remount is used to remount a path at a new mount point.
func (c *Core) remount(src, dst string) error {
c.mounts.Lock()
defer c.mounts.Unlock()
// Ensure we end the path in a slash
if !strings.HasSuffix(src, "/") {
src += "/"
}
if !strings.HasSuffix(dst, "/") {
dst += "/"
}
// Prevent protected paths from being remounted
for _, p := range protectedMounts {
if strings.HasPrefix(src, p) {
return fmt.Errorf("cannot remount '%s'", src)
}
}
// Verify exact match of the route
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)
}
// Mark the entry as tainted
if err := c.taintMountEntry(src); err != nil {
return err
}
// Taint the router path to prevent routing
if err := c.router.Taint(src); err != nil {
return err
}
// Invoke the rollback manager a final time
if err := c.rollback.Rollback(src); err != nil {
return err
}
// Revoke all the dynamic keys
if err := c.expiration.RevokePrefix(src); err != nil {
return err
}
// Update the entry in the mount table
newTable := c.mounts.ShallowClone()
var ent *MountEntry
for _, ent = range newTable.Entries {
if ent.Path == src {
ent.Path = dst
ent.Tainted = false
break
}
}
// Update the mount table
if err := c.persistMounts(newTable); err != nil {
ent.Path = src
ent.Tainted = true
return errors.New("failed to update mount table")
}
c.mounts = newTable
// Remount the backend
if err := c.router.Remount(src, dst); err != nil {
return err
}
// Un-taint the path
if err := c.router.Untaint(dst); err != nil {
return err
}
c.logger.Printf("[INFO] core: remounted '%s' to '%s'", src, dst)
return nil
}
// loadMounts is invoked as part of postUnseal to load the mount table
func (c *Core) loadMounts() error {
// Load the existing mount table
raw, err := c.barrier.Get(coreMountConfigPath)
if err != nil {
c.logger.Printf("[ERR] core: failed to read mount table: %v", err)
return errLoadMountsFailed
}
if raw != nil {
c.mounts = &MountTable{}
if err := json.Unmarshal(raw.Value, c.mounts); err != nil {
c.logger.Printf("[ERR] core: failed to decode mount table: %v", err)
return errLoadMountsFailed
}
}
// Done if we have restored the mount table
if c.mounts != nil {
return nil
}
// Create and persist the default mount table
c.mounts = defaultMountTable()
if err := c.persistMounts(c.mounts); err != nil {
return errLoadMountsFailed
}
return nil
}
// persistMounts is used to persist the mount table after modification
func (c *Core) persistMounts(table *MountTable) error {
// Marshal the table
raw, err := json.Marshal(table)
if err != nil {
c.logger.Printf("[ERR] core: failed to encode mount table: %v", err)
return err
}
// Create an entry
entry := &Entry{
Key: coreMountConfigPath,
Value: raw,
}
// Write to the physical backend
if err := c.barrier.Put(entry); err != nil {
c.logger.Printf("[ERR] core: failed to persist mount table: %v", err)
return err
}
return nil
}
// setupMounts is invoked after we've loaded the mount table to
// initialize the logical backends and setup the router
func (c *Core) setupMounts() error {
var backend logical.Backend
var view *BarrierView
var err error
for _, entry := range c.mounts.Entries {
// Initialize the backend, special casing for system
barrierPath := backendBarrierPrefix + entry.UUID + "/"
if entry.Type == "system" {
barrierPath = systemBarrierPrefix
}
// Create a barrier view using the UUID
view = NewBarrierView(c.barrier, barrierPath)
// Initialize the backend
// Create the new backend
backend, err = c.newLogicalBackend(entry.Type, c.mountEntrySysView(entry), view, nil)
if err != nil {
c.logger.Printf(
"[ERR] core: failed to create mount entry %#v: %v",
entry, err)
return errLoadMountsFailed
}
if entry.Type == "system" {
c.systemBarrierView = view
}
// Mount the backend
err = c.router.Mount(backend, entry.Path, entry, view)
if err != nil {
c.logger.Printf("[ERR] core: failed to mount entry %#v: %v", entry, err)
return errLoadMountsFailed
}
// Ensure the path is tainted if set in the mount table
if entry.Tainted {
c.router.Taint(entry.Path)
}
}
return nil
}
// unloadMounts is used before we seal the vault to reset the mounts to
// their unloaded state, calling Cleanup if defined. This is reversed by load and setup mounts.
func (c *Core) unloadMounts() error {
if c.mounts != nil {
for _, e := range c.mounts.Entries {
prefix := e.Path
b, ok := c.router.root.Get(prefix)
if ok {
b.(*routeEntry).backend.Cleanup()
}
}
}
c.mounts = nil
c.router = NewRouter()
c.systemBarrierView = nil
return nil
}
// newLogicalBackend is used to create and configure a new logical backend by name
func (c *Core) newLogicalBackend(t string, sysView logical.SystemView, view logical.Storage, conf map[string]string) (logical.Backend, error) {
f, ok := c.logicalBackends[t]
if !ok {
return nil, fmt.Errorf("unknown backend type: %s", t)
}
config := &logical.BackendConfig{
StorageView: view,
Logger: c.logger,
Config: conf,
System: sysView,
}
b, err := f(config)
if err != nil {
return nil, err
}
return b, nil
}
// mountEntrySysView creates a logical.SystemView from global and
// mount-specific entries; because this should be called when setting
// up a mountEntry, it doesn't check to ensure that me is not nil
func (c *Core) mountEntrySysView(me *MountEntry) logical.SystemView {
return dynamicSystemView{
core: c,
mountEntry: me,
}
}
// defaultMountTable creates a default mount table
func defaultMountTable() *MountTable {
table := &MountTable{}
genericMount := &MountEntry{
Path: "secret/",
Type: "generic",
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",
Description: "system endpoints used for control, policy and debugging",
UUID: uuid.GenerateUUID(),
}
table.Entries = append(table.Entries, genericMount)
table.Entries = append(table.Entries, cubbyholeMount)
table.Entries = append(table.Entries, sysMount)
return table
}