Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache trusted cert values, invalidating when anything changes #25421

Merged
merged 6 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 43 additions & 3 deletions builtin/credential/cert/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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{
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions builtin/credential/cert/path_certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,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
}
Expand Down Expand Up @@ -268,15 +268,16 @@ 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
}
return nil, nil
}

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
}
Expand Down Expand Up @@ -333,6 +334,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)
Expand Down Expand Up @@ -475,7 +477,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
}
Expand Down
20 changes: 17 additions & 3 deletions builtin/credential/cert/path_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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{
Expand Down Expand Up @@ -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
}
Expand All @@ -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{
Expand Down Expand Up @@ -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"`
}
3 changes: 3 additions & 0 deletions builtin/credential/cert/path_crls.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -313,6 +314,8 @@ func (b *backend) setCRL(ctx context.Context, storage logical.Storage, certList
}

b.crls[name] = crlInfo
b.flushTrustedCache()

return err
}

Expand Down
32 changes: 26 additions & 6 deletions builtin/credential/cert/path_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,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
Expand Down Expand Up @@ -581,18 +581,29 @@ 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
if certName != "" {
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
Expand All @@ -601,7 +612,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
Expand Down Expand Up @@ -630,7 +641,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,
})
Expand All @@ -646,6 +657,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
}

Expand Down
17 changes: 17 additions & 0 deletions builtin/credential/cert/path_login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,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"),
},
})
}
Expand Down Expand Up @@ -151,10 +155,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,
Expand Down
3 changes: 3 additions & 0 deletions changelog/25421.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
auth/cert: Cache trusted certs to reduce memory usage and improve performance of logins.
```
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2476,6 +2476,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=
Expand Down
2 changes: 2 additions & 0 deletions website/content/api-docs/auth/cert.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading