mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 17:52:32 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
3
changelog/26628.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
```release-note:improvement
|
||||
auth/aws: add support for external_ids in AWS assume-role
|
||||
```
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user