From 54c84decfd7d2eec0c940ca51ecf62691e28e4d1 Mon Sep 17 00:00:00 2001 From: Violet Hynes Date: Fri, 22 Sep 2023 10:57:38 -0400 Subject: [PATCH] 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 --- .../cache/cacheboltdb/bolt.go | 3 + .../cache/cachememdb/index.go | 11 + .../cache/cachememdb/index_test.go | 1 + command/agentproxyshared/cache/lease_cache.go | 286 +++++++++++++++--- command/proxy.go | 9 +- command/proxy/config/config.go | 5 +- command/proxy_test.go | 265 +++++++++++++++- 7 files changed, 525 insertions(+), 55 deletions(-) diff --git a/command/agentproxyshared/cache/cacheboltdb/bolt.go b/command/agentproxyshared/cache/cacheboltdb/bolt.go index a6740861a1..42c9fff485 100644 --- a/command/agentproxyshared/cache/cacheboltdb/bolt.go +++ b/command/agentproxyshared/cache/cacheboltdb/bolt.go @@ -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 diff --git a/command/agentproxyshared/cache/cachememdb/index.go b/command/agentproxyshared/cache/cachememdb/index.go index c4b079aef7..af80c0907e 100644 --- a/command/agentproxyshared/cache/cachememdb/index.go +++ b/command/agentproxyshared/cache/cachememdb/index.go @@ -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 diff --git a/command/agentproxyshared/cache/cachememdb/index_test.go b/command/agentproxyshared/cache/cachememdb/index_test.go index 871ac023e4..a218b4433a 100644 --- a/command/agentproxyshared/cache/cachememdb/index_test.go +++ b/command/agentproxyshared/cache/cachememdb/index_test.go @@ -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", diff --git a/command/agentproxyshared/cache/lease_cache.go b/command/agentproxyshared/cache/lease_cache.go index 79d5f2f3bc..be5ee90bff 100644 --- a/command/agentproxyshared/cache/lease_cache.go +++ b/command/agentproxyshared/cache/lease_cache.go @@ -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) diff --git a/command/proxy.go b/command/proxy.go index c00b850d1d..fdcee532dc 100644 --- a/command/proxy.go +++ b/command/proxy.go @@ -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)) diff --git a/command/proxy/config/config.go b/command/proxy/config/config.go index 2aebe3609a..1881f08633 100644 --- a/command/proxy/config/config.go +++ b/command/proxy/config/config.go @@ -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 diff --git a/command/proxy_test.go b/command/proxy_test.go index 4720e1909f..ecfe910803 100644 --- a/command/proxy_test.go +++ b/command/proxy_test.go @@ -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) { //----------------------------------------------------