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

Add ExternalID support to AWS Auth STS configuration #26628

Merged
merged 9 commits into from May 7, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 23 additions & 2 deletions builtin/credential/aws/backend_test.go
Expand Up @@ -1046,6 +1046,7 @@ This is an acceptance test.
export TEST_AWS_EC2_IAM_ROLE_ARN=$(aws iam get-role --role-name $(curl -q http://169.254.169.254/latest/meta-data/iam/security-credentials/ -S -s) --query Role.Arn --output text)
export TEST_AWS_EC2_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)


If the test is not being run on an EC2 instance that has access to
credentials using EC2RoleProvider, on top of the above vars, following
needs to be set:
Expand Down Expand Up @@ -1407,6 +1408,11 @@ func TestBackend_pathStsConfig(t *testing.T) {
"sts_role": "arn:aws:iam:account1:role/myRole",
}

data2 := map[string]interface{}{
"sts_role": "arn:aws:iam:account2:role/myRole2",
"external_id": "fake_id",
}

stsReq.Data = data
// test create operation
resp, err := b.HandleRequest(context.Background(), stsReq)
Expand Down Expand Up @@ -1440,13 +1446,28 @@ func TestBackend_pathStsConfig(t *testing.T) {

stsReq.Operation = logical.CreateOperation
stsReq.Path = "config/sts/account2"
stsReq.Data = data
// create another entry to test the list operation
stsReq.Data = data2
// create another entry with alternate data to test ExternalID and LIST
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If possible, could we add a block for checking "external_id": "fake_id" existence in a read response before line 1455?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a response check for data2

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

resp, err = b.HandleRequest(context.Background(), stsReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatal(err)
}

// test second read
stsReq.Operation = logical.ReadOperation
resp, err = b.HandleRequest(context.Background(), stsReq)
if err != nil {
t.Fatal(err)
}
expectedStsRole = "arn:aws:iam:account2:role/myRole2"
expectedExternalID := "fake_id"
if resp.Data["sts_role"].(string) != expectedStsRole {
t.Fatalf("bad: expected:%s\n got:%s\n", expectedStsRole, resp.Data["sts_role"].(string))
}
if resp.Data["external_id"].(string) != expectedExternalID {
t.Fatalf("bad: expected:%s\n got:%s\n", expectedExternalID, resp.Data["external_id"].(string))
}

stsReq.Operation = logical.ListOperation
stsReq.Path = "config/sts"
// test list operation
Expand Down
20 changes: 10 additions & 10 deletions builtin/credential/aws/client.go
Expand Up @@ -84,7 +84,7 @@ func (b *backend) getRawClientConfig(ctx context.Context, s logical.Storage, reg
// It uses getRawClientConfig to obtain config for the runtime environment, and if
// stsRole is a non-empty string, it will use AssumeRole to obtain a set of assumed
// credentials. The credentials will expire after 15 minutes but will auto-refresh.
func (b *backend) getClientConfig(ctx context.Context, s logical.Storage, region, stsRole, accountID, clientType string) (*aws.Config, error) {
func (b *backend) getClientConfig(ctx context.Context, s logical.Storage, region, stsRole, externalID, accountID, clientType string) (*aws.Config, error) {
config, err := b.getRawClientConfig(ctx, s, region, clientType)
if err != nil {
return nil, err
Expand All @@ -105,7 +105,7 @@ func (b *backend) getClientConfig(ctx context.Context, s logical.Storage, region
if err != nil {
return nil, err
}
assumedCredentials := stscreds.NewCredentials(sess, stsRole)
assumedCredentials := stscreds.NewCredentials(sess, stsRole, func(p *stscreds.AssumeRoleProvider) { p.ExternalID = aws.String(externalID) })
// Test that we actually have permissions to assume the role
if _, err = assumedCredentials.Get(); err != nil {
return nil, err
Expand Down Expand Up @@ -180,22 +180,22 @@ func (b *backend) setCachedUserId(userId, arn string) {
}
}

func (b *backend) stsRoleForAccount(ctx context.Context, s logical.Storage, accountID string) (string, error) {
func (b *backend) stsRoleForAccount(ctx context.Context, s logical.Storage, accountID string) (string, string, error) {
// Check if an STS configuration exists for the AWS account
sts, err := b.lockedAwsStsEntry(ctx, s, accountID)
if err != nil {
return "", fmt.Errorf("error fetching STS config for account ID %q: %w", accountID, err)
return "", "", fmt.Errorf("error fetching STS config for account ID %q: %w", accountID, err)
}
// An empty STS role signifies the master account
if sts != nil {
return sts.StsRole, nil
return sts.StsRole, sts.ExternalID, nil
}
return "", nil
return "", "", nil
}

// clientEC2 creates a client to interact with AWS EC2 API
func (b *backend) clientEC2(ctx context.Context, s logical.Storage, region, accountID string) (*ec2.EC2, error) {
stsRole, err := b.stsRoleForAccount(ctx, s, accountID)
stsRole, stsExternalID, err := b.stsRoleForAccount(ctx, s, accountID)
if err != nil {
return nil, err
}
Expand All @@ -218,7 +218,7 @@ func (b *backend) clientEC2(ctx context.Context, s logical.Storage, region, acco

// Create an AWS config object using a chain of providers
var awsConfig *aws.Config
awsConfig, err = b.getClientConfig(ctx, s, region, stsRole, accountID, "ec2")
awsConfig, err = b.getClientConfig(ctx, s, region, stsRole, stsExternalID, accountID, "ec2")
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -247,7 +247,7 @@ func (b *backend) clientEC2(ctx context.Context, s logical.Storage, region, acco

// clientIAM creates a client to interact with AWS IAM API
func (b *backend) clientIAM(ctx context.Context, s logical.Storage, region, accountID string) (*iam.IAM, error) {
stsRole, err := b.stsRoleForAccount(ctx, s, accountID)
stsRole, stsExternalID, err := b.stsRoleForAccount(ctx, s, accountID)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -277,7 +277,7 @@ func (b *backend) clientIAM(ctx context.Context, s logical.Storage, region, acco

// Create an AWS config object using a chain of providers
var awsConfig *aws.Config
awsConfig, err = b.getClientConfig(ctx, s, region, stsRole, accountID, "iam")
awsConfig, err = b.getClientConfig(ctx, s, region, stsRole, stsExternalID, accountID, "iam")
if err != nil {
return nil, err
}
Expand Down
26 changes: 22 additions & 4 deletions builtin/credential/aws/path_config_sts.go
Expand Up @@ -13,7 +13,8 @@ import (

// awsStsEntry is used to store details of an STS role for assumption
type awsStsEntry struct {
StsRole string `json:"sts_role"`
StsRole string `json:"sts_role"`
ExternalID string `json:"external_id,omitempty"` // optional, but recommended
}

func (b *backend) pathListSts() *framework.Path {
Expand Down Expand Up @@ -57,6 +58,11 @@ instances in this account.`,
Description: `AWS ARN for STS role to be assumed when interacting with the account specified.
The Vault server must have permissions to assume this role.`,
},
"external_id": {
Type: framework.TypeString,
Description: `AWS external ID to be used when assuming the STS role.`,
Required: false,
},
},

ExistenceCheck: b.pathConfigStsExistenceCheck,
Expand Down Expand Up @@ -192,10 +198,15 @@ func (b *backend) pathConfigStsRead(ctx context.Context, req *logical.Request, d
return nil, nil
}

dt := map[string]interface{}{
"sts_role": stsEntry.StsRole,
}
if stsEntry.ExternalID != "" {
dt["external_id"] = stsEntry.ExternalID
}

return &logical.Response{
Data: map[string]interface{}{
"sts_role": stsEntry.StsRole,
},
Data: dt,
}, nil
}

Expand Down Expand Up @@ -230,6 +241,13 @@ func (b *backend) pathConfigStsCreateUpdate(ctx context.Context, req *logical.Re
return logical.ErrorResponse("sts role cannot be empty"), nil
}

stsExternalID, ok := data.GetOk("external_id")
if ok {
stsEntry.ExternalID = stsExternalID.(string)
}

b.Logger().Info("setting sts", "account_id", accountID, "sts_role", stsEntry.StsRole, "external_id", stsEntry.ExternalID)

// save the provided STS role
if err := b.nonLockedSetAwsStsEntry(ctx, req.Storage, accountID, stsEntry); err != nil {
return nil, err
Expand Down
3 changes: 3 additions & 0 deletions changelog/26628.txt
@@ -0,0 +1,3 @@
```release-note:improvement
auth/aws: add support for external_ids in AWS assume-role
```
2 changes: 2 additions & 0 deletions website/content/api-docs/auth/aws.mdx
Expand Up @@ -438,6 +438,8 @@ when validating IAM principals or EC2 instances in the particular AWS account.
- `sts_role` `(string: <required>)` - AWS ARN for STS role to be assumed when
interacting with the account specified. The Vault server must have
permissions to assume this role.
- `external_id` `(string: "")` - The external ID expected by the STS role. The
associated STS role **must** be configured to require the external ID.

### Sample payload

Expand Down