OpenBao AWS Plugin Vulnerable to Cross-Account IAM Role Impersonation in AWS Auth Method
Description
OpenBao's AWS Plugin generates AWS access credentials based on IAM policies. Prior to version 0.1.1, the AWS Plugin is vulnerable to cross-account IAM role Impersonation in the AWS auth method. The vulnerability allows an IAM role from an untrusted AWS account to authenticate by impersonating a role with the same name in a trusted account, leading to unauthorized access. This impacts all users of the auth-aws plugin who operate in a multi-account AWS environment where IAM role names may not be unique across accounts. This vulnerability has been patched in version 0.1.1 of the auth-aws plugin. A workaround for this issue involves guaranteeing that IAM role names are unique across all AWS accounts that could potentially interact with your OpenBao environment, and to audit for any duplicate IAM roles.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
OpenBao AWS Plugin before 0.1.1 allows cross-account IAM role impersonation due to a caching flaw that fails to validate the AWS account ID, enabling role impersonation.
Vulnerability
Overview
CVE-2025-59048 is a cross-account IAM role impersonation vulnerability in the OpenBao AWS Plugin (auth-aws). The root cause is a flawed caching mechanism in the plugin's authentication logic that fails to validate the AWS account ID when caching and retrieving EC2 clients. Specifically, the clientEC2 function cached clients keyed only by region and STS role, without including the account ID, allowing a client created for one account to be reused for another account with the same role name [1][2].
Exploitation
An attacker controlling an IAM role in an untrusted AWS account can authenticate to OpenBao by impersonating a role with the same name in a trusted account. The vulnerability can be exploited even without wildcards in bound_iam_principal_arn; a role name collision between accounts is sufficient. The only prerequisite is that a duplicate IAM role name across AWS accounts that interact with the OpenBao environment [2].
Impact
Successful exploitation grants the attacker unauthorized access to OpenBao, potentially leading to exposure of secrets, data exfiltration, and privilege escalation within the trusted account's scope [2].
Mitigation
The vulnerability is patched in version 0.1.1 of the auth-aws plugin [1][2]. Users unable to upgrade should ensure IAM role names are unique across all AWS accounts that interact with OpenBao, and audit for duplicate roles. Removing wildcards from bound_iam_principal_arn is recommended but does not fully mitigate the issue if duplicate role names exist [2].
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/openbao/openbao-pluginsGo | < 0.1.1 | 0.1.1 |
Affected products
2- openbao/openbao-pluginsv5Range: < 0.1.1
Patches
12a77af368347Merge commit from fork
4 files changed · +121 −26
auth/aws/backend.go+6 −6 modified@@ -73,17 +73,17 @@ type backend struct { // of tidyCooldownPeriod. nextTidyTime time.Time - // Map to hold the EC2 client objects indexed by region and STS role. + // Map to hold the EC2 client objects indexed by region, account ID, and STS role. // This avoids the overhead of creating a client object for every login request. // When the credentials are modified or deleted, all the cached client objects // will be flushed. The empty STS role signifies the master account - EC2ClientsMap map[string]map[string]*ec2.EC2 + EC2ClientsMap map[string]map[string]map[string]*ec2.EC2 - // Map to hold the IAM client objects indexed by region and STS role. + // Map to hold the IAM client objects indexed by region, account ID, and STS role. // This avoids the overhead of creating a client object for every login request. // When the credentials are modified or deleted, all the cached client objects // will be flushed. The empty STS role signifies the master account - IAMClientsMap map[string]map[string]*iam.IAM + IAMClientsMap map[string]map[string]map[string]*iam.IAM // Map to associate a partition to a random region in that partition. Users of // this don't care what region in the partition they use, but there is some client @@ -122,8 +122,8 @@ func Backend(_ *logical.BackendConfig) (*backend, error) { // Setting the periodic func to be run once in an hour. // If there is a real need, this can be made configurable. tidyCooldownPeriod: time.Hour, - EC2ClientsMap: make(map[string]map[string]*ec2.EC2), - IAMClientsMap: make(map[string]map[string]*iam.IAM), + EC2ClientsMap: make(map[string]map[string]map[string]*ec2.EC2), + IAMClientsMap: make(map[string]map[string]map[string]*iam.IAM), iamUserIdToArnCache: cache.New(7*24*time.Hour, 24*time.Hour), tidyDenyListCASGuard: new(uint32), tidyAccessListCASGuard: new(uint32),
auth/aws/client.go+41 −18 modified@@ -190,6 +190,12 @@ func (b *backend) stsRoleForAccount(ctx context.Context, s logical.Storage, acco if sts != nil { return sts.StsRole, nil } + + // Return an error if there's no STS config for an account which is not the default one + if b.defaultAWSAccountID != "" && b.defaultAWSAccountID != accountID { + return "", fmt.Errorf("no STS configuration found for account ID %q", accountID) + } + return "", nil } @@ -200,20 +206,26 @@ func (b *backend) clientEC2(ctx context.Context, s logical.Storage, region, acco return nil, err } b.configMutex.RLock() - if b.EC2ClientsMap[region] != nil && b.EC2ClientsMap[region][stsRole] != nil { + if b.EC2ClientsMap[region] != nil && + b.EC2ClientsMap[region][accountID] != nil && + b.EC2ClientsMap[region][accountID][stsRole] != nil { defer b.configMutex.RUnlock() // If the client object was already created, return it - return b.EC2ClientsMap[region][stsRole], nil + b.Logger().Debug(fmt.Sprintf("returning cached client for region %s, account %s and stsRole %s", region, accountID, stsRole)) + return b.EC2ClientsMap[region][accountID][stsRole], nil } + b.Logger().Debug(fmt.Sprintf("no cached client for region %s, account %s and stsRole %s", region, accountID, stsRole)) // Release the read lock and acquire the write lock b.configMutex.RUnlock() b.configMutex.Lock() defer b.configMutex.Unlock() // If the client gets created while switching the locks, return it - if b.EC2ClientsMap[region] != nil && b.EC2ClientsMap[region][stsRole] != nil { - return b.EC2ClientsMap[region][stsRole], nil + if b.EC2ClientsMap[region] != nil && + b.EC2ClientsMap[region][accountID] != nil && + b.EC2ClientsMap[region][accountID][stsRole] != nil { + return b.EC2ClientsMap[region][accountID][stsRole], nil } // Create an AWS config object using a chain of providers @@ -237,13 +249,16 @@ func (b *backend) clientEC2(ctx context.Context, s logical.Storage, region, acco if client == nil { return nil, fmt.Errorf("could not obtain ec2 client") } + if _, ok := b.EC2ClientsMap[region]; !ok { - b.EC2ClientsMap[region] = map[string]*ec2.EC2{stsRole: client} - } else { - b.EC2ClientsMap[region][stsRole] = client + b.EC2ClientsMap[region] = make(map[string]map[string]*ec2.EC2) } + if _, ok := b.EC2ClientsMap[region][accountID]; !ok { + b.EC2ClientsMap[region][accountID] = make(map[string]*ec2.EC2) + } + b.EC2ClientsMap[region][accountID][stsRole] = client - return b.EC2ClientsMap[region][stsRole], nil + return b.EC2ClientsMap[region][accountID][stsRole], nil } // clientIAM creates a client to interact with AWS IAM API @@ -258,22 +273,26 @@ func (b *backend) clientIAM(ctx context.Context, s logical.Storage, region, acco b.Logger().Debug(fmt.Sprintf("found stsRole %s for account %s", stsRole, accountID)) } b.configMutex.RLock() - if b.IAMClientsMap[region] != nil && b.IAMClientsMap[region][stsRole] != nil { + if b.IAMClientsMap[region] != nil && + b.IAMClientsMap[region][accountID] != nil && + b.IAMClientsMap[region][accountID][stsRole] != nil { defer b.configMutex.RUnlock() // If the client object was already created, return it - b.Logger().Debug(fmt.Sprintf("returning cached client for region %s and stsRole %s", region, stsRole)) - return b.IAMClientsMap[region][stsRole], nil + b.Logger().Debug(fmt.Sprintf("returning cached client for region %s, account %s and stsRole %s", region, accountID, stsRole)) + return b.IAMClientsMap[region][accountID][stsRole], nil } - b.Logger().Debug(fmt.Sprintf("no cached client for region %s and stsRole %s", region, stsRole)) + b.Logger().Debug(fmt.Sprintf("no cached client for region %s, account %s and stsRole %s", region, accountID, stsRole)) // Release the read lock and acquire the write lock b.configMutex.RUnlock() b.configMutex.Lock() defer b.configMutex.Unlock() // If the client gets created while switching the locks, return it - if b.IAMClientsMap[region] != nil && b.IAMClientsMap[region][stsRole] != nil { - return b.IAMClientsMap[region][stsRole], nil + if b.IAMClientsMap[region] != nil && + b.IAMClientsMap[region][accountID] != nil && + b.IAMClientsMap[region][accountID][stsRole] != nil { + return b.IAMClientsMap[region][accountID][stsRole], nil } // Create an AWS config object using a chain of providers @@ -297,10 +316,14 @@ func (b *backend) clientIAM(ctx context.Context, s logical.Storage, region, acco if client == nil { return nil, fmt.Errorf("could not obtain iam client") } + if _, ok := b.IAMClientsMap[region]; !ok { - b.IAMClientsMap[region] = map[string]*iam.IAM{stsRole: client} - } else { - b.IAMClientsMap[region][stsRole] = client + b.IAMClientsMap[region] = make(map[string]map[string]*iam.IAM) } - return b.IAMClientsMap[region][stsRole], nil + if _, ok := b.IAMClientsMap[region][accountID]; !ok { + b.IAMClientsMap[region][accountID] = make(map[string]*iam.IAM) + } + b.IAMClientsMap[region][accountID][stsRole] = client + + return b.IAMClientsMap[region][accountID][stsRole], nil }
auth/aws/client_test.go+72 −0 added@@ -0,0 +1,72 @@ +// Copyright (c) 2025 OpenBao a Series of LF Projects, LLC +// SPDX-License-Identifier: MPL-2.0 + +package aws + +import ( + "context" + "fmt" + "testing" + + "github.com/openbao/openbao/sdk/v2/logical" +) + +// TestClientCache verifies that IAM clients for different +// AWS accounts are properly isolated in the cache +func TestClientCache(t *testing.T) { + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + + b, err := Backend(config) + if err != nil { + t.Fatal(err) + } + + ctx := context.Background() + if err := b.Setup(ctx, config); err != nil { + t.Fatal(err) + } + + account1 := "111111111111" + account2 := "222222222222" + + b.defaultAWSAccountID = account1 + + // This should work - same account as default + stsRole, err := b.stsRoleForAccount(ctx, storage, account1) + if err != nil { + t.Fatalf("Expected success for default account, got error: %v", err) + } + if stsRole != "" { + t.Fatalf("Expected empty STS role for default account, got: %v", stsRole) + } + + // This should fail - different account without STS config + _, err = b.stsRoleForAccount(ctx, storage, account2) + if err == nil { + t.Fatal("Expected error for cross-account access without STS config") + } + + // Verify the error message contains the expected error + expectedError := fmt.Sprintf("no STS configuration found for account ID %q", account2) + if err.Error() != expectedError { + t.Fatalf("Expected specific error message, got: %v", err) + } + + stsEntry := &awsStsEntry{ + StsRole: "arn:aws:iam::222222222222:role/cross-account-role", + } + err = b.lockedSetAwsStsEntry(ctx, storage, account2, stsEntry) + if err != nil { + t.Fatalf("Failed to set STS entry: %v", err) + } + + stsRole, err = b.stsRoleForAccount(ctx, storage, account2) + if err != nil { + t.Fatalf("Expected success for account with STS config, got error: %v", err) + } + if stsRole != stsEntry.StsRole { + t.Fatalf("Expected STS role %v, got: %v", stsEntry.StsRole, stsRole) + } +}
auth/aws/path_config_rotate_root.go+2 −2 modified@@ -177,8 +177,8 @@ func (b *backend) pathConfigRotateRootUpdate(ctx context.Context, req *logical.R // Previous cached clients need to be cleared because they may have been made using // the soon-to-be-obsolete credentials. - b.IAMClientsMap = make(map[string]map[string]*iam.IAM) - b.EC2ClientsMap = make(map[string]map[string]*ec2.EC2) + b.IAMClientsMap = make(map[string]map[string]map[string]*iam.IAM) + b.EC2ClientsMap = make(map[string]map[string]map[string]*ec2.EC2) // Now to clean up the old key. deleteAccessKeyInput := iam.DeleteAccessKeyInput{
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
3- github.com/advisories/GHSA-jp7h-4f3c-9rc7ghsaADVISORY
- github.com/openbao/openbao-plugins/commit/2a77af36834746ca6d3ac9bd1049154c84b3efaeghsax_refsource_MISCWEB
- github.com/openbao/openbao-plugins/security/advisories/GHSA-jp7h-4f3c-9rc7ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.