Skip to content

Commit 8d07273

Browse files
fix: cache aws auth client by account id (#9981) (#10107)
* fix aws auth client cache to use accound ID * return error if no sts config found * cache ec2 clients by account ID, region, and role * add changelog * fix log syntax Co-authored-by: John-Michael Faircloth <[email protected]>
1 parent 169d045 commit 8d07273

File tree

4 files changed

+67
-41
lines changed

4 files changed

+67
-41
lines changed

builtin/credential/aws/backend.go

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,19 @@ type backend struct {
7373
// of tidyCooldownPeriod.
7474
nextTidyTime time.Time
7575

76-
// Map to hold the EC2 client objects indexed by region and STS role.
76+
// Map to hold the EC2 client objects indexed by a composite key of Account
77+
// ID, region, and STS role.
7778
// This avoids the overhead of creating a client object for every login request.
7879
// When the credentials are modified or deleted, all the cached client objects
7980
// will be flushed. The empty STS role signifies the master account
80-
EC2ClientsMap map[string]map[string]*ec2.EC2
81+
EC2ClientsMap map[clientKey]*ec2.EC2
8182

82-
// Map to hold the IAM client objects indexed by region and STS role.
83+
// Map to hold the IAM client objects indexed by a composite key of Account
84+
// ID, region, and STS role.
8385
// This avoids the overhead of creating a client object for every login request.
8486
// When the credentials are modified or deleted, all the cached client objects
8587
// will be flushed. The empty STS role signifies the master account
86-
IAMClientsMap map[string]map[string]*iam.IAM
88+
IAMClientsMap map[clientKey]*iam.IAM
8789

8890
// Map to associate a partition to a random region in that partition. Users of
8991
// this don't care what region in the partition they use, but there is some client
@@ -117,13 +119,31 @@ type backend struct {
117119
deprecatedTerms *strings.Replacer
118120
}
119121

122+
// clientKey is a composite key for caching IAM or EC2 clients.
123+
type clientKey struct {
124+
AccountID string
125+
Region string
126+
STSRole string // Use an empty string for the master account
127+
}
128+
129+
func (c clientKey) String() string {
130+
// For clarity in logs, we explicitly state when the master account is
131+
// being used instead of showing an empty string for the role.
132+
rolePart := c.STSRole
133+
if rolePart == "" {
134+
rolePart = "<master-account>"
135+
}
136+
137+
return fmt.Sprintf("%s/%s/%s", c.AccountID, c.Region, rolePart)
138+
}
139+
120140
func Backend(_ *logical.BackendConfig) (*backend, error) {
121141
b := &backend{
122142
// Setting the periodic func to be run once in an hour.
123143
// If there is a real need, this can be made configurable.
124144
tidyCooldownPeriod: time.Hour,
125-
EC2ClientsMap: make(map[string]map[string]*ec2.EC2),
126-
IAMClientsMap: make(map[string]map[string]*iam.IAM),
145+
EC2ClientsMap: make(map[clientKey]*ec2.EC2),
146+
IAMClientsMap: make(map[clientKey]*iam.IAM),
127147
iamUserIdToArnCache: cache.New(7*24*time.Hour, 24*time.Hour),
128148
tidyDenyListCASGuard: new(uint32),
129149
tidyAccessListCASGuard: new(uint32),

builtin/credential/aws/client.go

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -175,21 +175,15 @@ func (b *backend) getClientConfig(ctx context.Context, s logical.Storage, region
175175
// the cached EC2 client objects will be flushed. Config mutex lock should be
176176
// acquired for write operation before calling this method.
177177
func (b *backend) flushCachedEC2Clients() {
178-
// deleting items in map during iteration is safe
179-
for region := range b.EC2ClientsMap {
180-
delete(b.EC2ClientsMap, region)
181-
}
178+
b.EC2ClientsMap = make(map[clientKey]*ec2.EC2)
182179
}
183180

184181
// flushCachedIAMClients deletes all the cached iam client objects from the
185182
// backend. If the client credentials configuration is deleted or updated in
186183
// the backend, all the cached IAM client objects will be flushed. Config mutex
187184
// lock should be acquired for write operation before calling this method.
188185
func (b *backend) flushCachedIAMClients() {
189-
// deleting items in map during iteration is safe
190-
for region := range b.IAMClientsMap {
191-
delete(b.IAMClientsMap, region)
192-
}
186+
b.IAMClientsMap = make(map[clientKey]*iam.IAM)
193187
}
194188

195189
// Gets an entry out of the user ID cache
@@ -221,6 +215,11 @@ func (b *backend) stsRoleForAccount(ctx context.Context, s logical.Storage, acco
221215
if sts != nil {
222216
return sts.StsRole, sts.ExternalID, nil
223217
}
218+
219+
// Return an error if there's no STS config for an account which is not the default one
220+
if b.defaultAWSAccountID != "" && b.defaultAWSAccountID != accountID {
221+
return "", "", fmt.Errorf("no STS configuration found for account ID %q", accountID)
222+
}
224223
return "", "", nil
225224
}
226225

@@ -231,10 +230,16 @@ func (b *backend) clientEC2(ctx context.Context, s logical.Storage, region, acco
231230
return nil, err
232231
}
233232
b.configMutex.RLock()
234-
if b.EC2ClientsMap[region] != nil && b.EC2ClientsMap[region][stsRole] != nil {
233+
234+
key := clientKey{
235+
AccountID: accountID,
236+
Region: region,
237+
STSRole: stsRole,
238+
}
239+
if cachedClient, ok := b.EC2ClientsMap[key]; ok {
235240
defer b.configMutex.RUnlock()
236241
// If the client object was already created, return it
237-
return b.EC2ClientsMap[region][stsRole], nil
242+
return cachedClient, nil
238243
}
239244

240245
// Release the read lock and acquire the write lock
@@ -243,8 +248,8 @@ func (b *backend) clientEC2(ctx context.Context, s logical.Storage, region, acco
243248
defer b.configMutex.Unlock()
244249

245250
// If the client gets created while switching the locks, return it
246-
if b.EC2ClientsMap[region] != nil && b.EC2ClientsMap[region][stsRole] != nil {
247-
return b.EC2ClientsMap[region][stsRole], nil
251+
if cachedClient, ok := b.EC2ClientsMap[key]; ok {
252+
return cachedClient, nil
248253
}
249254

250255
// Create an AWS config object using a chain of providers
@@ -267,13 +272,9 @@ func (b *backend) clientEC2(ctx context.Context, s logical.Storage, region, acco
267272
if client == nil {
268273
return nil, fmt.Errorf("could not obtain ec2 client")
269274
}
270-
if _, ok := b.EC2ClientsMap[region]; !ok {
271-
b.EC2ClientsMap[region] = map[string]*ec2.EC2{stsRole: client}
272-
} else {
273-
b.EC2ClientsMap[region][stsRole] = client
274-
}
275275

276-
return b.EC2ClientsMap[region][stsRole], nil
276+
b.EC2ClientsMap[key] = client
277+
return b.EC2ClientsMap[key], nil
277278
}
278279

279280
// clientIAM creates a client to interact with AWS IAM API
@@ -283,27 +284,34 @@ func (b *backend) clientIAM(ctx context.Context, s logical.Storage, region, acco
283284
return nil, err
284285
}
285286
if stsRole == "" {
286-
b.Logger().Debug(fmt.Sprintf("no stsRole found for %s", accountID))
287+
b.Logger().Debug("no stsRole found for account", "accountID", accountID)
287288
} else {
288-
b.Logger().Debug(fmt.Sprintf("found stsRole %s for account %s", stsRole, accountID))
289+
b.Logger().Debug("found stsRole for account", "stsRole", stsRole, "accountID", accountID)
289290
}
290291
b.configMutex.RLock()
291-
if b.IAMClientsMap[region] != nil && b.IAMClientsMap[region][stsRole] != nil {
292+
293+
key := clientKey{
294+
AccountID: accountID,
295+
Region: region,
296+
STSRole: stsRole,
297+
}
298+
if cachedClient, ok := b.IAMClientsMap[key]; ok {
292299
defer b.configMutex.RUnlock()
293300
// If the client object was already created, return it
294-
b.Logger().Debug(fmt.Sprintf("returning cached client for region %s and stsRole %s", region, stsRole))
295-
return b.IAMClientsMap[region][stsRole], nil
301+
b.Logger().Debug("returning cached client for key", "key", key)
302+
return cachedClient, nil
296303
}
297-
b.Logger().Debug(fmt.Sprintf("no cached client for region %s and stsRole %s", region, stsRole))
304+
b.Logger().Debug("no cached client for key", "key", key)
298305

299306
// Release the read lock and acquire the write lock
300307
b.configMutex.RUnlock()
301308
b.configMutex.Lock()
302309
defer b.configMutex.Unlock()
303310

304311
// If the client gets created while switching the locks, return it
305-
if b.IAMClientsMap[region] != nil && b.IAMClientsMap[region][stsRole] != nil {
306-
return b.IAMClientsMap[region][stsRole], nil
312+
if cachedClient, ok := b.IAMClientsMap[key]; ok {
313+
b.Logger().Debug("returning cached client for key", "key", key)
314+
return cachedClient, nil
307315
}
308316

309317
// Create an AWS config object using a chain of providers
@@ -326,12 +334,8 @@ func (b *backend) clientIAM(ctx context.Context, s logical.Storage, region, acco
326334
if client == nil {
327335
return nil, fmt.Errorf("could not obtain iam client")
328336
}
329-
if _, ok := b.IAMClientsMap[region]; !ok {
330-
b.IAMClientsMap[region] = map[string]*iam.IAM{stsRole: client}
331-
} else {
332-
b.IAMClientsMap[region][stsRole] = client
333-
}
334-
return b.IAMClientsMap[region][stsRole], nil
337+
b.IAMClientsMap[key] = client
338+
return b.IAMClientsMap[key], nil
335339
}
336340

337341
// PluginIdentityTokenFetcher fetches plugin identity tokens from Vault. It is provided

builtin/credential/aws/path_config_rotate_root.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"github.com/aws/aws-sdk-go/aws"
1111
"github.com/aws/aws-sdk-go/aws/credentials"
1212
"github.com/aws/aws-sdk-go/aws/session"
13-
"github.com/aws/aws-sdk-go/service/ec2"
1413
"github.com/aws/aws-sdk-go/service/iam"
1514
"github.com/aws/aws-sdk-go/service/iam/iamiface"
1615
"github.com/hashicorp/go-cleanhttp"
@@ -181,8 +180,8 @@ func (b *backend) rotateRoot(ctx context.Context, req *logical.Request) (*logica
181180

182181
// Previous cached clients need to be cleared because they may have been made using
183182
// the soon-to-be-obsolete credentials.
184-
b.IAMClientsMap = make(map[string]map[string]*iam.IAM)
185-
b.EC2ClientsMap = make(map[string]map[string]*ec2.EC2)
183+
b.flushCachedIAMClients()
184+
b.flushCachedEC2Clients()
186185

187186
// Now to clean up the old key.
188187
deleteAccessKeyInput := iam.DeleteAccessKeyInput{

changelog/_9981.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:security
2+
auth/aws: fix an issue where a user may be able to bypass authentication to Vault due to incorrect caching of the AWS client
3+
```

0 commit comments

Comments
 (0)