mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 19:17:58 +00:00
VAULT-19233 First part of caching static secrets work (#23054)
* VAULT-19233 First part of caching static secrets work * VAULT-19233 update godoc * VAULT-19233 invalidate cache on non-GET * VAULT-19233 add locking to proxy cache writes * VAULT-19233 update locking, future-proof * VAULT-19233 fix mutex * VAULT-19233 Use ParseSecret
This commit is contained in:
@@ -39,6 +39,9 @@ const (
|
||||
// TokenType - Bucket/type for auto-auth tokens
|
||||
TokenType = "token"
|
||||
|
||||
// StaticSecretType - Bucket/type for static secrets
|
||||
StaticSecretType = "static-secret"
|
||||
|
||||
// LeaseType - v2 Bucket/type for auth AND secret leases.
|
||||
//
|
||||
// This bucket stores keys in the same order they were created using
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -22,6 +23,12 @@ type Index struct {
|
||||
// Required: true, Unique: true
|
||||
Token string
|
||||
|
||||
// Tokens is a list of tokens that can access this cached response,
|
||||
// which is used for static secret caching, and enabling multiple
|
||||
// tokens to be able to access the same cache entry for static secrets.
|
||||
// Required: false, Unique: false
|
||||
Tokens []string
|
||||
|
||||
// TokenParent is the parent token of the token held by this index
|
||||
// Required: false, Unique: false
|
||||
TokenParent string
|
||||
@@ -71,6 +78,10 @@ type Index struct {
|
||||
|
||||
// Type is the index type (token, auth-lease, secret-lease)
|
||||
Type string
|
||||
|
||||
// IndexLock is a lock held for some indexes to prevent data
|
||||
// races upon update.
|
||||
IndexLock sync.Mutex
|
||||
}
|
||||
|
||||
type IndexName uint32
|
||||
|
||||
@@ -17,6 +17,7 @@ func TestSerializeDeserialize(t *testing.T) {
|
||||
testIndex := &Index{
|
||||
ID: "testid",
|
||||
Token: "testtoken",
|
||||
Tokens: []string{"token1", "token2"},
|
||||
TokenParent: "parent token",
|
||||
TokenAccessor: "test accessor",
|
||||
Namespace: "test namespace",
|
||||
|
||||
286
command/agentproxyshared/cache/lease_cache.go
vendored
286
command/agentproxyshared/cache/lease_cache.go
vendored
@@ -12,9 +12,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -99,16 +99,21 @@ type LeaseCache struct {
|
||||
// shuttingDown is used to determine if cache needs to be evicted or not
|
||||
// when the context is cancelled
|
||||
shuttingDown atomic.Bool
|
||||
|
||||
// cacheStaticSecrets is used to determine if the cache should also
|
||||
// cache static secrets, as well as dynamic secrets.
|
||||
cacheStaticSecrets bool
|
||||
}
|
||||
|
||||
// LeaseCacheConfig is the configuration for initializing a new
|
||||
// Lease.
|
||||
// LeaseCache.
|
||||
type LeaseCacheConfig struct {
|
||||
Client *api.Client
|
||||
BaseContext context.Context
|
||||
Proxier Proxier
|
||||
Logger hclog.Logger
|
||||
Storage *cacheboltdb.BoltStorage
|
||||
Client *api.Client
|
||||
BaseContext context.Context
|
||||
Proxier Proxier
|
||||
Logger hclog.Logger
|
||||
Storage *cacheboltdb.BoltStorage
|
||||
CacheStaticSecrets bool
|
||||
}
|
||||
|
||||
type inflightRequest struct {
|
||||
@@ -151,15 +156,16 @@ func NewLeaseCache(conf *LeaseCacheConfig) (*LeaseCache, error) {
|
||||
baseCtxInfo := cachememdb.NewContextInfo(conf.BaseContext)
|
||||
|
||||
return &LeaseCache{
|
||||
client: conf.Client,
|
||||
proxier: conf.Proxier,
|
||||
logger: conf.Logger,
|
||||
db: db,
|
||||
baseCtxInfo: baseCtxInfo,
|
||||
l: &sync.RWMutex{},
|
||||
idLocks: locksutil.CreateLocks(),
|
||||
inflightCache: gocache.New(gocache.NoExpiration, gocache.NoExpiration),
|
||||
ps: conf.Storage,
|
||||
client: conf.Client,
|
||||
proxier: conf.Proxier,
|
||||
logger: conf.Logger,
|
||||
db: db,
|
||||
baseCtxInfo: baseCtxInfo,
|
||||
l: &sync.RWMutex{},
|
||||
idLocks: locksutil.CreateLocks(),
|
||||
inflightCache: gocache.New(gocache.NoExpiration, gocache.NoExpiration),
|
||||
ps: conf.Storage,
|
||||
cacheStaticSecrets: conf.CacheStaticSecrets,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -180,9 +186,43 @@ func (c *LeaseCache) PersistentStorage() *cacheboltdb.BoltStorage {
|
||||
return c.ps
|
||||
}
|
||||
|
||||
// checkCacheForDynamicSecretRequest checks the cache for a particular request based on its
|
||||
// computed ID. It returns a non-nil *SendResponse if an entry is found.
|
||||
func (c *LeaseCache) checkCacheForDynamicSecretRequest(id string) (*SendResponse, error) {
|
||||
return c.checkCacheForRequest(id, nil)
|
||||
}
|
||||
|
||||
// checkCacheForStaticSecretRequest checks the cache for a particular request based on its
|
||||
// computed ID. It returns a non-nil *SendResponse if an entry is found.
|
||||
// If a request is provided, it will validate that the token is allowed to retrieve this
|
||||
// cache entry, and return nil if it isn't. It will also evict the cache if this is a non-GET
|
||||
// request.
|
||||
func (c *LeaseCache) checkCacheForStaticSecretRequest(id string, req *SendRequest) (*SendResponse, error) {
|
||||
return c.checkCacheForRequest(id, req)
|
||||
}
|
||||
|
||||
// checkCacheForRequest checks the cache for a particular request based on its
|
||||
// computed ID. It returns a non-nil *SendResponse if an entry is found.
|
||||
func (c *LeaseCache) checkCacheForRequest(id string) (*SendResponse, error) {
|
||||
// computed ID. It returns a non-nil *SendResponse if an entry is found.
|
||||
// If a token is provided, it will validate that the token is allowed to retrieve this
|
||||
// cache entry, and return nil if it isn't.
|
||||
func (c *LeaseCache) checkCacheForRequest(id string, req *SendRequest) (*SendResponse, error) {
|
||||
var token string
|
||||
if req != nil {
|
||||
token = req.Token
|
||||
// HEAD and OPTIONS are included as future-proofing, since neither of those modify the resource either.
|
||||
if req.Request.Method != http.MethodGet && req.Request.Method != http.MethodHead && req.Request.Method != http.MethodOptions {
|
||||
// This must be an update to the resource, so we should short-circuit and invalidate the cache
|
||||
// as we know the cache is now stale.
|
||||
c.logger.Debug("evicting index from cache, as non-GET received", "id", id, "method", req.Request.Method, "path", req.Request.URL.Path)
|
||||
err := c.db.Evict(cachememdb.IndexNameID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
index, err := c.db.Get(cachememdb.IndexNameID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -192,6 +232,16 @@ func (c *LeaseCache) checkCacheForRequest(id string) (*SendResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if token != "" {
|
||||
// This is a static secret check. We need to ensure that this token
|
||||
// has previously demonstrated access to this static secret.
|
||||
if !slices.Contains(index.Tokens, token) {
|
||||
// We don't have access to this static secret, so
|
||||
// we do not return the cached response.
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Cached request is found, deserialize the response
|
||||
reader := bufio.NewReader(bytes.NewReader(index.Response))
|
||||
resp, err := http.ReadResponse(reader, nil)
|
||||
@@ -221,36 +271,42 @@ func (c *LeaseCache) checkCacheForRequest(id string) (*SendResponse, error) {
|
||||
// it will return the cached response, otherwise it will delegate to the
|
||||
// underlying Proxier and cache the received response.
|
||||
func (c *LeaseCache) Send(ctx context.Context, req *SendRequest) (*SendResponse, error) {
|
||||
// Compute the index ID
|
||||
id, err := computeIndexID(req)
|
||||
// Compute the index ID for both static and dynamic secrets.
|
||||
// The primary difference is that for dynamic secrets, the
|
||||
// Vault token forms part of the index.
|
||||
dynamicSecretCacheId, err := computeIndexID(req)
|
||||
if err != nil {
|
||||
c.logger.Error("failed to compute cache key", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
staticSecretCacheId := computeStaticSecretCacheIndex(req)
|
||||
|
||||
// Check the inflight cache to see if there are other inflight requests
|
||||
// of the same kind, based on the computed ID. If so, we increment a counter
|
||||
|
||||
// Note: we lock both the dynamic secret cache ID and the static secret cache ID
|
||||
// as at this stage, we don't know what kind of secret it is.
|
||||
var inflight *inflightRequest
|
||||
|
||||
defer func() {
|
||||
// Cleanup on the cache if there are no remaining inflight requests.
|
||||
// This is the last step, so we defer the call first
|
||||
if inflight != nil && inflight.remaining.Load() == 0 {
|
||||
c.inflightCache.Delete(id)
|
||||
c.inflightCache.Delete(dynamicSecretCacheId)
|
||||
c.inflightCache.Delete(staticSecretCacheId)
|
||||
}
|
||||
}()
|
||||
|
||||
idLock := locksutil.LockForKey(c.idLocks, id)
|
||||
idLockDynamicSecret := locksutil.LockForKey(c.idLocks, dynamicSecretCacheId)
|
||||
|
||||
// Briefly grab an ID-based lock in here to emulate a load-or-store behavior
|
||||
// and prevent concurrent cacheable requests from being proxied twice if
|
||||
// they both miss the cache due to it being clean when peeking the cache
|
||||
// entry.
|
||||
idLock.Lock()
|
||||
inflightRaw, found := c.inflightCache.Get(id)
|
||||
idLockDynamicSecret.Lock()
|
||||
inflightRaw, found := c.inflightCache.Get(dynamicSecretCacheId)
|
||||
if found {
|
||||
idLock.Unlock()
|
||||
idLockDynamicSecret.Unlock()
|
||||
inflight = inflightRaw.(*inflightRequest)
|
||||
inflight.remaining.Inc()
|
||||
defer inflight.remaining.Dec()
|
||||
@@ -263,19 +319,52 @@ func (c *LeaseCache) Send(ctx context.Context, req *SendRequest) (*SendResponse,
|
||||
case <-inflight.ch:
|
||||
}
|
||||
} else {
|
||||
inflight = newInflightRequest()
|
||||
if inflight == nil {
|
||||
inflight = newInflightRequest()
|
||||
inflight.remaining.Inc()
|
||||
defer inflight.remaining.Dec()
|
||||
defer close(inflight.ch)
|
||||
}
|
||||
|
||||
c.inflightCache.Set(dynamicSecretCacheId, inflight, gocache.NoExpiration)
|
||||
idLockDynamicSecret.Unlock()
|
||||
}
|
||||
|
||||
idLockStaticSecret := locksutil.LockForKey(c.idLocks, staticSecretCacheId)
|
||||
|
||||
// Briefly grab an ID-based lock in here to emulate a load-or-store behavior
|
||||
// and prevent concurrent cacheable requests from being proxied twice if
|
||||
// they both miss the cache due to it being clean when peeking the cache
|
||||
// entry.
|
||||
idLockStaticSecret.Lock()
|
||||
inflightRaw, found = c.inflightCache.Get(staticSecretCacheId)
|
||||
if found {
|
||||
idLockStaticSecret.Unlock()
|
||||
inflight = inflightRaw.(*inflightRequest)
|
||||
inflight.remaining.Inc()
|
||||
defer inflight.remaining.Dec()
|
||||
|
||||
c.inflightCache.Set(id, inflight, gocache.NoExpiration)
|
||||
idLock.Unlock()
|
||||
// If found it means that there's an inflight request being processed.
|
||||
// We wait until that's finished before proceeding further.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-inflight.ch:
|
||||
}
|
||||
} else {
|
||||
if inflight == nil {
|
||||
inflight = newInflightRequest()
|
||||
inflight.remaining.Inc()
|
||||
defer inflight.remaining.Dec()
|
||||
defer close(inflight.ch)
|
||||
}
|
||||
|
||||
// Signal that the processing request is done
|
||||
defer close(inflight.ch)
|
||||
c.inflightCache.Set(staticSecretCacheId, inflight, gocache.NoExpiration)
|
||||
idLockStaticSecret.Unlock()
|
||||
}
|
||||
|
||||
// Check if the response for this request is already in the cache
|
||||
cachedResp, err := c.checkCacheForRequest(id)
|
||||
// Check if the response for this request is already in the dynamic secret cache
|
||||
cachedResp, err := c.checkCacheForDynamicSecretRequest(dynamicSecretCacheId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -284,6 +373,16 @@ func (c *LeaseCache) Send(ctx context.Context, req *SendRequest) (*SendResponse,
|
||||
return cachedResp, nil
|
||||
}
|
||||
|
||||
// Check if the response for this request is already in the static secret cache
|
||||
cachedResp, err = c.checkCacheForStaticSecretRequest(staticSecretCacheId, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cachedResp != nil {
|
||||
c.logger.Debug("returning cached response", "id", staticSecretCacheId, "path", req.Request.URL.Path)
|
||||
return cachedResp, nil
|
||||
}
|
||||
|
||||
c.logger.Debug("forwarding request from cache", "method", req.Request.Method, "path", req.Request.URL.Path)
|
||||
|
||||
// Pass the request down and get a response
|
||||
@@ -308,7 +407,6 @@ func (c *LeaseCache) Send(ctx context.Context, req *SendRequest) (*SendResponse,
|
||||
|
||||
// Build the index to cache based on the response received
|
||||
index := &cachememdb.Index{
|
||||
ID: id,
|
||||
Namespace: namespace,
|
||||
RequestPath: req.Request.URL.Path,
|
||||
LastRenewed: time.Now().UTC(),
|
||||
@@ -337,6 +435,20 @@ func (c *LeaseCache) Send(ctx context.Context, req *SendRequest) (*SendResponse,
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// TODO: if secret.MountType == "kvv1" || secret.MountType == "kvv2"
|
||||
if c.cacheStaticSecrets && secret != nil {
|
||||
index.Type = cacheboltdb.StaticSecretType
|
||||
index.ID = staticSecretCacheId
|
||||
err := c.cacheStaticSecret(ctx, req, resp, index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
} else {
|
||||
// Since it's not a static secret, set the ID to be the dynamic id
|
||||
index.ID = dynamicSecretCacheId
|
||||
}
|
||||
|
||||
// Short-circuit if the secret is not renewable
|
||||
tokenRenewable, err := secret.TokenIsRenewable()
|
||||
if err != nil {
|
||||
@@ -420,7 +532,7 @@ func (c *LeaseCache) Send(ctx context.Context, req *SendRequest) (*SendResponse,
|
||||
if resp.Response.Body != nil {
|
||||
resp.Response.Body.Close()
|
||||
}
|
||||
resp.Response.Body = ioutil.NopCloser(bytes.NewReader(resp.ResponseBody))
|
||||
resp.Response.Body = io.NopCloser(bytes.NewReader(resp.ResponseBody))
|
||||
|
||||
// Set the index's Response
|
||||
index.Response = respBytes.Bytes()
|
||||
@@ -440,20 +552,86 @@ func (c *LeaseCache) Send(ctx context.Context, req *SendRequest) (*SendResponse,
|
||||
index.RequestToken = req.Token
|
||||
index.RequestHeader = req.Request.Header
|
||||
|
||||
// Store the index in the cache
|
||||
c.logger.Debug("storing response into the cache", "method", req.Request.Method, "path", req.Request.URL.Path)
|
||||
err = c.Set(ctx, index)
|
||||
if err != nil {
|
||||
c.logger.Error("failed to cache the proxied response", "error", err)
|
||||
return nil, err
|
||||
if index.Type != cacheboltdb.StaticSecretType {
|
||||
// Store the index in the cache
|
||||
c.logger.Debug("storing response into the cache", "method", req.Request.Method, "path", req.Request.URL.Path)
|
||||
err = c.Set(ctx, index)
|
||||
if err != nil {
|
||||
c.logger.Error("failed to cache the proxied response", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Start renewing the secret in the response
|
||||
go c.startRenewing(renewCtx, index, req, secret)
|
||||
}
|
||||
|
||||
// Start renewing the secret in the response
|
||||
go c.startRenewing(renewCtx, index, req, secret)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *LeaseCache) cacheStaticSecret(ctx context.Context, req *SendRequest, resp *SendResponse, index *cachememdb.Index) error {
|
||||
// If a cached version of this secret exists, we now have access, so
|
||||
// we don't need to re-cache, just update index.Tokens
|
||||
indexFromCache, err := c.db.Get(cachememdb.IndexNameID, index.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We must hold a lock for the index while it's being updated.
|
||||
// We keep the two locking mechanisms distinct, so that it's only writes
|
||||
// that have to be serial.
|
||||
index.IndexLock.Lock()
|
||||
defer index.IndexLock.Unlock()
|
||||
|
||||
// The index already exists, so all we need to do is add our token
|
||||
// to the index's allowed token list, then re-store it
|
||||
if indexFromCache != nil {
|
||||
indexFromCache.Tokens = append(indexFromCache.Tokens, req.Token)
|
||||
|
||||
return c.storeStaticSecretIndex(ctx, req, indexFromCache)
|
||||
}
|
||||
|
||||
// Serialize the response to store it in the cached index
|
||||
var respBytes bytes.Buffer
|
||||
err = resp.Response.Write(&respBytes)
|
||||
if err != nil {
|
||||
c.logger.Error("failed to serialize response", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Reset the response body for upper layers to read
|
||||
if resp.Response.Body != nil {
|
||||
resp.Response.Body.Close()
|
||||
}
|
||||
resp.Response.Body = io.NopCloser(bytes.NewReader(resp.ResponseBody))
|
||||
|
||||
// Set the index's Response
|
||||
index.Response = respBytes.Bytes()
|
||||
|
||||
// Set the index's tokens
|
||||
index.Tokens = []string{req.Token}
|
||||
|
||||
// Set the index type
|
||||
index.Type = cacheboltdb.StaticSecretType
|
||||
|
||||
return c.storeStaticSecretIndex(ctx, req, index)
|
||||
}
|
||||
|
||||
func (c *LeaseCache) storeStaticSecretIndex(ctx context.Context, req *SendRequest, index *cachememdb.Index) error {
|
||||
// Store the index in the cache
|
||||
c.logger.Debug("storing response into the cache", "method", req.Request.Method, "path", req.Request.URL.Path)
|
||||
err := c.Set(ctx, index)
|
||||
if err != nil {
|
||||
c.logger.Error("failed to cache the proxied response", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: We need to also update the cache for the token's permission capabilities.
|
||||
// TODO: for this we'll need: req.Token, req.URL.Path
|
||||
// TODO: we need to build a NEW index, with a hash of the token as the ID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *LeaseCache) createCtxInfo(ctx context.Context) *cachememdb.ContextInfo {
|
||||
if ctx == nil {
|
||||
c.l.RLock()
|
||||
@@ -575,7 +753,7 @@ func computeIndexID(req *SendRequest) (string, error) {
|
||||
}
|
||||
|
||||
// Reset the request body after it has been closed by Write
|
||||
req.Request.Body = ioutil.NopCloser(bytes.NewReader(req.RequestBody))
|
||||
req.Request.Body = io.NopCloser(bytes.NewReader(req.RequestBody))
|
||||
|
||||
// Append req.Token into the byte slice. This is needed since auto-auth'ed
|
||||
// requests sets the token directly into SendRequest.Token
|
||||
@@ -586,6 +764,14 @@ func computeIndexID(req *SendRequest) (string, error) {
|
||||
return hex.EncodeToString(cryptoutil.Blake2b256Hash(string(b.Bytes()))), nil
|
||||
}
|
||||
|
||||
// computeStaticSecretCacheIndex results in a value that uniquely identifies a static
|
||||
// secret's cached ID. Notably, we intentionally ignore headers (for example,
|
||||
// the X-Vault-Token header) to remain agnostic to which token is being
|
||||
// used in the request. We care only about the path.
|
||||
func computeStaticSecretCacheIndex(req *SendRequest) string {
|
||||
return hex.EncodeToString(cryptoutil.Blake2b256Hash(req.Request.URL.Path))
|
||||
}
|
||||
|
||||
// HandleCacheClear returns a handlerFunc that can perform cache clearing operations.
|
||||
func (c *LeaseCache) HandleCacheClear(ctx context.Context) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -662,7 +848,11 @@ func (c *LeaseCache) handleCacheClear(ctx context.Context, in *cacheClearInput)
|
||||
return err
|
||||
}
|
||||
for _, index := range indexes {
|
||||
index.RenewCtxInfo.CancelFunc()
|
||||
if index.RenewCtxInfo != nil {
|
||||
if index.RenewCtxInfo.CancelFunc != nil {
|
||||
index.RenewCtxInfo.CancelFunc()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case "token":
|
||||
@@ -684,7 +874,7 @@ func (c *LeaseCache) handleCacheClear(ctx context.Context, in *cacheClearInput)
|
||||
index.RenewCtxInfo.CancelFunc()
|
||||
|
||||
case "token_accessor":
|
||||
if in.TokenAccessor == "" {
|
||||
if in.TokenAccessor == "" && in.Type != cacheboltdb.StaticSecretType {
|
||||
return errors.New("token accessor not provided")
|
||||
}
|
||||
|
||||
@@ -1123,7 +1313,9 @@ func (c *LeaseCache) restoreLeaseRenewCtx(index *cachememdb.Index) error {
|
||||
}
|
||||
renewCtxInfo = c.createCtxInfo(parentCtx)
|
||||
default:
|
||||
return fmt.Errorf("unknown cached index item: %s", index.ID)
|
||||
// This isn't a renewable cache entry, i.e. a static secret cache entry.
|
||||
// We return, because there's nothing to do.
|
||||
return nil
|
||||
}
|
||||
|
||||
renewCtx := context.WithValue(renewCtxInfo.Ctx, contextIndexID, index.ID)
|
||||
|
||||
@@ -440,10 +440,11 @@ func (c *ProxyCommand) Run(args []string) int {
|
||||
// Create the lease cache proxier and set its underlying proxier to
|
||||
// the API proxier.
|
||||
leaseCache, err = cache.NewLeaseCache(&cache.LeaseCacheConfig{
|
||||
Client: proxyClient,
|
||||
BaseContext: ctx,
|
||||
Proxier: apiProxy,
|
||||
Logger: cacheLogger.Named("leasecache"),
|
||||
Client: proxyClient,
|
||||
BaseContext: ctx,
|
||||
Proxier: apiProxy,
|
||||
Logger: cacheLogger.Named("leasecache"),
|
||||
CacheStaticSecrets: config.Cache.CacheStaticSecrets,
|
||||
})
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error creating lease cache: %v", err))
|
||||
|
||||
@@ -101,8 +101,9 @@ type APIProxy struct {
|
||||
|
||||
// Cache contains any configuration needed for Cache mode
|
||||
type Cache struct {
|
||||
Persist *agentproxyshared.PersistConfig `hcl:"persist"`
|
||||
InProcDialer transportDialer `hcl:"-"`
|
||||
Persist *agentproxyshared.PersistConfig `hcl:"persist"`
|
||||
InProcDialer transportDialer `hcl:"-"`
|
||||
CacheStaticSecrets bool `hcl:"cache_static_secrets"`
|
||||
}
|
||||
|
||||
// AutoAuth is the configured authentication method and sinks
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
@@ -578,8 +579,8 @@ vault {
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// TestProxy_Cache_DynamicSecret Tests that the cache successfully caches a dynamic secret
|
||||
// going through the Proxy,
|
||||
// TestProxy_Cache_DynamicSecret tests that the cache successfully caches a dynamic secret
|
||||
// going through the Proxy, and that a subsequent request will be served from the cache.
|
||||
func TestProxy_Cache_DynamicSecret(t *testing.T) {
|
||||
logger := logging.NewVaultLogger(hclog.Trace)
|
||||
cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
|
||||
@@ -685,6 +686,266 @@ vault {
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// TestProxy_Cache_StaticSecret Tests that the cache successfully caches a static secret
|
||||
// going through the Proxy,
|
||||
func TestProxy_Cache_StaticSecret(t *testing.T) {
|
||||
logger := logging.NewVaultLogger(hclog.Trace)
|
||||
cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
|
||||
HandlerFunc: vaulthttp.Handler,
|
||||
})
|
||||
cluster.Start()
|
||||
defer cluster.Cleanup()
|
||||
|
||||
serverClient := cluster.Cores[0].Client
|
||||
|
||||
// Unset the environment variable so that proxy picks up the right test
|
||||
// cluster address
|
||||
defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress))
|
||||
os.Unsetenv(api.EnvVaultAddress)
|
||||
|
||||
cacheConfig := `
|
||||
cache {
|
||||
cache_static_secrets = true
|
||||
}
|
||||
`
|
||||
listenAddr := generateListenerAddress(t)
|
||||
listenConfig := fmt.Sprintf(`
|
||||
listener "tcp" {
|
||||
address = "%s"
|
||||
tls_disable = true
|
||||
}
|
||||
`, listenAddr)
|
||||
|
||||
config := fmt.Sprintf(`
|
||||
vault {
|
||||
address = "%s"
|
||||
tls_skip_verify = true
|
||||
}
|
||||
%s
|
||||
%s
|
||||
log_level = "trace"
|
||||
`, serverClient.Address(), cacheConfig, listenConfig)
|
||||
configPath := makeTempFile(t, "config.hcl", config)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
// Start proxy
|
||||
_, cmd := testProxyCommand(t, logger)
|
||||
cmd.startedCh = make(chan struct{})
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
cmd.Run([]string{"-config", configPath})
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-cmd.startedCh:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Errorf("timeout")
|
||||
}
|
||||
|
||||
proxyClient, err := api.NewClient(api.DefaultConfig())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
proxyClient.SetToken(serverClient.Token())
|
||||
proxyClient.SetMaxRetries(0)
|
||||
err = proxyClient.SetAddress("http://" + listenAddr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
secretData := map[string]interface{}{
|
||||
"foo": "bar",
|
||||
}
|
||||
|
||||
// Create kvv1 secret
|
||||
err = serverClient.KVv1("secret").Put(context.Background(), "my-secret", secretData)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// We use raw requests so we can check the headers for cache hit/miss.
|
||||
// We expect the first to miss, and the second to hit.
|
||||
req := proxyClient.NewRequest(http.MethodGet, "/v1/secret/my-secret")
|
||||
resp1, err := proxyClient.RawRequest(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cacheValue := resp1.Header.Get("X-Cache")
|
||||
require.Equal(t, "MISS", cacheValue)
|
||||
|
||||
req = proxyClient.NewRequest(http.MethodGet, "/v1/secret/my-secret")
|
||||
resp2, err := proxyClient.RawRequest(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cacheValue = resp2.Header.Get("X-Cache")
|
||||
require.Equal(t, "HIT", cacheValue)
|
||||
|
||||
// Lastly, we check to make sure the actual data we received is
|
||||
// as we expect. We must use ParseSecret due to the raw requests.
|
||||
secret1, err := api.ParseSecret(resp1.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.Equal(t, secretData, secret1.Data)
|
||||
|
||||
secret2, err := api.ParseSecret(resp2.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.Equal(t, secret1.Data, secret2.Data)
|
||||
|
||||
close(cmd.ShutdownCh)
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// TestProxy_Cache_StaticSecretInvalidation Tests that the cache successfully caches a static secret
|
||||
// going through the Proxy, and that it gets invalidated by a POST.
|
||||
func TestProxy_Cache_StaticSecretInvalidation(t *testing.T) {
|
||||
logger := logging.NewVaultLogger(hclog.Trace)
|
||||
cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
|
||||
HandlerFunc: vaulthttp.Handler,
|
||||
})
|
||||
cluster.Start()
|
||||
defer cluster.Cleanup()
|
||||
|
||||
serverClient := cluster.Cores[0].Client
|
||||
|
||||
// Unset the environment variable so that proxy picks up the right test
|
||||
// cluster address
|
||||
defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress))
|
||||
os.Unsetenv(api.EnvVaultAddress)
|
||||
|
||||
cacheConfig := `
|
||||
cache {
|
||||
cache_static_secrets = true
|
||||
}
|
||||
`
|
||||
listenAddr := generateListenerAddress(t)
|
||||
listenConfig := fmt.Sprintf(`
|
||||
listener "tcp" {
|
||||
address = "%s"
|
||||
tls_disable = true
|
||||
}
|
||||
`, listenAddr)
|
||||
|
||||
config := fmt.Sprintf(`
|
||||
vault {
|
||||
address = "%s"
|
||||
tls_skip_verify = true
|
||||
}
|
||||
%s
|
||||
%s
|
||||
log_level = "trace"
|
||||
`, serverClient.Address(), cacheConfig, listenConfig)
|
||||
configPath := makeTempFile(t, "config.hcl", config)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
// Start proxy
|
||||
_, cmd := testProxyCommand(t, logger)
|
||||
cmd.startedCh = make(chan struct{})
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
cmd.Run([]string{"-config", configPath})
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-cmd.startedCh:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Errorf("timeout")
|
||||
}
|
||||
|
||||
proxyClient, err := api.NewClient(api.DefaultConfig())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
proxyClient.SetToken(serverClient.Token())
|
||||
proxyClient.SetMaxRetries(0)
|
||||
err = proxyClient.SetAddress("http://" + listenAddr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
secretData := map[string]interface{}{
|
||||
"foo": "bar",
|
||||
}
|
||||
|
||||
secretData2 := map[string]interface{}{
|
||||
"bar": "baz",
|
||||
}
|
||||
|
||||
// Create kvv1 secret
|
||||
err = serverClient.KVv1("secret").Put(context.Background(), "my-secret", secretData)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// We use raw requests so we can check the headers for cache hit/miss.
|
||||
req := proxyClient.NewRequest(http.MethodGet, "/v1/secret/my-secret")
|
||||
resp1, err := proxyClient.RawRequest(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cacheValue := resp1.Header.Get("X-Cache")
|
||||
require.Equal(t, "MISS", cacheValue)
|
||||
|
||||
// Update the secret using the proxy client
|
||||
err = proxyClient.KVv1("secret").Put(context.Background(), "my-secret", secretData2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp2, err := proxyClient.RawRequest(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cacheValue = resp2.Header.Get("X-Cache")
|
||||
// This should miss too, as we just updated it
|
||||
require.Equal(t, "MISS", cacheValue)
|
||||
|
||||
resp3, err := proxyClient.RawRequest(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cacheValue = resp3.Header.Get("X-Cache")
|
||||
// This should hit, as the third request should get the cached value
|
||||
require.Equal(t, "HIT", cacheValue)
|
||||
|
||||
// Lastly, we check to make sure the actual data we received is
|
||||
// as we expect. We must use ParseSecret due to the raw requests.
|
||||
secret1, err := api.ParseSecret(resp1.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.Equal(t, secretData, secret1.Data)
|
||||
|
||||
secret2, err := api.ParseSecret(resp2.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.Equal(t, secretData2, secret2.Data)
|
||||
|
||||
secret3, err := api.ParseSecret(resp3.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.Equal(t, secret2.Data, secret3.Data)
|
||||
|
||||
close(cmd.ShutdownCh)
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// TestProxy_ApiProxy_Retry Tests the retry functionalities of Vault Proxy's API Proxy
|
||||
func TestProxy_ApiProxy_Retry(t *testing.T) {
|
||||
//----------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user