Skip to content

Commit

Permalink
Add Session tags and external id support for AWS Secrets
Browse files Browse the repository at this point in the history
It is now possible to set the AWS session tags and external id when assuming an IAM role
via STS AssumeRole.

Here's an example setup

AWS Role - Trust relationship needs the `sts:TagSession` permission set as described in https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_permissions-required
```bash
vault secrets enable aws
vault write aws/config/root access_key=<key> secret_key=<secret> region=us-west-2
vault write aws/roles/my-role credential_type=assumed_role max_sts_ttl=1h role_arns="arn:aws:iam::000000000000:role/test" session_tags="department=eng" session_tags="project=p1" external_id=123
```

Read the role definition
```
vault read  aws/roles/my-role
Key                         Value
---                         -----
credential_type             assumed_role
default_sts_ttl             0s
external_id                 123
iam_groups                  <nil>
iam_tags                    <nil>
max_sts_ttl                 1h
permissions_boundary_arn    n/a
policy_arns                 <nil>
policy_document             n/a
role_arns                   [arn:aws:iam::000000000000:role/test]
session_tags                map[department:eng project:p1]
user_path                   n/a
```

```bash
vault read  aws/sts/my-role
Key               Value
---               -----
access_key        XXXX
arn               arn:aws:sts::000000000000:assumed-role/test/vault-token-my-role-1674516375-mBKZQssrwtP6gl3kbmWq
secret_key        XXX
security_token    XXX
ttl               59m59s
```

The cloudtrail for AssumeRole looks like
```
	"userIdentity": {
        "type": "IAMUser",
        "principalId": "XXX",
        "arn": "arn:aws:iam::000000000000:user/vault-user",
        "accountId": "000000000000",
        "accessKeyId": "XXX",
        "userName": "vault-user"
    },
    "eventTime": "2023-01-23T23:31:12Z",
    "eventSource": "sts.amazonaws.com",
    "eventName": "AssumeRole",
    "awsRegion": "us-east-1",
    "sourceIPAddress": "XXX",
    "userAgent": "aws-sdk-go/1.44.128 (go1.19.5; darwin; arm64)",
    "requestParameters": {
        "roleArn": "arn:aws:iam::000000000000:role/test",
        "roleSessionName": "vault-token-my-role-1674516375",
        "durationSeconds": 3600,
        "tags": [
            {
                "key": "department",
                "value": "eng"
            },
            {
                "key": "project",
                "value": "p1"
            }
        ],
        "externalId": "123"
    },
	...
```

When using the creds are used for accessing an S3 resource with Attribute-based Access Control (ABAC), you can now see that only requests with principalTag project=p1 are allowed while requests to path p2 are denied.

```
aws s3 cp a.txt s3://session-tags-test/p1/a.txt
upload: ./a.txt to s3://session-tags-test/p1/a.txt
➜  ~ aws s3 cp a.txt s3://session-tags-test/p2/a.txt
upload failed: ./a.txt to s3://session-tags-test/p2/a.txt An error occurred (AccessDenied) when calling the PutObject operation: Access Denied
```

The IAM policy had the following block in the above case
```
	{
            "Action": [
                "s3:Describe*",
                "s3:Get*",
                "s3:List*",
                "s3:PutObject*"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:s3:::session-tags-test/${aws:PrincipalTag/project}/*",
                "arn:aws:s3:::session-tags-test/${aws:PrincipalTag/project}"
            ]
        }
```

Troubleshooting:
Error:
```
 Error assuming role: AccessDenied: User: arn:aws:iam::000000000000:user/vault-user is not authorized to perform: sts:TagSession on resource: arn:aws:iam::000000000000:role/test
	status code: 403, request id: ba8ab60e-2fdf-4668-81ad-5fe83e9b898e
```

Remedy: Assign the `sts:TagSession` permission to the `arn:aws:iam::000000000000:role/test`. See https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_permissions-required

Error:
```
* Error assuming role: AccessDenied: User: arn:aws:iam::000000000000:user/vault-user is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::000000000000:role/test
	status code: 403, request id: c0117588-9c84-490d-9b36-91135545dec1
```
Remedy: If the external ID is set, follow https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html to ensure that external ID matches the ID set on the role. If you have not added the externalID condition on the role, it would not affect the assume role operation when an external ID is set only in Vault.

This change does not add support for transitive keys but it should be simple to add it in the future.

Closes hashicorp#3790 hashicorp#7960
  • Loading branch information
harsimranmaan committed Aug 3, 2023
1 parent 6b31e45 commit 8470de9
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 7 deletions.
44 changes: 41 additions & 3 deletions builtin/logical/aws/path_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ user generated. When credential_type is assumed_role or federation_token, this
will be passed in as the Policy parameter to the AssumeRole or
GetFederationToken API call, acting as a filter on permissions available.`,
},

"iam_groups": {
Type: framework.TypeCommaStringSlice,
Description: `Names of IAM groups that generated IAM users will be added to. For a credential
Expand All @@ -115,7 +114,23 @@ delimited key pairs.`,
Value: "[key1=value1, key2=value2]",
},
},

"session_tags": {
Type: framework.TypeKVPairs,
Description: fmt.Sprintf(`Session tags to be set for %q creds created by this role. These must be presented
as Key-Value pairs. This can be represented as a map or a list of equal sign
delimited key pairs.`, assumedRoleCred),
DisplayAttrs: &framework.DisplayAttributes{
Name: "Session Tags",
Value: "[key1=value1, key2=value2]",
},
},
"external_id": {
Type: framework.TypeString,
Description: "External ID to set when assuming the role; only valid when credential_type is" + assumedRoleCred,
DisplayAttrs: &framework.DisplayAttributes{
Name: "External ID",
},
},
"default_sts_ttl": {
Type: framework.TypeDurationSecond,
Description: fmt.Sprintf("Default TTL for %s and %s credential types when no TTL is explicitly requested with the credentials", assumedRoleCred, federationTokenCred),
Expand Down Expand Up @@ -328,6 +343,14 @@ func (b *backend) pathRolesWrite(ctx context.Context, req *logical.Request, d *f
roleEntry.IAMTags = iamTags.(map[string]string)
}

if sessionTags, ok := d.GetOk("session_tags"); ok {
roleEntry.SessionTags = sessionTags.(map[string]string)
}

if externalID, ok := d.GetOk("external_id"); ok {
roleEntry.ExternalID = externalID.(string)
}

if legacyRole != "" {
roleEntry = upgradeLegacyPolicyEntry(legacyRole)
if roleEntry.InvalidData != "" {
Expand Down Expand Up @@ -514,6 +537,8 @@ type awsRoleEntry struct {
PolicyDocument string `json:"policy_document"` // JSON-serialized inline policy to attach to IAM users and/or to specify as the Policy parameter in AssumeRole calls
IAMGroups []string `json:"iam_groups"` // Names of IAM groups that generated IAM users will be added to
IAMTags map[string]string `json:"iam_tags"` // IAM tags that will be added to the generated IAM users
SessionTags map[string]string `json:"session_tags"` // Session tags that will be added as Tags parameter in AssumedRole calls
ExternalID string `json:"external_id"` // External ID to added as ExternalID in AssumeRole calls
InvalidData string `json:"invalid_data,omitempty"` // Invalid role data. Exists to support converting the legacy role data into the new format
ProhibitFlexibleCredPath bool `json:"prohibit_flexible_cred_path,omitempty"` // Disallow accessing STS credentials via the creds path and vice verse
Version int `json:"version"` // Version number of the role format
Expand All @@ -531,6 +556,8 @@ func (r *awsRoleEntry) toResponseData() map[string]interface{} {
"policy_document": r.PolicyDocument,
"iam_groups": r.IAMGroups,
"iam_tags": r.IAMTags,
"session_tags": r.SessionTags,
"external_id": r.ExternalID,
"default_sts_ttl": int64(r.DefaultSTSTTL.Seconds()),
"max_sts_ttl": int64(r.MaxSTSTTL.Seconds()),
"user_path": r.UserPath,
Expand Down Expand Up @@ -576,7 +603,7 @@ func (r *awsRoleEntry) validate() error {
errors = multierror.Append(errors, fmt.Errorf("user_path parameter only valid for %s credential type", iamUserCred))
}
if !userPathRegex.MatchString(r.UserPath) {
errors = multierror.Append(errors, fmt.Errorf("The specified value for user_path is invalid. It must match %q regexp", userPathRegex.String()))
errors = multierror.Append(errors, fmt.Errorf("invalid user_path value. It must match %q regexp", userPathRegex.String()))
}
}

Expand All @@ -592,6 +619,17 @@ func (r *awsRoleEntry) validate() error {
if len(r.RoleArns) > 0 && !strutil.StrListContains(r.CredentialTypes, assumedRoleCred) {
errors = multierror.Append(errors, fmt.Errorf("cannot supply role_arns when credential_type isn't %s", assumedRoleCred))
}
if len(r.SessionTags) > 0 && !strutil.StrListContains(r.CredentialTypes, assumedRoleCred) {
errors = multierror.Append(errors, fmt.Errorf("cannot supply session_tags when credential_type isn't %s", assumedRoleCred))
// https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_know
if len(r.SessionTags) > 50 {
errors = multierror.Append(errors, fmt.Errorf("cannot supply more than %d session_tags", 50))
}
}

if r.ExternalID != "" && !strutil.StrListContains(r.CredentialTypes, assumedRoleCred) {
errors = multierror.Append(errors, fmt.Errorf("cannot supply external_id when credential_type isn't %s", assumedRoleCred))
}

return errors.ErrorOrNil()
}
Expand Down
9 changes: 7 additions & 2 deletions builtin/logical/aws/path_roles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,8 +392,13 @@ func TestRoleEntryValidationAssumedRoleCred(t *testing.T) {
RoleArns: []string{"arn:aws:iam::123456789012:role/SomeRole"},
PolicyArns: []string{adminAccessPolicyARN},
PolicyDocument: allowAllPolicyDocument,
DefaultSTSTTL: 2,
MaxSTSTTL: 3,
ExternalID: "my-ext-id",
SessionTags: map[string]string{
"Key1": "Value1",
"Key2": "Value2",
},
DefaultSTSTTL: 2,
MaxSTSTTL: 3,
}
if err := roleEntry.validate(); err != nil {
t.Errorf("bad: valid roleEntry %#v failed validation: %v", roleEntry, err)
Expand Down
2 changes: 1 addition & 1 deletion builtin/logical/aws/path_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ func (b *backend) pathCredsRead(ctx context.Context, req *logical.Request, d *fr
case !strutil.StrListContains(role.RoleArns, roleArn):
return logical.ErrorResponse(fmt.Sprintf("role_arn %q not in allowed role arns for Vault role %q", roleArn, roleName)), nil
}
return b.assumeRole(ctx, req.Storage, req.DisplayName, roleName, roleArn, role.PolicyDocument, role.PolicyArns, role.IAMGroups, ttl, roleSessionName)
return b.assumeRole(ctx, req.Storage, req.DisplayName, roleName, roleArn, role.PolicyDocument, role.PolicyArns, role.IAMGroups, ttl, roleSessionName, role.SessionTags, role.ExternalID)
case federationTokenCred:
return b.getFederationToken(ctx, req.Storage, req.DisplayName, roleName, role.PolicyDocument, role.PolicyArns, role.IAMGroups, ttl)
default:
Expand Down
12 changes: 11 additions & 1 deletion builtin/logical/aws/secret_access_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ func (b *backend) getFederationToken(ctx context.Context, s logical.Storage,

func (b *backend) assumeRole(ctx context.Context, s logical.Storage,
displayName, roleName, roleArn, policy string, policyARNs []string,
iamGroups []string, lifeTimeInSeconds int64, roleSessionName string) (*logical.Response, error,
iamGroups []string, lifeTimeInSeconds int64, roleSessionName string, sessionTags map[string]string, externalID string) (*logical.Response, error,
) {
// grab any IAM group policies associated with the vault role, both inline
// and managed
Expand Down Expand Up @@ -241,6 +241,16 @@ func (b *backend) assumeRole(ctx context.Context, s logical.Storage,
if len(policyARNs) > 0 {
assumeRoleInput.SetPolicyArns(convertPolicyARNs(policyARNs))
}
if externalID != "" {
assumeRoleInput.SetExternalId(externalID)
}
if len(sessionTags) > 0 {
var tags []*sts.Tag
for k, v := range sessionTags {
tags = append(tags, &sts.Tag{Key: aws.String(k), Value: aws.String(v)})
}
assumeRoleInput.SetTags(tags)
}
tokenResp, err := stsClient.AssumeRoleWithContext(ctx, assumeRoleInput)
if err != nil {
return logical.ErrorResponse("Error assuming role: %s", err), awsutil.CheckAWSError(err)
Expand Down

0 comments on commit 8470de9

Please sign in to comment.