Skip to content

Commit

Permalink
AWS secrets: add support for STS session tags
Browse files Browse the repository at this point in the history
Adds support for configuring session tags for assume role operations.
  • Loading branch information
benashz committed Jun 26, 2024
1 parent 41caa2d commit 8148b7d
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 48 deletions.
126 changes: 103 additions & 23 deletions builtin/logical/aws/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func TestAcceptanceBackend_basicSTS(t *testing.T) {
PreCheck: func() {
testAccPreCheck(t)
createUser(t, userName, accessKey)
createRole(t, roleName, awsAccountID, []string{ec2PolicyArn})
createRole(t, roleName, awsAccountID, []string{ec2PolicyArn}, nil)
// Sleep sometime because AWS is eventually consistent
// Both the createUser and createRole depend on this
log.Println("[WARN] Sleeping for 10 seconds waiting for AWS...")
Expand Down Expand Up @@ -252,23 +252,32 @@ func getAccountID() (string, error) {
return *res.Account, nil
}

func createRole(t *testing.T, roleName, awsAccountID string, policyARNs []string) {
const testRoleAssumePolicy = `{
func createRole(t *testing.T, roleName, awsAccountID string, policyARNs, extraTrustPolicies []string) {
t.Helper()

trustPolicyStmts := append([]string{
fmt.Sprintf(`
{
"Effect":"Allow",
"Principal": {
"AWS": "arn:aws:iam::%s:root"
},
"Action": [
"sts:AssumeRole",
"sts:SetSourceIdentity"
]
}`, awsAccountID),
},
extraTrustPolicies...)

testRoleAssumePolicy := fmt.Sprintf(`{
"Version": "2012-10-17",
"Statement": [
{
"Effect":"Allow",
"Principal": {
"AWS": "arn:aws:iam::%s:root"
},
"Action": [
"sts:AssumeRole",
"sts:SetSourceIdentity"
]
}
%s
]
}
`
`, strings.Join(trustPolicyStmts, ","))

awsConfig := &aws.Config{
Region: aws.String("us-east-1"),
HTTPClient: cleanhttp.DefaultClient(),
Expand All @@ -278,23 +287,23 @@ func createRole(t *testing.T, roleName, awsAccountID string, policyARNs []string
t.Fatal(err)
}
svc := iam.New(sess)
trustPolicy := fmt.Sprintf(testRoleAssumePolicy, awsAccountID)

params := &iam.CreateRoleInput{
AssumeRolePolicyDocument: aws.String(trustPolicy),
AssumeRolePolicyDocument: aws.String(testRoleAssumePolicy),
RoleName: aws.String(roleName),
Path: aws.String("/"),
}

log.Printf("[INFO] AWS CreateRole: %s", roleName)
if _, err := svc.CreateRole(params); err != nil {
output, err := svc.CreateRole(params)
if err != nil {
t.Fatalf("AWS CreateRole failed: %v", err)
}

for _, policyARN := range policyARNs {
attachment := &iam.AttachRolePolicyInput{
PolicyArn: aws.String(policyARN),
RoleName: aws.String(roleName), // Required
RoleName: output.Role.RoleName,
}
_, err = svc.AttachRolePolicy(attachment)
if err != nil {
Expand Down Expand Up @@ -657,7 +666,7 @@ func testAccStepRotateRoot(oldAccessKey *awsAccessKey) logicaltest.TestStep {
}
}

func testAccStepRead(t *testing.T, path, name string, credentialTests []credentialTestFunc) logicaltest.TestStep {
func testAccStepRead(_ *testing.T, path, name string, credentialTests []credentialTestFunc) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.ReadOperation,
Path: path + "/" + name,
Expand Down Expand Up @@ -1137,7 +1146,7 @@ func TestAcceptanceBackend_AssumedRoleWithPolicyDoc(t *testing.T) {
AcceptanceTest: true,
PreCheck: func() {
testAccPreCheck(t)
createRole(t, roleName, awsAccountID, []string{ec2PolicyArn})
createRole(t, roleName, awsAccountID, []string{ec2PolicyArn}, nil)
// Sleep sometime because AWS is eventually consistent
log.Println("[WARN] Sleeping for 10 seconds waiting for AWS...")
time.Sleep(10 * time.Second)
Expand Down Expand Up @@ -1173,7 +1182,7 @@ func TestAcceptanceBackend_AssumedRoleWithPolicyARN(t *testing.T) {
AcceptanceTest: true,
PreCheck: func() {
testAccPreCheck(t)
createRole(t, roleName, awsAccountID, []string{ec2PolicyArn, iamPolicyArn})
createRole(t, roleName, awsAccountID, []string{ec2PolicyArn, iamPolicyArn}, nil)
log.Printf("[WARN] Sleeping for 10 seconds waiting for AWS...")
time.Sleep(10 * time.Second)
},
Expand Down Expand Up @@ -1225,7 +1234,7 @@ func TestAcceptanceBackend_AssumedRoleWithGroups(t *testing.T) {
AcceptanceTest: true,
PreCheck: func() {
testAccPreCheck(t)
createRole(t, roleName, awsAccountID, []string{ec2PolicyArn})
createRole(t, roleName, awsAccountID, []string{ec2PolicyArn}, nil)
createGroup(t, groupName, allowAllButDescribeAzs, []string{})
// Sleep sometime because AWS is eventually consistent
log.Println("[WARN] Sleeping for 10 seconds waiting for AWS...")
Expand All @@ -1247,6 +1256,77 @@ func TestAcceptanceBackend_AssumedRoleWithGroups(t *testing.T) {
})
}

func TestAcceptanceBackend_AssumedRoleWithSessionTags(t *testing.T) {

Check failure on line 1259 in builtin/logical/aws/backend_test.go

View workflow job for this annotation

GitHub Actions / Code checks

Test TestAcceptanceBackend_AssumedRoleWithSessionTags is missing a go doc
t.Parallel()
roleName := generateUniqueRoleName(t.Name())
awsAccountID, err := getAccountID()
if err != nil {
t.Logf("Unable to retrive user via sts:GetCallerIdentity: %#v", err)
t.Skip("Could not determine AWS account ID from sts:GetCallerIdentity for acceptance tests, skipping")
}

// This looks a bit curious. The policy document and the role document act
// as a logical intersection of policies. The role allows ec2:Describe*
// (among other permissions). This policy allows everything BUT
// ec2:DescribeAvailabilityZones. Thus, the logical intersection of the two
// is all ec2:Describe* EXCEPT ec2:DescribeAvailabilityZones, and so the
// describeAZs call should fail
allowAllButDescribeAzs := `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"NotAction": "ec2:DescribeAvailabilityZones",
"Resource": "*"
}
]
}`

roleARN := fmt.Sprintf("arn:aws:iam::%s:role/%s", awsAccountID, roleName)
roleData := map[string]interface{}{
"policy_document": allowAllButDescribeAzs,
"role_arns": []string{roleARN},
"credential_type": assumedRoleCred,
"session_tags": map[string]string{
"foo": "bar",
"baz": "qux",
},
}

// allowSessionTagsPolicy allows the role to tag the session, it needs to be
// included in the trust policy.
allowSessionTagsPolicy := fmt.Sprintf(`
{
"Sid": "AllowPassSessionTagsAndTransitive",
"Effect": "Allow",
"Action": "sts:TagSession",
"Principal": {
"AWS": "arn:aws:iam::%s:root"
}
}
`, awsAccountID)

logicaltest.Test(t, logicaltest.TestCase{
AcceptanceTest: true,
PreCheck: func() {
testAccPreCheck(t)
createRole(t, roleName, awsAccountID, []string{ec2PolicyArn}, []string{allowSessionTagsPolicy})
log.Println("[WARN] Sleeping for 10 seconds waiting for AWS...")
time.Sleep(10 * time.Second)
},
LogicalBackend: getBackend(t),
Steps: []logicaltest.TestStep{
testAccStepConfig(t),
testAccStepWriteRole(t, "test", roleData),
testAccStepRead(t, "sts", "test", []credentialTestFunc{describeInstancesTest, describeAzsTestUnauthorized}),
testAccStepRead(t, "creds", "test", []credentialTestFunc{describeInstancesTest, describeAzsTestUnauthorized}),
},
Teardown: func() error {
return deleteTestRole(roleName)
},
})
}

func TestAcceptanceBackend_FederationTokenWithPolicyARN(t *testing.T) {
t.Parallel()
userName := generateUniqueUserName(t.Name())
Expand Down Expand Up @@ -1427,7 +1507,7 @@ func TestAcceptanceBackend_RoleDefaultSTSTTL(t *testing.T) {
AcceptanceTest: true,
PreCheck: func() {
testAccPreCheck(t)
createRole(t, roleName, awsAccountID, []string{ec2PolicyArn})
createRole(t, roleName, awsAccountID, []string{ec2PolicyArn}, nil)
log.Println("[WARN] Sleeping for 10 seconds waiting for AWS...")
time.Sleep(10 * time.Second)
},
Expand Down
38 changes: 37 additions & 1 deletion builtin/logical/aws/path_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,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, %s, and %s credential types when no TTL is explicitly requested with the credentials", assumedRoleCred, federationTokenCred, sessionTokenCred),
Expand Down Expand Up @@ -341,6 +357,14 @@ func (b *backend) pathRolesWrite(ctx context.Context, req *logical.Request, d *f
roleEntry.SerialNumber = serialNumber.(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 @@ -527,6 +551,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 @@ -545,6 +571,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 @@ -612,6 +640,14 @@ func (r *awsRoleEntry) validate() error {
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))
}

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
79 changes: 69 additions & 10 deletions builtin/logical/aws/path_roles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ package aws

import (
"context"
"errors"
"reflect"
"strconv"
"strings"
"testing"

"github.com/hashicorp/go-multierror"
"github.com/hashicorp/vault/sdk/logical"
)

Expand Down Expand Up @@ -366,22 +368,74 @@ func TestRoleEntryValidationIamUserCred(t *testing.T) {
CredentialTypes: []string{iamUserCred},
RoleArns: []string{"arn:aws:iam::123456789012:role/SomeRole"},
}
if roleEntry.validate() == nil {
t.Errorf("bad: invalid roleEntry with invalid RoleArns parameter %#v passed validation", roleEntry)
}
assertMultiError(t, roleEntry.validate(),
[]error{
errors.New(
"cannot supply role_arns when credential_type isn't assumed_role",
),
})

roleEntry = awsRoleEntry{
CredentialTypes: []string{iamUserCred},
PolicyArns: []string{adminAccessPolicyARN},
DefaultSTSTTL: 1,
}
if roleEntry.validate() == nil {
t.Errorf("bad: invalid roleEntry with unrecognized DefaultSTSTTL %#v passed validation", roleEntry)
}
assertMultiError(t, roleEntry.validate(),
[]error{
errors.New(
"default_sts_ttl parameter only valid for assumed_role, federation_token, and session_token credential types",
),
})
roleEntry.DefaultSTSTTL = 0

roleEntry.MaxSTSTTL = 1
if roleEntry.validate() == nil {
t.Errorf("bad: invalid roleEntry with unrecognized MaxSTSTTL %#v passed validation", roleEntry)
assertMultiError(t, roleEntry.validate(),
[]error{
errors.New(
"max_sts_ttl parameter only valid for assumed_role, federation_token, and session_token credential types",
),
})
roleEntry.MaxSTSTTL = 0

roleEntry.SessionTags = map[string]string{
"Key1": "Value1",
"Key2": "Value2",
}
assertMultiError(t, roleEntry.validate(),
[]error{
errors.New(
"cannot supply session_tags when credential_type isn't assumed_role",
),
})
roleEntry.SessionTags = nil

roleEntry.ExternalID = "my-ext-id"
assertMultiError(t, roleEntry.validate(),
[]error{
errors.New(
"cannot supply external_id when credential_type isn't assumed_role"),
})
}

func assertMultiError(t *testing.T, err error, expected []error) {
t.Helper()

if err == nil {
t.Errorf("expected error, got nil")
return
}

var multiErr *multierror.Error
if errors.As(err, &multiErr) {
if multiErr.Len() != len(expected) {
t.Errorf("expected %d error, got %d", len(expected), multiErr.Len())
} else {
if !reflect.DeepEqual(expected, multiErr.Errors) {
t.Errorf("expected error %q, actual %q", expected, multiErr.Errors)
}
}
} else {
t.Errorf("expected multierror, got %T", err)
}
}

Expand All @@ -392,8 +446,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
Loading

0 comments on commit 8148b7d

Please sign in to comment.