Add ExternalID support to AWS Auth STS configuration (#26628)

* add basic external id support to aws auth sts configuration

---------

Co-authored-by: Sarah Chavis <62406755+schavis@users.noreply.github.com>
This commit is contained in:
kpcraig
2024-05-07 11:10:57 -04:00
committed by GitHub
parent 6a351401e6
commit bef178b4a5
5 changed files with 60 additions and 16 deletions

View File

@@ -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:
@@ -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)
@@ -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
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

View File

@@ -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
@@ -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
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}

View File

@@ -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 {
@@ -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,
@@ -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
}
@@ -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

3
changelog/26628.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:improvement
auth/aws: add support for external_ids in AWS assume-role
```

View File

@@ -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