diff --git a/builtin/logical/aws/backend_test.go b/builtin/logical/aws/backend_test.go index c7bd2840e931..86acc65bd219 100644 --- a/builtin/logical/aws/backend_test.go +++ b/builtin/logical/aws/backend_test.go @@ -500,6 +500,23 @@ func testAccStepRead(t *testing.T, path, name string, credentialTests []credenti } } +func testAccStepReadTTL(name string, maximumTTL time.Duration) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.ReadOperation, + Path: "creds/" + name, + Check: func(resp *logical.Response) error { + if resp.Secret == nil { + return fmt.Errorf("bad: nil Secret returned") + } + ttl := resp.Secret.TTL + if ttl > maximumTTL { + return fmt.Errorf("bad: ttl of %d greater than maximum of %d", ttl/time.Second, maximumTTL/time.Second) + } + return nil + }, + } +} + func describeInstancesTest(accessKey, secretKey, token string) error { creds := credentials.NewStaticCredentials(accessKey, secretKey, token) awsConfig := &aws.Config{ @@ -634,6 +651,7 @@ func testAccStepReadPolicy(t *testing.T, name string, value string) logicaltest. "role_arns": []string(nil), "policy_document": value, "credential_types": []string{iamUserCred, federationTokenCred}, + "default_sts_ttl": int64(0), } if !reflect.DeepEqual(resp.Data, expected) { return fmt.Errorf("bad: got: %#v\nexpected: %#v", resp.Data, expected) @@ -730,6 +748,7 @@ func TestBackend_iamUserManagedInlinePolicies(t *testing.T) { "policy_arns": []string{ec2PolicyArn, iamPolicyArn}, "credential_types": []string{iamUserCred}, "role_arns": []string(nil), + "default_sts_ttl": int64(0), } logicaltest.Test(t, logicaltest.TestCase{ AcceptanceTest: true, @@ -796,6 +815,40 @@ func TestBackend_AssumedRoleWithPolicyDoc(t *testing.T) { }) } +func TestBackend_RoleDefaultSTSTTL(t *testing.T) { + t.Parallel() + roleName := generateUniqueName(t.Name()) + minAwsAssumeRoleDuration := 900 + 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") + } + roleData := map[string]interface{}{ + "role_arns": []string{fmt.Sprintf("arn:aws:iam::%s:role/%s", awsAccountID, roleName)}, + "credential_type": assumedRoleCred, + "default_sts_ttl": minAwsAssumeRoleDuration, + } + logicaltest.Test(t, logicaltest.TestCase{ + AcceptanceTest: true, + PreCheck: func() { + testAccPreCheck(t) + createRole(t, roleName, awsAccountID) + log.Println("[WARN] Sleeping for 10 seconds waiting for AWS...") + time.Sleep(10 * time.Second) + }, + Backend: getBackend(t), + Steps: []logicaltest.TestStep{ + testAccStepConfig(t), + testAccStepWriteRole(t, "test", roleData), + testAccStepReadTTL("test", time.Duration(minAwsAssumeRoleDuration)*time.Second), // allow a little slack + }, + Teardown: func() error { + return deleteTestRole(roleName) + }, + }) +} + func TestBackend_policyArnCrud(t *testing.T) { t.Parallel() logicaltest.Test(t, logicaltest.TestCase{ @@ -829,6 +882,7 @@ func testAccStepReadArnPolicy(t *testing.T, name string, value string) logicalte "role_arns": []string(nil), "policy_document": "", "credential_types": []string{iamUserCred}, + "default_sts_ttl": int64(0), } if !reflect.DeepEqual(resp.Data, expected) { return fmt.Errorf("bad: got: %#v\nexpected: %#v", resp.Data, expected) diff --git a/builtin/logical/aws/path_roles.go b/builtin/logical/aws/path_roles.go index 531e746ccfcc..2ac166b00639 100644 --- a/builtin/logical/aws/path_roles.go +++ b/builtin/logical/aws/path_roles.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "strings" + "time" "github.com/aws/aws-sdk-go/aws/arn" "github.com/hashicorp/vault/helper/consts" @@ -61,6 +62,11 @@ will be passed in as the Policy parameter to the AssumeRole or GetFederationToken API call, acting as a filter on permissions available.`, }, + "default_sts_ttl": &framework.FieldSchema{ + 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), + }, + "arn": &framework.FieldSchema{ Type: framework.TypeString, Description: `Deprecated; use role_arns or policy_arns instead. ARN Reference to a managed policy @@ -206,6 +212,16 @@ func (b *backend) pathRolesWrite(ctx context.Context, req *logical.Request, d *f roleEntry.PolicyDocument = compacted } + if defaultSTSTTLRaw, ok := d.GetOk("default_sts_ttl"); ok { + if legacyRole != "" { + return logical.ErrorResponse("cannot supply deprecated role or policy parameters with default_sts_ttl"), nil + } + if !strutil.StrListContains(roleEntry.CredentialTypes, assumedRoleCred) && !strutil.StrListContains(roleEntry.CredentialTypes, federationTokenCred) { + return logical.ErrorResponse(fmt.Sprintf("default_sts_ttl parameter only valid for %s and %s credential types", assumedRoleCred, federationTokenCred)), nil + } + roleEntry.DefaultSTSTTL = time.Duration(defaultSTSTTLRaw.(int)) * time.Second + } + if legacyRole != "" { roleEntry = upgradeLegacyPolicyEntry(legacyRole) if roleEntry.InvalidData != "" { @@ -374,13 +390,14 @@ func setAwsRole(ctx context.Context, s logical.Storage, roleName string, roleEnt } type awsRoleEntry struct { - CredentialTypes []string `json:"credential_types"` // Entries must all be in the set of ("iam_user", "assumed_role", "federation_token") - PolicyArns []string `json:"policy_arns"` // ARNs of managed policies to attach to an IAM user - RoleArns []string `json:"role_arns"` // ARNs of roles to assume for AssumedRole credentials - 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 - 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 + CredentialTypes []string `json:"credential_types"` // Entries must all be in the set of ("iam_user", "assumed_role", "federation_token") + PolicyArns []string `json:"policy_arns"` // ARNs of managed policies to attach to an IAM user + RoleArns []string `json:"role_arns"` // ARNs of roles to assume for AssumedRole credentials + 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 + 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 + DefaultSTSTTL time.Duration `json:"default_sts_ttl"` // Default TTL for STS credentials } func (r *awsRoleEntry) toResponseData() map[string]interface{} { @@ -389,6 +406,7 @@ func (r *awsRoleEntry) toResponseData() map[string]interface{} { "policy_arns": r.PolicyArns, "role_arns": r.RoleArns, "policy_document": r.PolicyDocument, + "default_sts_ttl": int64(r.DefaultSTSTTL.Seconds()), } if r.InvalidData != "" { respData["invalid_data"] = r.InvalidData diff --git a/builtin/logical/aws/path_roles_test.go b/builtin/logical/aws/path_roles_test.go index d3705c0bd97d..b0d4bff38d7a 100644 --- a/builtin/logical/aws/path_roles_test.go +++ b/builtin/logical/aws/path_roles_test.go @@ -23,6 +23,7 @@ func TestBackend_PathListRoles(t *testing.T) { roleData := map[string]interface{}{ "role_arns": []string{"arn:aws:iam::123456789012:role/path/RoleName"}, "credential_type": assumedRoleCred, + "default_sts_ttl": 3600, } roleReq := &logical.Request{ diff --git a/builtin/logical/aws/path_user.go b/builtin/logical/aws/path_user.go index 7ea0def82c6e..99aa8bac3984 100644 --- a/builtin/logical/aws/path_user.go +++ b/builtin/logical/aws/path_user.go @@ -58,7 +58,16 @@ func (b *backend) pathCredsRead(ctx context.Context, req *logical.Request, d *fr "Role '%s' not found", roleName)), nil } - ttl := int64(d.Get("ttl").(int)) + var ttl int64 + ttlRaw, ok := d.GetOk("ttl") + switch { + case ok: + ttl = int64(ttlRaw.(int)) + case role.DefaultSTSTTL > 0: + ttl = int64(role.DefaultSTSTTL.Seconds()) + default: + ttl = int64(d.Get("ttl").(int)) + } roleArn := d.Get("role_arn").(string) var credentialType string diff --git a/website/source/api/secret/aws/index.html.md b/website/source/api/secret/aws/index.html.md index fd246dda88e6..e7adcad75447 100644 --- a/website/source/api/secret/aws/index.html.md +++ b/website/source/api/secret/aws/index.html.md @@ -220,6 +220,11 @@ updated with the new attributes. user has. With `assumed_role` and `federation_token`, the policy document will act as a filter on what the credentials can do. +- `default_sts_ttl` `(string)` - The default TTL for STS credentials. When a TTL is not + specified when STS credentials are requested, and a default TTL is specified + on the role, then this default TTL will be used. Valid only when + `credential_type` is one of `assumed_role` or `federation_token`. + Legacy parameters: These parameters are supported for backwards compatibility only. They cannot be @@ -392,7 +397,9 @@ credentials retrieved through `/aws/creds` must be of the `iam_user` type. required otherwise. - `ttl` `(string: "3600s")` – Specifies the TTL for the use of the STS token. This is specified as a string with a duration suffix. Valid only when - `credential_type` is `assumed_role` or `federation_token`. AWS places limits + `credential_type` is `assumed_role` or `federation_token`. When not specified, + the `default_sts_ttl` set for the role will be used. If that is also not set, then + the default value of `3600s` will be used. AWS places limits on the maximum TTL allowed. See the AWS documentation on the `DurationSeconds` parameter for [AssumeRole](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html)