mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 17:52:32 +00:00
Cache trusted cert values, invalidating when anything changes (#25421)
* Cache trusted cert values, invalidating when anything changes * rename to something more indicative * defer * changelog * Use an LRU cache rather than a static map so we can't use too much memory. Add docs, unit tests * Don't add to cache if disabled. But this races if just a bool, so make the disabled an atomic
This commit is contained in:
@@ -16,12 +16,19 @@ import (
|
||||
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
lru "github.com/hashicorp/golang-lru/v2"
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/helper/ocsp"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
const operationPrefixCert = "cert"
|
||||
const (
|
||||
operationPrefixCert = "cert"
|
||||
trustedCertPath = "cert/"
|
||||
|
||||
defaultRoleCacheSize = 200
|
||||
maxRoleCacheSize = 10000
|
||||
)
|
||||
|
||||
func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
|
||||
b := Backend()
|
||||
@@ -32,7 +39,11 @@ func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend,
|
||||
}
|
||||
|
||||
func Backend() *backend {
|
||||
var b backend
|
||||
// ignoring the error as it only can occur with <= 0 size
|
||||
cache, _ := lru.New[string, *trusted](defaultRoleCacheSize)
|
||||
b := backend{
|
||||
trustedCache: cache,
|
||||
}
|
||||
b.Backend = &framework.Backend{
|
||||
Help: backendHelp,
|
||||
PathsSpecial: &logical.Paths{
|
||||
@@ -59,6 +70,13 @@ func Backend() *backend {
|
||||
return &b
|
||||
}
|
||||
|
||||
type trusted struct {
|
||||
pool *x509.CertPool
|
||||
trusted []*ParsedCert
|
||||
trustedNonCAs []*ParsedCert
|
||||
ocspConf *ocsp.VerifyConfig
|
||||
}
|
||||
|
||||
type backend struct {
|
||||
*framework.Backend
|
||||
MapCertId *framework.PathMap
|
||||
@@ -68,6 +86,9 @@ type backend struct {
|
||||
ocspClientMutex sync.RWMutex
|
||||
ocspClient *ocsp.Client
|
||||
configUpdated atomic.Bool
|
||||
|
||||
trustedCache *lru.Cache[string, *trusted]
|
||||
trustedCacheDisabled atomic.Bool
|
||||
}
|
||||
|
||||
func (b *backend) initialize(ctx context.Context, req *logical.InitializationRequest) error {
|
||||
@@ -98,6 +119,7 @@ func (b *backend) invalidate(_ context.Context, key string) {
|
||||
case key == "config":
|
||||
b.configUpdated.Store(true)
|
||||
}
|
||||
b.flushTrustedCache()
|
||||
}
|
||||
|
||||
func (b *backend) initOCSPClient(cacheSize int) {
|
||||
@@ -109,9 +131,21 @@ func (b *backend) initOCSPClient(cacheSize int) {
|
||||
func (b *backend) updatedConfig(config *config) {
|
||||
b.ocspClientMutex.Lock()
|
||||
defer b.ocspClientMutex.Unlock()
|
||||
|
||||
switch {
|
||||
case config.RoleCacheSize < 0:
|
||||
// Just to clean up memory
|
||||
b.trustedCacheDisabled.Store(true)
|
||||
b.trustedCache.Purge()
|
||||
case config.RoleCacheSize == 0:
|
||||
config.RoleCacheSize = defaultRoleCacheSize
|
||||
fallthrough
|
||||
default:
|
||||
b.trustedCache.Resize(config.RoleCacheSize)
|
||||
b.trustedCacheDisabled.Store(false)
|
||||
}
|
||||
b.initOCSPClient(config.OcspCacheSize)
|
||||
b.configUpdated.Store(false)
|
||||
return
|
||||
}
|
||||
|
||||
func (b *backend) fetchCRL(ctx context.Context, storage logical.Storage, name string, crl *CRLInfo) error {
|
||||
@@ -161,6 +195,12 @@ func (b *backend) storeConfig(ctx context.Context, storage logical.Storage, conf
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *backend) flushTrustedCache() {
|
||||
if b.trustedCache != nil { // defensive
|
||||
b.trustedCache.Purge()
|
||||
}
|
||||
}
|
||||
|
||||
const backendHelp = `
|
||||
The "cert" credential provider allows authentication using
|
||||
TLS client certificates. A client connects to Vault and uses
|
||||
|
||||
@@ -234,7 +234,7 @@ certificate.`,
|
||||
}
|
||||
|
||||
func (b *backend) Cert(ctx context.Context, s logical.Storage, n string) (*CertEntry, error) {
|
||||
entry, err := s.Get(ctx, "cert/"+strings.ToLower(n))
|
||||
entry, err := s.Get(ctx, trustedCertPath+strings.ToLower(n))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -267,7 +267,8 @@ func (b *backend) Cert(ctx context.Context, s logical.Storage, n string) (*CertE
|
||||
}
|
||||
|
||||
func (b *backend) pathCertDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
err := req.Storage.Delete(ctx, "cert/"+strings.ToLower(d.Get("name").(string)))
|
||||
defer b.flushTrustedCache()
|
||||
err := req.Storage.Delete(ctx, trustedCertPath+strings.ToLower(d.Get("name").(string)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -275,7 +276,7 @@ func (b *backend) pathCertDelete(ctx context.Context, req *logical.Request, d *f
|
||||
}
|
||||
|
||||
func (b *backend) pathCertList(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
certs, err := req.Storage.List(ctx, "cert/")
|
||||
certs, err := req.Storage.List(ctx, trustedCertPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -332,6 +333,7 @@ func (b *backend) pathCertRead(ctx context.Context, req *logical.Request, d *fra
|
||||
}
|
||||
|
||||
func (b *backend) pathCertWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
defer b.flushTrustedCache()
|
||||
name := strings.ToLower(d.Get("name").(string))
|
||||
|
||||
cert, err := b.Cert(ctx, req.Storage, name)
|
||||
@@ -474,7 +476,7 @@ func (b *backend) pathCertWrite(ctx context.Context, req *logical.Request, d *fr
|
||||
}
|
||||
|
||||
// Store it
|
||||
entry, err := logical.StorageEntryJSON("cert/"+name, cert)
|
||||
entry, err := logical.StorageEntryJSON(trustedCertPath+name, cert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
const maxCacheSize = 100000
|
||||
const maxOcspCacheSize = 100000
|
||||
|
||||
func pathConfig(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
@@ -37,6 +37,11 @@ func pathConfig(b *backend) *framework.Path {
|
||||
Default: 100,
|
||||
Description: `The size of the in memory OCSP response cache, shared by all configured certs`,
|
||||
},
|
||||
"role_cache_size": {
|
||||
Type: framework.TypeInt,
|
||||
Default: defaultRoleCacheSize,
|
||||
Description: `The size of the in memory role cache`,
|
||||
},
|
||||
},
|
||||
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
@@ -70,11 +75,18 @@ func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, dat
|
||||
}
|
||||
if cacheSizeRaw, ok := data.GetOk("ocsp_cache_size"); ok {
|
||||
cacheSize := cacheSizeRaw.(int)
|
||||
if cacheSize < 2 || cacheSize > maxCacheSize {
|
||||
return logical.ErrorResponse("invalid cache size, must be >= 2 and <= %d", maxCacheSize), nil
|
||||
if cacheSize < 2 || cacheSize > maxOcspCacheSize {
|
||||
return logical.ErrorResponse("invalid ocsp cache size, must be >= 2 and <= %d", maxOcspCacheSize), nil
|
||||
}
|
||||
config.OcspCacheSize = cacheSize
|
||||
}
|
||||
if cacheSizeRaw, ok := data.GetOk("role_cache_size"); ok {
|
||||
cacheSize := cacheSizeRaw.(int)
|
||||
if (cacheSize < 0 && cacheSize != -1) || cacheSize > maxRoleCacheSize {
|
||||
return logical.ErrorResponse("invalid role cache size, must be <= %d or -1 to disable role caching", maxRoleCacheSize), nil
|
||||
}
|
||||
config.RoleCacheSize = cacheSize
|
||||
}
|
||||
if err := b.storeConfig(ctx, req.Storage, config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -91,6 +103,7 @@ func (b *backend) pathConfigRead(ctx context.Context, req *logical.Request, d *f
|
||||
"disable_binding": cfg.DisableBinding,
|
||||
"enable_identity_alias_metadata": cfg.EnableIdentityAliasMetadata,
|
||||
"ocsp_cache_size": cfg.OcspCacheSize,
|
||||
"role_cache_size": cfg.RoleCacheSize,
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
@@ -119,4 +132,5 @@ type config struct {
|
||||
DisableBinding bool `json:"disable_binding"`
|
||||
EnableIdentityAliasMetadata bool `json:"enable_identity_alias_metadata"`
|
||||
OcspCacheSize int `json:"ocsp_cache_size"`
|
||||
RoleCacheSize int `json:"role_cache_size"`
|
||||
}
|
||||
|
||||
@@ -190,6 +190,7 @@ func (b *backend) pathCRLDelete(ctx context.Context, req *logical.Request, d *fr
|
||||
|
||||
b.crlUpdateMutex.Lock()
|
||||
defer b.crlUpdateMutex.Unlock()
|
||||
defer b.flushTrustedCache()
|
||||
|
||||
_, ok := b.crls[name]
|
||||
if !ok {
|
||||
@@ -313,6 +314,8 @@ func (b *backend) setCRL(ctx context.Context, storage logical.Storage, certList
|
||||
}
|
||||
|
||||
b.crls[name] = crlInfo
|
||||
b.flushTrustedCache()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -256,7 +256,7 @@ func (b *backend) verifyCredentials(ctx context.Context, req *logical.Request, d
|
||||
}
|
||||
|
||||
// Load the trusted certificates and other details
|
||||
roots, trusted, trustedNonCAs, verifyConf := b.loadTrustedCerts(ctx, req.Storage, certName)
|
||||
roots, trusted, trustedNonCAs, verifyConf := b.getTrustedCerts(ctx, req.Storage, certName)
|
||||
|
||||
// Get the list of full chains matching the connection and validates the
|
||||
// certificate itself
|
||||
@@ -580,10 +580,21 @@ func (b *backend) certificateExtensionsMetadata(clientCert *x509.Certificate, co
|
||||
return metadata
|
||||
}
|
||||
|
||||
// getTrustedCerts is used to load all the trusted certificates from the backend, cached
|
||||
|
||||
func (b *backend) getTrustedCerts(ctx context.Context, storage logical.Storage, certName string) (pool *x509.CertPool, trusted []*ParsedCert, trustedNonCAs []*ParsedCert, conf *ocsp.VerifyConfig) {
|
||||
if !b.trustedCacheDisabled.Load() {
|
||||
if trusted, found := b.trustedCache.Get(certName); found {
|
||||
return trusted.pool, trusted.trusted, trusted.trustedNonCAs, trusted.ocspConf
|
||||
}
|
||||
}
|
||||
return b.loadTrustedCerts(ctx, storage, certName)
|
||||
}
|
||||
|
||||
// loadTrustedCerts is used to load all the trusted certificates from the backend
|
||||
func (b *backend) loadTrustedCerts(ctx context.Context, storage logical.Storage, certName string) (pool *x509.CertPool, trusted []*ParsedCert, trustedNonCAs []*ParsedCert, conf *ocsp.VerifyConfig) {
|
||||
func (b *backend) loadTrustedCerts(ctx context.Context, storage logical.Storage, certName string) (pool *x509.CertPool, trustedCerts []*ParsedCert, trustedNonCAs []*ParsedCert, conf *ocsp.VerifyConfig) {
|
||||
pool = x509.NewCertPool()
|
||||
trusted = make([]*ParsedCert, 0)
|
||||
trustedCerts = make([]*ParsedCert, 0)
|
||||
trustedNonCAs = make([]*ParsedCert, 0)
|
||||
|
||||
var names []string
|
||||
@@ -591,7 +602,7 @@ func (b *backend) loadTrustedCerts(ctx context.Context, storage logical.Storage,
|
||||
names = append(names, certName)
|
||||
} else {
|
||||
var err error
|
||||
names, err = storage.List(ctx, "cert/")
|
||||
names, err = storage.List(ctx, trustedCertPath)
|
||||
if err != nil {
|
||||
b.Logger().Error("failed to list trusted certs", "error", err)
|
||||
return
|
||||
@@ -600,7 +611,7 @@ func (b *backend) loadTrustedCerts(ctx context.Context, storage logical.Storage,
|
||||
|
||||
conf = &ocsp.VerifyConfig{}
|
||||
for _, name := range names {
|
||||
entry, err := b.Cert(ctx, storage, strings.TrimPrefix(name, "cert/"))
|
||||
entry, err := b.Cert(ctx, storage, strings.TrimPrefix(name, trustedCertPath))
|
||||
if err != nil {
|
||||
b.Logger().Error("failed to load trusted cert", "name", name, "error", err)
|
||||
continue
|
||||
@@ -629,7 +640,7 @@ func (b *backend) loadTrustedCerts(ctx context.Context, storage logical.Storage,
|
||||
}
|
||||
|
||||
// Create a ParsedCert entry
|
||||
trusted = append(trusted, &ParsedCert{
|
||||
trustedCerts = append(trustedCerts, &ParsedCert{
|
||||
Entry: entry,
|
||||
Certificates: parsed,
|
||||
})
|
||||
@@ -645,6 +656,15 @@ func (b *backend) loadTrustedCerts(ctx context.Context, storage logical.Storage,
|
||||
conf.QueryAllServers = conf.QueryAllServers || entry.OcspQueryAllServers
|
||||
}
|
||||
}
|
||||
|
||||
if !b.trustedCacheDisabled.Load() {
|
||||
b.trustedCache.Add(certName, &trusted{
|
||||
pool: pool,
|
||||
trusted: trustedCerts,
|
||||
trustedNonCAs: trustedNonCAs,
|
||||
ocspConf: conf,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,10 @@ func TestCert_RoleResolve(t *testing.T) {
|
||||
testAccStepCert(t, "web", ca, "foo", allowed{dns: "example.com"}, false),
|
||||
testAccStepLoginWithName(t, connState, "web"),
|
||||
testAccStepResolveRoleWithName(t, connState, "web"),
|
||||
// Test with caching disabled
|
||||
testAccStepSetRoleCacheSize(t, -1),
|
||||
testAccStepLoginWithName(t, connState, "web"),
|
||||
testAccStepResolveRoleWithName(t, connState, "web"),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -148,10 +152,23 @@ func TestCert_RoleResolveWithoutProvidingCertName(t *testing.T) {
|
||||
testAccStepCert(t, "web", ca, "foo", allowed{dns: "example.com"}, false),
|
||||
testAccStepLoginWithName(t, connState, "web"),
|
||||
testAccStepResolveRoleWithEmptyDataMap(t, connState, "web"),
|
||||
testAccStepSetRoleCacheSize(t, -1),
|
||||
testAccStepLoginWithName(t, connState, "web"),
|
||||
testAccStepResolveRoleWithEmptyDataMap(t, connState, "web"),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func testAccStepSetRoleCacheSize(t *testing.T, size int) logicaltest.TestStep {
|
||||
return logicaltest.TestStep{
|
||||
Operation: logical.UpdateOperation,
|
||||
Path: "config",
|
||||
Data: map[string]interface{}{
|
||||
"role_cache_size": size,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func testAccStepResolveRoleWithEmptyDataMap(t *testing.T, connState tls.ConnectionState, certName string) logicaltest.TestStep {
|
||||
return logicaltest.TestStep{
|
||||
Operation: logical.ResolveRoleOperation,
|
||||
|
||||
3
changelog/25421.txt
Normal file
3
changelog/25421.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
```release-note:improvement
|
||||
auth/cert: Cache trusted certs to reduce memory usage and improve performance of logins.
|
||||
```
|
||||
1
go.mod
1
go.mod
@@ -123,6 +123,7 @@ require (
|
||||
github.com/hashicorp/go-uuid v1.0.3
|
||||
github.com/hashicorp/go-version v1.6.0
|
||||
github.com/hashicorp/golang-lru v1.0.2
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||
github.com/hashicorp/hcl v1.0.1-vault-5
|
||||
github.com/hashicorp/hcl/v2 v2.16.2
|
||||
github.com/hashicorp/hcp-link v0.2.1
|
||||
|
||||
2
go.sum
2
go.sum
@@ -2469,6 +2469,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
|
||||
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM=
|
||||
github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
|
||||
|
||||
@@ -360,6 +360,8 @@ Configuration options for the method.
|
||||
`allowed_metadata_extensions` will be stored in the alias
|
||||
- `ocsp_cache_size` `(int: 100)` - The size of the OCSP response LRU cache. Note
|
||||
that this cache is used for all configured certificates.
|
||||
- `role_cache_size` `(int: 200)` - The size of the role cache. Use `-1` to disable
|
||||
role caching.
|
||||
|
||||
### Sample payload
|
||||
|
||||
|
||||
Reference in New Issue
Block a user