Skip to content

Commit bef178b

Browse files
kpcraigschavis
andauthored
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>
1 parent 6a35140 commit bef178b

File tree

5 files changed

+60
-16
lines changed

5 files changed

+60
-16
lines changed

builtin/credential/aws/backend_test.go

+23-2
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,7 @@ This is an acceptance test.
10461046
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)
10471047
export TEST_AWS_EC2_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
10481048
1049+
10491050
If the test is not being run on an EC2 instance that has access to
10501051
credentials using EC2RoleProvider, on top of the above vars, following
10511052
needs to be set:
@@ -1407,6 +1408,11 @@ func TestBackend_pathStsConfig(t *testing.T) {
14071408
"sts_role": "arn:aws:iam:account1:role/myRole",
14081409
}
14091410

1411+
data2 := map[string]interface{}{
1412+
"sts_role": "arn:aws:iam:account2:role/myRole2",
1413+
"external_id": "fake_id",
1414+
}
1415+
14101416
stsReq.Data = data
14111417
// test create operation
14121418
resp, err := b.HandleRequest(context.Background(), stsReq)
@@ -1440,13 +1446,28 @@ func TestBackend_pathStsConfig(t *testing.T) {
14401446

14411447
stsReq.Operation = logical.CreateOperation
14421448
stsReq.Path = "config/sts/account2"
1443-
stsReq.Data = data
1444-
// create another entry to test the list operation
1449+
stsReq.Data = data2
1450+
// create another entry with alternate data to test ExternalID and LIST
14451451
resp, err = b.HandleRequest(context.Background(), stsReq)
14461452
if err != nil || (resp != nil && resp.IsError()) {
14471453
t.Fatal(err)
14481454
}
14491455

1456+
// test second read
1457+
stsReq.Operation = logical.ReadOperation
1458+
resp, err = b.HandleRequest(context.Background(), stsReq)
1459+
if err != nil {
1460+
t.Fatal(err)
1461+
}
1462+
expectedStsRole = "arn:aws:iam:account2:role/myRole2"
1463+
expectedExternalID := "fake_id"
1464+
if resp.Data["sts_role"].(string) != expectedStsRole {
1465+
t.Fatalf("bad: expected:%s\n got:%s\n", expectedStsRole, resp.Data["sts_role"].(string))
1466+
}
1467+
if resp.Data["external_id"].(string) != expectedExternalID {
1468+
t.Fatalf("bad: expected:%s\n got:%s\n", expectedExternalID, resp.Data["external_id"].(string))
1469+
}
1470+
14501471
stsReq.Operation = logical.ListOperation
14511472
stsReq.Path = "config/sts"
14521473
// test list operation

builtin/credential/aws/client.go

+10-10
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func (b *backend) getRawClientConfig(ctx context.Context, s logical.Storage, reg
8484
// It uses getRawClientConfig to obtain config for the runtime environment, and if
8585
// stsRole is a non-empty string, it will use AssumeRole to obtain a set of assumed
8686
// credentials. The credentials will expire after 15 minutes but will auto-refresh.
87-
func (b *backend) getClientConfig(ctx context.Context, s logical.Storage, region, stsRole, accountID, clientType string) (*aws.Config, error) {
87+
func (b *backend) getClientConfig(ctx context.Context, s logical.Storage, region, stsRole, externalID, accountID, clientType string) (*aws.Config, error) {
8888
config, err := b.getRawClientConfig(ctx, s, region, clientType)
8989
if err != nil {
9090
return nil, err
@@ -105,7 +105,7 @@ func (b *backend) getClientConfig(ctx context.Context, s logical.Storage, region
105105
if err != nil {
106106
return nil, err
107107
}
108-
assumedCredentials := stscreds.NewCredentials(sess, stsRole)
108+
assumedCredentials := stscreds.NewCredentials(sess, stsRole, func(p *stscreds.AssumeRoleProvider) { p.ExternalID = aws.String(externalID) })
109109
// Test that we actually have permissions to assume the role
110110
if _, err = assumedCredentials.Get(); err != nil {
111111
return nil, err
@@ -180,22 +180,22 @@ func (b *backend) setCachedUserId(userId, arn string) {
180180
}
181181
}
182182

183-
func (b *backend) stsRoleForAccount(ctx context.Context, s logical.Storage, accountID string) (string, error) {
183+
func (b *backend) stsRoleForAccount(ctx context.Context, s logical.Storage, accountID string) (string, string, error) {
184184
// Check if an STS configuration exists for the AWS account
185185
sts, err := b.lockedAwsStsEntry(ctx, s, accountID)
186186
if err != nil {
187-
return "", fmt.Errorf("error fetching STS config for account ID %q: %w", accountID, err)
187+
return "", "", fmt.Errorf("error fetching STS config for account ID %q: %w", accountID, err)
188188
}
189189
// An empty STS role signifies the master account
190190
if sts != nil {
191-
return sts.StsRole, nil
191+
return sts.StsRole, sts.ExternalID, nil
192192
}
193-
return "", nil
193+
return "", "", nil
194194
}
195195

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

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

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

278278
// Create an AWS config object using a chain of providers
279279
var awsConfig *aws.Config
280-
awsConfig, err = b.getClientConfig(ctx, s, region, stsRole, accountID, "iam")
280+
awsConfig, err = b.getClientConfig(ctx, s, region, stsRole, stsExternalID, accountID, "iam")
281281
if err != nil {
282282
return nil, err
283283
}

builtin/credential/aws/path_config_sts.go

+22-4
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import (
1313

1414
// awsStsEntry is used to store details of an STS role for assumption
1515
type awsStsEntry struct {
16-
StsRole string `json:"sts_role"`
16+
StsRole string `json:"sts_role"`
17+
ExternalID string `json:"external_id,omitempty"` // optional, but recommended
1718
}
1819

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

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

201+
dt := map[string]interface{}{
202+
"sts_role": stsEntry.StsRole,
203+
}
204+
if stsEntry.ExternalID != "" {
205+
dt["external_id"] = stsEntry.ExternalID
206+
}
207+
195208
return &logical.Response{
196-
Data: map[string]interface{}{
197-
"sts_role": stsEntry.StsRole,
198-
},
209+
Data: dt,
199210
}, nil
200211
}
201212

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

244+
stsExternalID, ok := data.GetOk("external_id")
245+
if ok {
246+
stsEntry.ExternalID = stsExternalID.(string)
247+
}
248+
249+
b.Logger().Info("setting sts", "account_id", accountID, "sts_role", stsEntry.StsRole, "external_id", stsEntry.ExternalID)
250+
233251
// save the provided STS role
234252
if err := b.nonLockedSetAwsStsEntry(ctx, req.Storage, accountID, stsEntry); err != nil {
235253
return nil, err

changelog/26628.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:improvement
2+
auth/aws: add support for external_ids in AWS assume-role
3+
```

website/content/api-docs/auth/aws.mdx

+2
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,8 @@ when validating IAM principals or EC2 instances in the particular AWS account.
438438
- `sts_role` `(string: <required>)` - AWS ARN for STS role to be assumed when
439439
interacting with the account specified. The Vault server must have
440440
permissions to assume this role.
441+
- `external_id` `(string: "")` - The external ID expected by the STS role. The
442+
associated STS role **must** be configured to require the external ID.
441443

442444
### Sample payload
443445

0 commit comments

Comments
 (0)