open-vault/vault/expiration.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

663 lines
18 KiB
Go

package vault
import (
"encoding/json"
"fmt"
"log"
"os"
"path"
"strings"
"sync"
"time"
"github.com/armon/go-metrics"
"github.com/hashicorp/vault/helper/uuid"
"github.com/hashicorp/vault/logical"
)
const (
// expirationSubPath is the sub-path used for the expiration manager
// view. This is nested under the system view.
expirationSubPath = "expire/"
// leaseViewPrefix is the prefix used for the ID based lookup of leases.
leaseViewPrefix = "id/"
// tokenViewPrefix is the prefix used for the token based lookup of leases.
tokenViewPrefix = "token/"
// maxRevokeAttempts limits how many revoke attempts are made
maxRevokeAttempts = 6
// revokeRetryBase is a baseline retry time
revokeRetryBase = 10 * time.Second
// minRevokeDelay is used to prevent an instant revoke on restore
minRevokeDelay = 5 * time.Second
// maxLeaseDuration is the default maximum lease duration
maxLeaseTTL = 30 * 24 * time.Hour
// defaultLeaseDuration is the default lease duration used when no lease is specified
defaultLeaseTTL = maxLeaseTTL
)
// ExpirationManager is used by the Core to manage leases. Secrets
// can provide a lease, meaning that they can be renewed or revoked.
// If a secret is not renewed in timely manner, it may be expired, and
// the ExpirationManager will handle doing automatic revocation.
type ExpirationManager struct {
router *Router
idView *BarrierView
tokenView *BarrierView
tokenStore *TokenStore
logger *log.Logger
pending map[string]*time.Timer
pendingLock sync.Mutex
}
// NewExpirationManager creates a new ExpirationManager that is backed
// using a given view, and uses the provided router for revocation.
func NewExpirationManager(router *Router, view *BarrierView, ts *TokenStore, logger *log.Logger) *ExpirationManager {
if logger == nil {
logger = log.New(os.Stderr, "", log.LstdFlags)
}
exp := &ExpirationManager{
router: router,
idView: view.SubView(leaseViewPrefix),
tokenView: view.SubView(tokenViewPrefix),
tokenStore: ts,
logger: logger,
pending: make(map[string]*time.Timer),
}
return exp
}
// setupExpiration is invoked after we've loaded the mount table to
// initialize the expiration manager
func (c *Core) setupExpiration() error {
// Create a sub-view
view := c.systemBarrierView.SubView(expirationSubPath)
// Create the manager
mgr := NewExpirationManager(c.router, view, c.tokenStore, c.logger)
c.expiration = mgr
// Link the token store to this
c.tokenStore.SetExpirationManager(mgr)
// Restore the existing state
if err := c.expiration.Restore(); err != nil {
return fmt.Errorf("expiration state restore failed: %v", err)
}
return nil
}
// stopExpiration is used to stop the expiration manager before
// sealing the Vault.
func (c *Core) stopExpiration() error {
if c.expiration != nil {
if err := c.expiration.Stop(); err != nil {
return err
}
c.expiration = nil
}
return nil
}
// Restore is used to recover the lease states when starting.
// This is used after starting the vault.
func (m *ExpirationManager) Restore() error {
m.pendingLock.Lock()
defer m.pendingLock.Unlock()
// Accumulate existing leases
existing, err := CollectKeys(m.idView)
if err != nil {
return fmt.Errorf("failed to scan for leases: %v", err)
}
// Restore each key
for _, leaseID := range existing {
// Load the entry
le, err := m.loadEntry(leaseID)
if err != nil {
return err
}
// If there is no entry, nothing to restore
if le == nil {
continue
}
// If there is no expiry time, don't do anything
if le.ExpireTime.IsZero() {
continue
}
// Determine the remaining time to expiration
expires := le.ExpireTime.Sub(time.Now().UTC())
if expires <= 0 {
expires = minRevokeDelay
}
// Setup revocation timer
m.pending[le.LeaseID] = time.AfterFunc(expires, func() {
m.expireID(le.LeaseID)
})
}
if len(m.pending) > 0 {
m.logger.Printf("[INFO] expire: restored %d leases", len(m.pending))
}
return nil
}
// Stop is used to prevent further automatic revocations.
// This must be called before sealing the view.
func (m *ExpirationManager) Stop() error {
// Stop all the pending expiration timers
m.pendingLock.Lock()
for _, timer := range m.pending {
timer.Stop()
}
m.pending = make(map[string]*time.Timer)
m.pendingLock.Unlock()
return nil
}
// Revoke is used to revoke a secret named by the given LeaseID
func (m *ExpirationManager) Revoke(leaseID string) error {
defer metrics.MeasureSince([]string{"expire", "revoke"}, time.Now())
// Load the entry
le, err := m.loadEntry(leaseID)
if err != nil {
return err
}
// If there is no entry, nothing to revoke
if le == nil {
return nil
}
// Revoke the entry
if err := m.revokeEntry(le); err != nil {
return err
}
// Delete the entry
if err := m.deleteEntry(leaseID); err != nil {
return err
}
// Delete the secondary index
if err := m.indexByToken(le.ClientToken, le.LeaseID); err != nil {
return err
}
// Clear the expiration handler
m.pendingLock.Lock()
if timer, ok := m.pending[leaseID]; ok {
timer.Stop()
delete(m.pending, leaseID)
}
m.pendingLock.Unlock()
return nil
}
// RevokePrefix is used to revoke all secrets with a given prefix.
// The prefix maps to that of the mount table to make this simpler
// to reason about.
func (m *ExpirationManager) RevokePrefix(prefix string) error {
defer metrics.MeasureSince([]string{"expire", "revoke-prefix"}, time.Now())
// Ensure there is a trailing slash
if !strings.HasSuffix(prefix, "/") {
prefix = prefix + "/"
}
// Accumulate existing leases
sub := m.idView.SubView(prefix)
existing, err := CollectKeys(sub)
if err != nil {
return fmt.Errorf("failed to scan for leases: %v", err)
}
// Revoke all the keys
for idx, suffix := range existing {
leaseID := prefix + suffix
if err := m.Revoke(leaseID); err != nil {
return fmt.Errorf("failed to revoke '%s' (%d / %d): %v",
leaseID, idx+1, len(existing), err)
}
}
return nil
}
// RevokeByToken is used to revoke all the secrets issued with
// a given token. This is done by using the secondary index.
func (m *ExpirationManager) RevokeByToken(token string) error {
defer metrics.MeasureSince([]string{"expire", "revoke-by-token"}, time.Now())
// Lookup the leases
existing, err := m.lookupByToken(token)
if err != nil {
return fmt.Errorf("failed to scan for leases: %v", err)
}
// Revoke all the keys
for idx, leaseID := range existing {
if err := m.Revoke(leaseID); err != nil {
return fmt.Errorf("failed to revoke '%s' (%d / %d): %v",
leaseID, idx+1, len(existing), err)
}
}
return nil
}
// Renew is used to renew a secret using the given leaseID
// and a renew interval. The increment may be ignored.
func (m *ExpirationManager) Renew(leaseID string, increment time.Duration) (*logical.Response, error) {
defer metrics.MeasureSince([]string{"expire", "renew"}, time.Now())
// Load the entry
le, err := m.loadEntry(leaseID)
if err != nil {
return nil, err
}
// Check if the lease is renewable
if err := le.renewable(); err != nil {
return nil, err
}
// Attempt to renew the entry
resp, err := m.renewEntry(le, increment)
if err != nil {
return nil, err
}
// Fast-path if there is no lease
if resp == nil || resp.Secret == nil || !resp.Secret.LeaseEnabled() {
return resp, nil
}
// Validate the lease
if err := resp.Secret.Validate(); err != nil {
return nil, err
}
// Attach the LeaseID
resp.Secret.LeaseID = leaseID
// Update the lease entry
le.Data = resp.Data
le.Secret = resp.Secret
le.ExpireTime = resp.Secret.ExpirationTime()
if err := m.persistEntry(le); err != nil {
return nil, err
}
// Update the expiration time
m.updatePending(le, resp.Secret.LeaseTotal())
// Return the response
return resp, nil
}
// RenewToken is used to renew a token which does not need to
// invoke a logical backend.
func (m *ExpirationManager) RenewToken(source string, token string,
increment time.Duration) (*logical.Auth, error) {
defer metrics.MeasureSince([]string{"expire", "renew-token"}, time.Now())
// Compute the Lease ID
leaseID := path.Join(source, m.tokenStore.SaltID(token))
// Load the entry
le, err := m.loadEntry(leaseID)
if err != nil {
return nil, err
}
// Check if the lease is renewable
if err := le.renewable(); err != nil {
return nil, err
}
// Attempt to renew the auth entry
resp, err := m.renewAuthEntry(le, increment)
if err != nil {
return nil, err
}
// Fast-path if there is no renewal
if resp == nil {
return nil, nil
}
if resp.Auth == nil || !resp.Auth.LeaseEnabled() {
return resp.Auth, nil
}
// Attach the ClientToken
resp.Auth.ClientToken = token
resp.Auth.Increment = 0
// Update the lease entry
le.Auth = resp.Auth
le.ExpireTime = resp.Auth.ExpirationTime()
if err := m.persistEntry(le); err != nil {
return nil, err
}
// Update the expiration time
m.updatePending(le, resp.Auth.LeaseTotal())
return resp.Auth, nil
}
// Register is used to take a request and response with an associated
// lease. The secret gets assigned a LeaseID and the management of
// of lease is assumed by the expiration manager.
func (m *ExpirationManager) Register(req *logical.Request, resp *logical.Response) (string, error) {
defer metrics.MeasureSince([]string{"expire", "register"}, time.Now())
// Ignore if there is no leased secret
if resp == nil || resp.Secret == nil {
return "", nil
}
// Validate the secret
if err := resp.Secret.Validate(); err != nil {
return "", err
}
// Create a lease entry
le := leaseEntry{
LeaseID: path.Join(req.Path, uuid.GenerateUUID()),
ClientToken: req.ClientToken,
Path: req.Path,
Data: resp.Data,
Secret: resp.Secret,
IssueTime: time.Now().UTC(),
ExpireTime: resp.Secret.ExpirationTime(),
}
// Encode the entry
if err := m.persistEntry(&le); err != nil {
return "", err
}
// Maintain secondary index by token
if err := m.indexByToken(le.ClientToken, le.LeaseID); err != nil {
return "", err
}
// Setup revocation timer if there is a lease
m.updatePending(&le, resp.Secret.LeaseTotal())
// Done
return le.LeaseID, nil
}
// RegisterAuth is used to take an Auth response with an associated lease.
// The token does not get a LeaseID, but the lease management is handled by
// the expiration manager.
func (m *ExpirationManager) RegisterAuth(source string, auth *logical.Auth) error {
defer metrics.MeasureSince([]string{"expire", "register-auth"}, time.Now())
// Create a lease entry
le := leaseEntry{
LeaseID: path.Join(source, m.tokenStore.SaltID(auth.ClientToken)),
ClientToken: auth.ClientToken,
Auth: auth,
Path: source,
IssueTime: time.Now().UTC(),
ExpireTime: auth.ExpirationTime(),
}
// Encode the entry
if err := m.persistEntry(&le); err != nil {
return err
}
// Setup revocation timer
m.updatePending(&le, auth.LeaseTotal())
return nil
}
// updatePending is used to update a pending invocation for a lease
func (m *ExpirationManager) updatePending(le *leaseEntry, leaseTotal time.Duration) {
m.pendingLock.Lock()
defer m.pendingLock.Unlock()
// Check for an existing timer
timer, ok := m.pending[le.LeaseID]
// Create entry if it does not exist
if !ok && leaseTotal > 0 {
timer := time.AfterFunc(leaseTotal, func() {
m.expireID(le.LeaseID)
})
m.pending[le.LeaseID] = timer
return
}
// Delete the timer if the expiration time is zero
if ok && leaseTotal == 0 {
timer.Stop()
delete(m.pending, le.LeaseID)
return
}
// Extend the timer by the lease total
if ok && leaseTotal > 0 {
timer.Reset(leaseTotal)
}
}
// expireID is invoked when a given ID is expired
func (m *ExpirationManager) expireID(leaseID string) {
// Clear from the pending expiration
m.pendingLock.Lock()
delete(m.pending, leaseID)
m.pendingLock.Unlock()
for attempt := uint(0); attempt < maxRevokeAttempts; attempt++ {
err := m.Revoke(leaseID)
if err == nil {
m.logger.Printf("[INFO] expire: revoked '%s'", leaseID)
return
}
m.logger.Printf("[ERR] expire: failed to revoke '%s': %v", leaseID, err)
time.Sleep((1 << attempt) * revokeRetryBase)
}
m.logger.Printf("[ERR] expire: maximum revoke attempts for '%s' reached", leaseID)
}
// revokeEntry is used to attempt revocation of an internal entry
func (m *ExpirationManager) revokeEntry(le *leaseEntry) error {
// Revocation of login tokens is special since we can by-pass the
// backend and directly interact with the token store
if le.Auth != nil {
if err := m.tokenStore.RevokeTree(le.Auth.ClientToken); err != nil {
return fmt.Errorf("failed to revoke token: %v", err)
}
return nil
}
// Handle standard revocation via backends
_, err := m.router.Route(logical.RevokeRequest(
le.Path, le.Secret, le.Data))
if err != nil {
return fmt.Errorf("failed to revoke entry: %v", err)
}
return nil
}
// renewEntry is used to attempt renew of an internal entry
func (m *ExpirationManager) renewEntry(le *leaseEntry, increment time.Duration) (*logical.Response, error) {
secret := *le.Secret
secret.IssueTime = le.IssueTime
secret.Increment = increment
secret.LeaseID = ""
req := logical.RenewRequest(le.Path, &secret, le.Data)
resp, err := m.router.Route(req)
if err != nil {
return nil, fmt.Errorf("failed to renew entry: %v", err)
}
return resp, nil
}
// renewAuthEntry is used to attempt renew of an auth entry
func (m *ExpirationManager) renewAuthEntry(le *leaseEntry, increment time.Duration) (*logical.Response, error) {
auth := *le.Auth
auth.IssueTime = le.IssueTime
auth.Increment = increment
auth.ClientToken = ""
req := logical.RenewAuthRequest(le.Path, &auth, nil)
resp, err := m.router.Route(req)
if err != nil {
return nil, fmt.Errorf("failed to renew entry: %v", err)
}
return resp, nil
}
// loadEntry is used to read a lease entry
func (m *ExpirationManager) loadEntry(leaseID string) (*leaseEntry, error) {
out, err := m.idView.Get(leaseID)
if err != nil {
return nil, fmt.Errorf("failed to read lease entry: %v", err)
}
if out == nil {
return nil, nil
}
le, err := decodeLeaseEntry(out.Value)
if err != nil {
return nil, fmt.Errorf("failed to decode lease entry: %v", err)
}
return le, nil
}
// persistEntry is used to persist a lease entry
func (m *ExpirationManager) persistEntry(le *leaseEntry) error {
// Encode the entry
buf, err := le.encode()
if err != nil {
return fmt.Errorf("failed to encode lease entry: %v", err)
}
// Write out to the view
ent := logical.StorageEntry{
Key: le.LeaseID,
Value: buf,
}
if err := m.idView.Put(&ent); err != nil {
return fmt.Errorf("failed to persist lease entry: %v", err)
}
return nil
}
// deleteEntry is used to delete a lease entry
func (m *ExpirationManager) deleteEntry(leaseID string) error {
if err := m.idView.Delete(leaseID); err != nil {
return fmt.Errorf("failed to delete lease entry: %v", err)
}
return nil
}
// indexByToken creates a secondary index from the token to a lease entry
func (m *ExpirationManager) indexByToken(token, leaseID string) error {
ent := logical.StorageEntry{
Key: m.tokenStore.SaltID(token) + "/" + m.tokenStore.SaltID(leaseID),
Value: []byte(leaseID),
}
if err := m.tokenView.Put(&ent); err != nil {
return fmt.Errorf("failed to persist lease index entry: %v", err)
}
return nil
}
// removeIndexByToken removes the secondary index from the token to a lease entry
func (m *ExpirationManager) removeIndexByToken(token, leaseID string) error {
key := m.tokenStore.SaltID(token) + "/" + m.tokenStore.SaltID(leaseID)
if err := m.tokenView.Delete(key); err != nil {
return fmt.Errorf("failed to delete lease index entry: %v", err)
}
return nil
}
// lookupByToken is used to lookup all the leaseID's via the
func (m *ExpirationManager) lookupByToken(token string) ([]string, error) {
// Scan via the index for sub-leases
prefix := m.tokenStore.SaltID(token) + "/"
subKeys, err := m.tokenView.List(prefix)
if err != nil {
return nil, fmt.Errorf("failed to list leases: %v", err)
}
// Read each index entry
leaseIDs := make([]string, 0, len(subKeys))
for _, sub := range subKeys {
out, err := m.tokenView.Get(prefix + sub)
if err != nil {
return nil, fmt.Errorf("failed to read lease index: %v", err)
}
if out == nil {
continue
}
leaseIDs = append(leaseIDs, string(out.Value))
}
return leaseIDs, nil
}
// emitMetrics is invoked periodically to emit statistics
func (m *ExpirationManager) emitMetrics() {
m.pendingLock.Lock()
num := len(m.pending)
m.pendingLock.Unlock()
metrics.SetGauge([]string{"expire", "num_leases"}, float32(num))
}
// leaseEntry is used to structure the values the expiration
// manager stores. This is used to handle renew and revocation.
type leaseEntry struct {
LeaseID string `json:"lease_id"`
ClientToken string `json:"client_token"`
Path string `json:"path"`
Data map[string]interface{} `json:"data"`
Secret *logical.Secret `json:"secret"`
Auth *logical.Auth `json:"auth"`
IssueTime time.Time `json:"issue_time"`
ExpireTime time.Time `json:"expire_time"`
}
// encode is used to JSON encode the lease entry
func (l *leaseEntry) encode() ([]byte, error) {
return json.Marshal(l)
}
func (le *leaseEntry) renewable() error {
// If there is no entry, cannot review
if le == nil || le.ExpireTime.IsZero() {
return fmt.Errorf("lease not found or lease is not renewable")
}
// Determine if the lease is expired
if le.ExpireTime.Before(time.Now().UTC()) {
return fmt.Errorf("lease expired")
}
// Determine if the lease is renewable
if le.Secret != nil && !le.Secret.Renewable {
return fmt.Errorf("lease is not renewable")
}
if le.Auth != nil && !le.Auth.Renewable {
return fmt.Errorf("lease is not renewable")
}
return nil
}
// decodeLeaseEntry is used to reverse encode and return a new entry
func decodeLeaseEntry(buf []byte) (*leaseEntry, error) {
out := new(leaseEntry)
return out, json.Unmarshal(buf, out)
}