mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-18 18:38:08 -05:00
AWS secrets: add support for STS session tags (#27620)
Adds support for configuring session tags for assume role operations.
This commit is contained in:
parent
8f26f19950
commit
a05deb5f37
7 changed files with 445 additions and 92 deletions
|
|
@ -36,6 +36,23 @@ import (
|
|||
|
||||
var initSetup sync.Once
|
||||
|
||||
// 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
|
||||
const allowAllButDescribeAzs = `{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"NotAction": "ec2:DescribeAvailabilityZones",
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
type mockIAMClient struct {
|
||||
iamiface.IAMAPI
|
||||
}
|
||||
|
|
@ -97,7 +114,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...")
|
||||
|
|
@ -123,7 +140,8 @@ func TestAcceptanceBackend_basicSTS(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestBackend_policyCrud(t *testing.T) {
|
||||
// TestBackend_policyCRUD tests the CRUD operations for a policy.
|
||||
func TestBackend_policyCRUD(t *testing.T) {
|
||||
t.Parallel()
|
||||
compacted, err := compactJSON(testDynamoPolicy)
|
||||
if err != nil {
|
||||
|
|
@ -252,23 +270,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(),
|
||||
|
|
@ -278,23 +305,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 {
|
||||
|
|
@ -315,21 +342,21 @@ func createUser(t *testing.T, userName string, accessKey *awsAccessKey) {
|
|||
// do anything
|
||||
// 4. Generate API creds to get an actual access key and secret key
|
||||
timebombPolicyTemplate := `{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Deny",
|
||||
"Action": "*",
|
||||
"Resource": "*",
|
||||
"Condition": {
|
||||
"DateGreaterThan": {
|
||||
"aws:CurrentTime": "%s"
|
||||
}
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Deny",
|
||||
"Action": "*",
|
||||
"Resource": "*",
|
||||
"Condition": {
|
||||
"DateGreaterThan": {
|
||||
"aws:CurrentTime": "%s"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
validity := time.Duration(2 * time.Hour)
|
||||
expiry := time.Now().Add(validity)
|
||||
timebombPolicy := fmt.Sprintf(timebombPolicyTemplate, expiry.Format(time.RFC3339))
|
||||
|
|
@ -657,7 +684,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,
|
||||
|
|
@ -909,6 +936,8 @@ func testAccStepReadPolicy(t *testing.T, name string, value string) logicaltest.
|
|||
"iam_groups": []string(nil),
|
||||
"iam_tags": map[string]string(nil),
|
||||
"mfa_serial_number": "",
|
||||
"session_tags": map[string]string(nil),
|
||||
"external_id": "",
|
||||
}
|
||||
if !reflect.DeepEqual(resp.Data, expected) {
|
||||
return fmt.Errorf("bad: got: %#v\nexpected: %#v", resp.Data, expected)
|
||||
|
|
@ -1107,22 +1136,7 @@ func TestAcceptanceBackend_iamUserGroups(t *testing.T) {
|
|||
func TestAcceptanceBackend_AssumedRoleWithPolicyDoc(t *testing.T) {
|
||||
t.Parallel()
|
||||
roleName := generateUniqueRoleName(t.Name())
|
||||
// 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": "*"
|
||||
}]
|
||||
}
|
||||
`
|
||||
|
||||
awsAccountID, err := getAccountID()
|
||||
if err != nil {
|
||||
t.Logf("Unable to retrive user via sts:GetCallerIdentity: %#v", err)
|
||||
|
|
@ -1137,7 +1151,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)
|
||||
|
|
@ -1173,7 +1187,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)
|
||||
},
|
||||
|
|
@ -1194,22 +1208,7 @@ func TestAcceptanceBackend_AssumedRoleWithGroups(t *testing.T) {
|
|||
t.Parallel()
|
||||
roleName := generateUniqueRoleName(t.Name())
|
||||
groupName := generateUniqueGroupName(t.Name())
|
||||
// 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": "*"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
awsAccountID, err := getAccountID()
|
||||
if err != nil {
|
||||
t.Logf("Unable to retrive user via sts:GetCallerIdentity: %#v", err)
|
||||
|
|
@ -1225,7 +1224,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...")
|
||||
|
|
@ -1247,6 +1246,62 @@ func TestAcceptanceBackend_AssumedRoleWithGroups(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
// TestAcceptanceBackend_AssumedRoleWithSessionTags tests that session tags are
|
||||
// passed to the assumed role.
|
||||
func TestAcceptanceBackend_AssumedRoleWithSessionTags(t *testing.T) {
|
||||
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")
|
||||
}
|
||||
|
||||
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())
|
||||
|
|
@ -1328,6 +1383,7 @@ func TestAcceptanceBackend_FederationTokenWithGroups(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
// TestAcceptanceBackend_SessionToken
|
||||
func TestAcceptanceBackend_SessionToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
userName := generateUniqueUserName(t.Name())
|
||||
|
|
@ -1427,7 +1483,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)
|
||||
},
|
||||
|
|
@ -1443,7 +1499,8 @@ func TestAcceptanceBackend_RoleDefaultSTSTTL(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestBackend_policyArnCrud(t *testing.T) {
|
||||
// TestBackend_policyArnCRUD test the CRUD operations for policy ARNs.
|
||||
func TestBackend_policyArnCRUD(t *testing.T) {
|
||||
t.Parallel()
|
||||
logicaltest.Test(t, logicaltest.TestCase{
|
||||
AcceptanceTest: false,
|
||||
|
|
@ -1483,6 +1540,8 @@ func testAccStepReadArnPolicy(t *testing.T, name string, value string) logicalte
|
|||
"iam_groups": []string(nil),
|
||||
"iam_tags": map[string]string(nil),
|
||||
"mfa_serial_number": "",
|
||||
"session_tags": map[string]string(nil),
|
||||
"external_id": "",
|
||||
}
|
||||
if !reflect.DeepEqual(resp.Data, expected) {
|
||||
return fmt.Errorf("bad: got: %#v\nexpected: %#v", resp.Data, expected)
|
||||
|
|
@ -1503,7 +1562,8 @@ func testAccStepWriteArnRoleRef(t *testing.T, vaultRoleName, awsRoleName, awsAcc
|
|||
}
|
||||
}
|
||||
|
||||
func TestBackend_iamGroupsCrud(t *testing.T) {
|
||||
// TestBackend_iamGroupsCRUD tests CRUD operations for IAM groups.
|
||||
func TestBackend_iamGroupsCRUD(t *testing.T) {
|
||||
t.Parallel()
|
||||
logicaltest.Test(t, logicaltest.TestCase{
|
||||
AcceptanceTest: false,
|
||||
|
|
@ -1554,6 +1614,8 @@ func testAccStepReadIamGroups(t *testing.T, name string, groups []string) logica
|
|||
"iam_groups": groups,
|
||||
"iam_tags": map[string]string(nil),
|
||||
"mfa_serial_number": "",
|
||||
"session_tags": map[string]string(nil),
|
||||
"external_id": "",
|
||||
}
|
||||
if !reflect.DeepEqual(resp.Data, expected) {
|
||||
return fmt.Errorf("bad: got: %#v\nexpected: %#v", resp.Data, expected)
|
||||
|
|
@ -1564,7 +1626,8 @@ func testAccStepReadIamGroups(t *testing.T, name string, groups []string) logica
|
|||
}
|
||||
}
|
||||
|
||||
func TestBackend_iamTagsCrud(t *testing.T) {
|
||||
// TestBackend_iamTagsCRUD tests the CRUD operations for IAM tags.
|
||||
func TestBackend_iamTagsCRUD(t *testing.T) {
|
||||
logicaltest.Test(t, logicaltest.TestCase{
|
||||
AcceptanceTest: false,
|
||||
LogicalBackend: getBackend(t),
|
||||
|
|
@ -1614,6 +1677,176 @@ func testAccStepReadIamTags(t *testing.T, name string, tags map[string]string) l
|
|||
"iam_groups": []string(nil),
|
||||
"iam_tags": tags,
|
||||
"mfa_serial_number": "",
|
||||
"session_tags": map[string]string(nil),
|
||||
"external_id": "",
|
||||
}
|
||||
if !reflect.DeepEqual(resp.Data, expected) {
|
||||
return fmt.Errorf("bad: got: %#v\nexpected: %#v", resp.Data, expected)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TestBackend_stsSessionTagsCRUD tests the CRUD operations for STS session tags.
|
||||
func TestBackend_stsSessionTagsCRUD(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tagParams0 := map[string]string{"tag1": "value1", "tag2": "value2"}
|
||||
tagParams1 := map[string]string{"tag1": "value1", "tag2": "value4", "tag3": "value3"}
|
||||
|
||||
// list of tags in the form of "key=value"
|
||||
tagParamsList0 := []string{"key1=value1", "key2=value2"}
|
||||
tagParamsList0Expect := map[string]string{"key1": "value1", "key2": "value2"}
|
||||
tagParamsList1 := []string{"key1=value2", "key3=value4"}
|
||||
tagParamsList1Expect := map[string]string{"key1": "value2", "key3": "value4"}
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
expectTags []map[string]string
|
||||
tagsParams []any
|
||||
externalIDs []string
|
||||
}
|
||||
|
||||
for _, tt := range []testCase{
|
||||
{
|
||||
name: "mapped-only",
|
||||
tagsParams: []any{
|
||||
tagParams0,
|
||||
map[string]string{},
|
||||
tagParams1,
|
||||
},
|
||||
expectTags: []map[string]string{
|
||||
tagParams0,
|
||||
{},
|
||||
tagParams1,
|
||||
},
|
||||
externalIDs: []string{"foo", "", "bar"},
|
||||
},
|
||||
{
|
||||
name: "string-list-only",
|
||||
tagsParams: []any{
|
||||
tagParamsList0,
|
||||
tagParamsList1,
|
||||
},
|
||||
expectTags: []map[string]string{
|
||||
tagParamsList0Expect,
|
||||
tagParamsList1Expect,
|
||||
},
|
||||
externalIDs: []string{"foo"},
|
||||
},
|
||||
{
|
||||
name: "mixed-param-types",
|
||||
tagsParams: []any{
|
||||
tagParams0,
|
||||
tagParamsList0,
|
||||
tagParams1,
|
||||
tagParamsList1,
|
||||
},
|
||||
expectTags: []map[string]string{
|
||||
tagParams0,
|
||||
tagParamsList0Expect,
|
||||
tagParams1,
|
||||
tagParamsList1Expect,
|
||||
},
|
||||
externalIDs: []string{"foo", "bar"},
|
||||
},
|
||||
{
|
||||
name: "unset-tags",
|
||||
tagsParams: []any{
|
||||
tagParams0,
|
||||
map[string]string{},
|
||||
},
|
||||
expectTags: []map[string]string{
|
||||
tagParams0,
|
||||
{},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
steps := []logicaltest.TestStep{
|
||||
testAccStepConfig(t),
|
||||
}
|
||||
|
||||
if len(tt.tagsParams) != len(tt.expectTags) {
|
||||
t.Fatalf("invalid test case: test case params and expect must have the same length")
|
||||
}
|
||||
|
||||
// lastNonEmptyExternalID is used to store the last non-empty external ID for the
|
||||
// test case. The value will is expected to be set on the role. Setting the value
|
||||
// to an empty string has no effect on update operations.
|
||||
var lastNonEmptyExternalID string
|
||||
for idx, params := range tt.tagsParams {
|
||||
var externalID string
|
||||
if len(tt.externalIDs) > idx {
|
||||
externalID = tt.externalIDs[idx]
|
||||
}
|
||||
if externalID != "" {
|
||||
lastNonEmptyExternalID = externalID
|
||||
}
|
||||
steps = append(steps, testAccStepWriteSTSSessionTags(t, tt.name, params, externalID))
|
||||
steps = append(steps, testAccStepReadSTSSessionTags(t, tt.name, tt.expectTags[idx], lastNonEmptyExternalID, false))
|
||||
}
|
||||
steps = append(
|
||||
steps,
|
||||
testAccStepDeletePolicy(t, tt.name),
|
||||
testAccStepReadSTSSessionTags(t, tt.name, nil, "", true),
|
||||
)
|
||||
logicaltest.Test(t, logicaltest.TestCase{
|
||||
AcceptanceTest: false,
|
||||
LogicalBackend: getBackend(t),
|
||||
Steps: steps,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testAccStepWriteSTSSessionTags(t *testing.T, name string, tags any, externalID string) logicaltest.TestStep {
|
||||
t.Helper()
|
||||
|
||||
data := map[string]interface{}{
|
||||
"credential_type": assumedRoleCred,
|
||||
"session_tags": tags,
|
||||
}
|
||||
if externalID != "" {
|
||||
data["external_id"] = externalID
|
||||
}
|
||||
return logicaltest.TestStep{
|
||||
Operation: logical.UpdateOperation,
|
||||
Path: "roles/" + name,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func testAccStepReadSTSSessionTags(t *testing.T, name string, tags any, externalID string, expectNilResp bool) logicaltest.TestStep {
|
||||
t.Helper()
|
||||
|
||||
return logicaltest.TestStep{
|
||||
Operation: logical.ReadOperation,
|
||||
Path: "roles/" + name,
|
||||
Check: func(resp *logical.Response) error {
|
||||
if resp == nil {
|
||||
if expectNilResp {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("vault response not received")
|
||||
}
|
||||
|
||||
expected := map[string]interface{}{
|
||||
"policy_arns": []string(nil),
|
||||
"role_arns": []string(nil),
|
||||
"policy_document": "",
|
||||
"credential_type": assumedRoleCred,
|
||||
"default_sts_ttl": int64(0),
|
||||
"max_sts_ttl": int64(0),
|
||||
"user_path": "",
|
||||
"permissions_boundary_arn": "",
|
||||
"iam_groups": []string(nil),
|
||||
"iam_tags": map[string]string(nil),
|
||||
"mfa_serial_number": "",
|
||||
"session_tags": tags,
|
||||
"external_id": externalID,
|
||||
}
|
||||
if !reflect.DeepEqual(resp.Data, expected) {
|
||||
return fmt.Errorf("bad: got: %#v\nexpected: %#v", resp.Data, expected)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
@ -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 != "" {
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ package aws
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -157,7 +157,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)
|
||||
case sessionTokenCred:
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ func (b *backend) getSessionToken(ctx context.Context, s logical.Storage, serial
|
|||
|
||||
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
|
||||
|
|
@ -295,6 +295,19 @@ func (b *backend) assumeRole(ctx context.Context, s logical.Storage,
|
|||
if len(policyARNs) > 0 {
|
||||
assumeRoleInput.SetPolicyArns(convertPolicyARNs(policyARNs))
|
||||
}
|
||||
if externalID != "" {
|
||||
assumeRoleInput.SetExternalId(externalID)
|
||||
}
|
||||
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)
|
||||
|
|
|
|||
5
changelog/27620.txt
Normal file
5
changelog/27620.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
```release-note:feature
|
||||
**AWS secrets engine STS session tags support**: Adds support for setting STS
|
||||
session tags when generating temporary credentials using the AWS secrets
|
||||
engine.
|
||||
```
|
||||
|
|
@ -31,7 +31,7 @@ files, or IAM/ECS instances.
|
|||
|
||||
- Static credentials provided to the API as a payload
|
||||
|
||||
- [Plugin workload identity federation](/vault/docs/secrets/aws#plugin-workload-identity-federation-wif)
|
||||
- [Plugin workload identity federation](/vault/docs/secrets/aws#plugin-workload-identity-federation-wif)
|
||||
credentials
|
||||
|
||||
- Credentials in the `AWS_ACCESS_KEY`, `AWS_SECRET_KEY`, and `AWS_REGION`
|
||||
|
|
@ -60,15 +60,15 @@ valid AWS credentials with proper permissions.
|
|||
- `secret_key` `(string: "")` – Specifies the AWS secret access key. Must be provided with
|
||||
`access_key`.
|
||||
|
||||
- `role_arn` `(string: "")` – <EnterpriseAlert product="vault" inline /> Role ARN to assume
|
||||
- `role_arn` `(string: "")` – <EnterpriseAlert product="vault" inline /> Role ARN to assume
|
||||
for plugin workload identity federation. Required with `identity_token_audience`.
|
||||
|
||||
- `identity_token_audience` `(string: "")` - <EnterpriseAlert product="vault" inline /> The
|
||||
audience claim value for plugin identity tokens. Must match an allowed audience configured
|
||||
- `identity_token_audience` `(string: "")` - <EnterpriseAlert product="vault" inline /> The
|
||||
audience claim value for plugin identity tokens. Must match an allowed audience configured
|
||||
for the target [IAM OIDC identity provider](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html#manage-oidc-provider-console).
|
||||
Mutually exclusive with `access_key`.
|
||||
|
||||
- `identity_token_ttl` `(string/int: 3600)` - <EnterpriseAlert product="vault" inline /> The
|
||||
- `identity_token_ttl` `(string/int: 3600)` - <EnterpriseAlert product="vault" inline /> The
|
||||
TTL of generated tokens. Defaults to 1 hour. Uses [duration format strings](/vault/docs/concepts/duration-format).
|
||||
|
||||
- `region` `(string: <optional>)` – Specifies the AWS region. If not set it
|
||||
|
|
@ -316,6 +316,13 @@ updated with the new attributes.
|
|||
TTL are capped to `max_sts_ttl`). Valid only when `credential_type` is one of
|
||||
`assumed_role` or `federation_token`.
|
||||
|
||||
- `session_tags` `(list: [])` - The set of key-value pairs to be included as tags for the STS session.
|
||||
Allowed formats are a map of strings or a list of strings in the format `key=value`.
|
||||
Valid only when `credential_type` is set to `assumed_role`.
|
||||
|
||||
- `external_id` `(string)` - The external ID to use when assuming the role.
|
||||
Valid only when `credential_type` is set to `assumed_role`.
|
||||
|
||||
- `user_path` `(string)` - The path for the user name. Valid only when
|
||||
`credential_type` is `iam_user`. Default is `/`
|
||||
|
||||
|
|
@ -645,7 +652,7 @@ $ curl \
|
|||
"data": {
|
||||
"access_key": "AKIA...",
|
||||
"secret_key": "xlCs...",
|
||||
"session_token": "FwoG...",
|
||||
"session_token": "FwoG..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -660,7 +667,7 @@ to the configured `rotation_period`.
|
|||
Vault will create a new credential upon configuration, and if the maximum number of access keys already exist,
|
||||
Vault will rotate the oldest one. Vault must do this to know the credential. At each rotation period, Vault will
|
||||
continue to prioritize rotating the oldest-existing credential.
|
||||
|
||||
|
||||
For example, if an IAM User has no access keys when onboarded into Vault, then Vault will generate its first access
|
||||
key for the user. On the first rotation, Vault will generate a second access key for the user. It is only upon the
|
||||
next rotation cycle that the first access key will now be rotated.
|
||||
|
|
|
|||
Loading…
Reference in a new issue