From 54bf0807c131681cee979abdcfd90674eaf7b6e5 Mon Sep 17 00:00:00 2001 From: Robert <17119716+robmonte@users.noreply.github.com> Date: Wed, 8 Nov 2023 17:06:28 -0600 Subject: [PATCH] secrets/aws: add support for STS Session Tokens with TOTP (#23690) * Add test coverage * Add session_token field, deprecate security_token * Undo auth docs * Update api docs * Add MFA code support --------- Co-authored-by: Graham Christensen Co-authored-by: Sarah Chavis <62406755+schavis@users.noreply.github.com> Co-authored-by: Austin Gebauer <34121980+austingebauer@users.noreply.github.com> --- builtin/logical/aws/backend_test.go | 97 ++++++++++++++++++- builtin/logical/aws/path_roles.go | 38 ++++++-- builtin/logical/aws/path_user.go | 7 ++ builtin/logical/aws/secret_access_keys.go | 66 ++++++++++++- builtin/logical/aws/stepwise_test.go | 2 +- changelog/23690.txt | 3 + ui/app/models/aws-credential.js | 7 +- ui/app/models/role-aws.js | 5 + ui/tests/unit/adapters/aws-credential-test.js | 11 +++ website/content/api-docs/secret/aws.mdx | 64 ++++++++++-- .../docs/commands/pki/health-check.mdx | 6 +- website/content/docs/deprecation/index.mdx | 1 + website/content/docs/secrets/aws.mdx | 72 ++++++++++++-- 13 files changed, 341 insertions(+), 38 deletions(-) create mode 100644 changelog/23690.txt diff --git a/builtin/logical/aws/backend_test.go b/builtin/logical/aws/backend_test.go index 0a886edad7..6aaca8848f 100644 --- a/builtin/logical/aws/backend_test.go +++ b/builtin/logical/aws/backend_test.go @@ -667,7 +667,7 @@ func testAccStepRead(t *testing.T, path, name string, credentialTests []credenti var d struct { AccessKey string `mapstructure:"access_key"` SecretKey string `mapstructure:"secret_key"` - STSToken string `mapstructure:"security_token"` + STSToken string `mapstructure:"session_token"` } if err := mapstructure.Decode(resp.Data, &d); err != nil { return err @@ -684,6 +684,15 @@ func testAccStepRead(t *testing.T, path, name string, credentialTests []credenti } } +func testAccStepReadWithMFA(t *testing.T, path, name, mfaCode string, credentialTests []credentialTestFunc) logicaltest.TestStep { + step := testAccStepRead(t, path, name, credentialTests) + step.Data = map[string]interface{}{ + "mfa_code": mfaCode, + } + + return step +} + func testAccStepReadSTSResponse(name string, maximumTTL time.Duration) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.ReadOperation, @@ -901,6 +910,7 @@ func testAccStepReadPolicy(t *testing.T, name string, value string) logicaltest. "permissions_boundary_arn": "", "iam_groups": []string(nil), "iam_tags": map[string]string(nil), + "mfa_serial_number": "", } if !reflect.DeepEqual(resp.Data, expected) { return fmt.Errorf("bad: got: %#v\nexpected: %#v", resp.Data, expected) @@ -1024,6 +1034,7 @@ func TestAcceptanceBackend_iamUserManagedInlinePoliciesGroups(t *testing.T) { "permissions_boundary_arn": "", "iam_groups": []string{groupName}, "iam_tags": map[string]string(nil), + "mfa_serial_number": "", } logicaltest.Test(t, logicaltest.TestCase{ @@ -1068,6 +1079,7 @@ func TestAcceptanceBackend_iamUserGroups(t *testing.T) { "permissions_boundary_arn": "", "iam_groups": []string{group1Name, group2Name}, "iam_tags": map[string]string(nil), + "mfa_serial_number": "", } logicaltest.Test(t, logicaltest.TestCase{ @@ -1318,6 +1330,86 @@ func TestAcceptanceBackend_FederationTokenWithGroups(t *testing.T) { }) } +func TestAcceptanceBackend_SessionToken(t *testing.T) { + t.Parallel() + userName := generateUniqueUserName(t.Name()) + accessKey := &awsAccessKey{} + + roleData := map[string]interface{}{ + "credential_type": sessionTokenCred, + } + logicaltest.Test(t, logicaltest.TestCase{ + AcceptanceTest: true, + PreCheck: func() { + testAccPreCheck(t) + createUser(t, userName, accessKey) + // Sleep sometime because AWS is eventually consistent + log.Println("[WARN] Sleeping for 10 seconds waiting for AWS...") + time.Sleep(10 * time.Second) + }, + LogicalBackend: getBackend(t), + Steps: []logicaltest.TestStep{ + testAccStepConfigWithCreds(t, accessKey), + testAccStepWriteRole(t, "test", roleData), + testAccStepRead(t, "sts", "test", []credentialTestFunc{listDynamoTablesTest}), + testAccStepRead(t, "creds", "test", []credentialTestFunc{listDynamoTablesTest}), + }, + Teardown: func() error { + return deleteTestUser(accessKey, userName) + }, + }) +} + +// Running this test requires a pre-made IAM user that has the necessary access permissions set +// and a set MFA device. This device serial number along with the other associated values must +// be set to the environment variables in the function below. +// For this reason, the test is currently a manually run-only acceptance test. +func TestAcceptanceBackend_SessionTokenWithMFA(t *testing.T) { + t.Parallel() + + serial, found := os.LookupEnv("AWS_TEST_MFA_SERIAL_NUMBER") + if !found { + t.Skipf("AWS_TEST_MFA_SERIAL_NUMBER not set, skipping") + } + code, found := os.LookupEnv("AWS_TEST_MFA_CODE") + if !found { + t.Skipf("AWS_TEST_MFA_CODE not set, skipping") + } + accessKeyID, found := os.LookupEnv("AWS_TEST_MFA_USER_ACCESS_KEY") + if !found { + t.Skipf("AWS_TEST_MFA_USER_ACCESS_KEY not set, skipping") + } + secretKey, found := os.LookupEnv("AWS_TEST_MFA_USER_SECRET_KEY") + if !found { + t.Skipf("AWS_TEST_MFA_USER_SECRET_KEY not set, skipping") + } + + accessKey := &awsAccessKey{} + accessKey.AccessKeyID = accessKeyID + accessKey.SecretAccessKey = secretKey + + roleData := map[string]interface{}{ + "credential_type": sessionTokenCred, + "mfa_serial_number": serial, + } + logicaltest.Test(t, logicaltest.TestCase{ + AcceptanceTest: true, + PreCheck: func() { + testAccPreCheck(t) + // Sleep sometime because AWS is eventually consistent + log.Println("[WARN] Sleeping for 10 seconds waiting for AWS...") + time.Sleep(10 * time.Second) + }, + LogicalBackend: getBackend(t), + Steps: []logicaltest.TestStep{ + testAccStepConfigWithCreds(t, accessKey), + testAccStepWriteRole(t, "test", roleData), + testAccStepReadWithMFA(t, "sts", "test", code, []credentialTestFunc{listDynamoTablesTest}), + testAccStepReadWithMFA(t, "creds", "test", code, []credentialTestFunc{listDynamoTablesTest}), + }, + }) +} + func TestAcceptanceBackend_RoleDefaultSTSTTL(t *testing.T) { t.Parallel() roleName := generateUniqueRoleName(t.Name()) @@ -1392,6 +1484,7 @@ func testAccStepReadArnPolicy(t *testing.T, name string, value string) logicalte "permissions_boundary_arn": "", "iam_groups": []string(nil), "iam_tags": map[string]string(nil), + "mfa_serial_number": "", } if !reflect.DeepEqual(resp.Data, expected) { return fmt.Errorf("bad: got: %#v\nexpected: %#v", resp.Data, expected) @@ -1462,6 +1555,7 @@ func testAccStepReadIamGroups(t *testing.T, name string, groups []string) logica "permissions_boundary_arn": "", "iam_groups": groups, "iam_tags": map[string]string(nil), + "mfa_serial_number": "", } if !reflect.DeepEqual(resp.Data, expected) { return fmt.Errorf("bad: got: %#v\nexpected: %#v", resp.Data, expected) @@ -1521,6 +1615,7 @@ func testAccStepReadIamTags(t *testing.T, name string, tags map[string]string) l "permissions_boundary_arn": "", "iam_groups": []string(nil), "iam_tags": tags, + "mfa_serial_number": "", } 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 5d3b8bd543..abf24a072e 100644 --- a/builtin/logical/aws/path_roles.go +++ b/builtin/logical/aws/path_roles.go @@ -61,7 +61,7 @@ func pathRoles(b *backend) *framework.Path { "credential_type": { Type: framework.TypeString, - Description: fmt.Sprintf("Type of credential to retrieve. Must be one of %s, %s, or %s", assumedRoleCred, iamUserCred, federationTokenCred), + Description: fmt.Sprintf("Type of credential to retrieve. Must be one of %s, %s, %s, or %s", assumedRoleCred, iamUserCred, federationTokenCred, sessionTokenCred), }, "role_arns": { @@ -118,7 +118,7 @@ delimited key pairs.`, "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), + Description: fmt.Sprintf("Default TTL for %s, %s, and %s credential types when no TTL is explicitly requested with the credentials", assumedRoleCred, federationTokenCred, sessionTokenCred), DisplayAttrs: &framework.DisplayAttributes{ Name: "Default STS TTL", }, @@ -126,7 +126,7 @@ delimited key pairs.`, "max_sts_ttl": { Type: framework.TypeDurationSecond, - Description: fmt.Sprintf("Max allowed TTL for %s and %s credential types", assumedRoleCred, federationTokenCred), + Description: fmt.Sprintf("Max allowed TTL for %s, %s, and %s credential types", assumedRoleCred, federationTokenCred, sessionTokenCred), DisplayAttrs: &framework.DisplayAttributes{ Name: "Max STS TTL", }, @@ -161,6 +161,15 @@ delimited key pairs.`, }, Default: "/", }, + + "mfa_serial_number": { + Type: framework.TypeString, + Description: fmt.Sprintf(`Identification number or ARN of the MFA device associated with the root config user. Only valid +when credential_type is %s. This is only required when the IAM user has an MFA device configured.`, sessionTokenCred), + DisplayAttrs: &framework.DisplayAttributes{ + Name: "MFA Device Serial Number", + }, + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -328,6 +337,10 @@ func (b *backend) pathRolesWrite(ctx context.Context, req *logical.Request, d *f roleEntry.IAMTags = iamTags.(map[string]string) } + if serialNumber, ok := d.GetOk("mfa_serial_number"); ok { + roleEntry.SerialNumber = serialNumber.(string) + } + if legacyRole != "" { roleEntry = upgradeLegacyPolicyEntry(legacyRole) if roleEntry.InvalidData != "" { @@ -521,6 +534,7 @@ type awsRoleEntry struct { MaxSTSTTL time.Duration `json:"max_sts_ttl"` // Max allowed TTL for STS credentials UserPath string `json:"user_path"` // The path for the IAM user when using "iam_user" credential type PermissionsBoundaryARN string `json:"permissions_boundary_arn"` // ARN of an IAM policy to attach as a permissions boundary + SerialNumber string `json:"mfa_serial_number"` // Serial number or ARN of the MFA device } func (r *awsRoleEntry) toResponseData() map[string]interface{} { @@ -535,6 +549,7 @@ func (r *awsRoleEntry) toResponseData() map[string]interface{} { "max_sts_ttl": int64(r.MaxSTSTTL.Seconds()), "user_path": r.UserPath, "permissions_boundary_arn": r.PermissionsBoundaryARN, + "mfa_serial_number": r.SerialNumber, } if r.InvalidData != "" { @@ -550,19 +565,19 @@ func (r *awsRoleEntry) validate() error { errors = multierror.Append(errors, fmt.Errorf("did not supply credential_type")) } - allowedCredentialTypes := []string{iamUserCred, assumedRoleCred, federationTokenCred} + allowedCredentialTypes := []string{iamUserCred, assumedRoleCred, federationTokenCred, sessionTokenCred} for _, credType := range r.CredentialTypes { if !strutil.StrListContains(allowedCredentialTypes, credType) { errors = multierror.Append(errors, fmt.Errorf("unrecognized credential type: %s", credType)) } } - if r.DefaultSTSTTL != 0 && !strutil.StrListContains(r.CredentialTypes, assumedRoleCred) && !strutil.StrListContains(r.CredentialTypes, federationTokenCred) { - errors = multierror.Append(errors, fmt.Errorf("default_sts_ttl parameter only valid for %s and %s credential types", assumedRoleCred, federationTokenCred)) + if r.DefaultSTSTTL != 0 && !strutil.StrListContains(r.CredentialTypes, assumedRoleCred) && !strutil.StrListContains(r.CredentialTypes, federationTokenCred) && !strutil.StrListContains(r.CredentialTypes, sessionTokenCred) { + errors = multierror.Append(errors, fmt.Errorf("default_sts_ttl parameter only valid for %s, %s, and %s credential types", assumedRoleCred, federationTokenCred, sessionTokenCred)) } - if r.MaxSTSTTL != 0 && !strutil.StrListContains(r.CredentialTypes, assumedRoleCred) && !strutil.StrListContains(r.CredentialTypes, federationTokenCred) { - errors = multierror.Append(errors, fmt.Errorf("max_sts_ttl parameter only valid for %s and %s credential types", assumedRoleCred, federationTokenCred)) + if r.MaxSTSTTL != 0 && !strutil.StrListContains(r.CredentialTypes, assumedRoleCred) && !strutil.StrListContains(r.CredentialTypes, federationTokenCred) && !strutil.StrListContains(r.CredentialTypes, sessionTokenCred) { + errors = multierror.Append(errors, fmt.Errorf("max_sts_ttl parameter only valid for %s, %s, and %s credential types", assumedRoleCred, federationTokenCred, sessionTokenCred)) } if r.MaxSTSTTL > 0 && @@ -576,7 +591,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("the specified value for user_path is invalid. It must match %q regexp", userPathRegex.String())) } } @@ -589,6 +604,10 @@ func (r *awsRoleEntry) validate() error { } } + if (r.PolicyDocument != "" || len(r.PolicyArns) != 0) && strutil.StrListContains(r.CredentialTypes, sessionTokenCred) { + errors = multierror.Append(errors, fmt.Errorf("cannot supply a policy or role when using credential_type %s", sessionTokenCred)) + } + 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)) } @@ -606,6 +625,7 @@ const ( assumedRoleCred = "assumed_role" iamUserCred = "iam_user" federationTokenCred = "federation_token" + sessionTokenCred = "session_token" ) const pathListRolesHelpSyn = `List the existing roles in this backend` diff --git a/builtin/logical/aws/path_user.go b/builtin/logical/aws/path_user.go index 401f03e7b8..46b9c3e928 100644 --- a/builtin/logical/aws/path_user.go +++ b/builtin/logical/aws/path_user.go @@ -48,6 +48,10 @@ func pathUser(b *backend) *framework.Path { Description: "Session name to use when assuming role. Max chars: 64", Query: true, }, + "mfa_code": { + Type: framework.TypeString, + Description: "MFA code to provide for session tokens", + }, }, Operations: map[logical.Operation]framework.OperationHandler{ @@ -107,6 +111,7 @@ func (b *backend) pathCredsRead(ctx context.Context, req *logical.Request, d *fr roleArn := d.Get("role_arn").(string) roleSessionName := d.Get("role_session_name").(string) + mfaCode := d.Get("mfa_code").(string) var credentialType string switch { @@ -155,6 +160,8 @@ func (b *backend) pathCredsRead(ctx context.Context, req *logical.Request, d *fr return b.assumeRole(ctx, req.Storage, req.DisplayName, roleName, roleArn, role.PolicyDocument, role.PolicyArns, role.IAMGroups, ttl, roleSessionName) case federationTokenCred: return b.getFederationToken(ctx, req.Storage, req.DisplayName, roleName, role.PolicyDocument, role.PolicyArns, role.IAMGroups, ttl) + case sessionTokenCred: + return b.getSessionToken(ctx, req.Storage, role.SerialNumber, mfaCode, ttl) default: return logical.ErrorResponse(fmt.Sprintf("unknown credential_type: %q", credentialType)), nil } diff --git a/builtin/logical/aws/secret_access_keys.go b/builtin/logical/aws/secret_access_keys.go index d79f288b2c..ed856bbd2f 100644 --- a/builtin/logical/aws/secret_access_keys.go +++ b/builtin/logical/aws/secret_access_keys.go @@ -38,9 +38,14 @@ func secretAccessKeys(b *backend) *framework.Secret { Type: framework.TypeString, Description: "Secret Key", }, + "session_token": { + Type: framework.TypeString, + Description: "Session Token", + }, "security_token": { Type: framework.TypeString, Description: "Security Token", + Deprecated: true, }, }, @@ -161,11 +166,12 @@ func (b *backend) getFederationToken(ctx context.Context, s logical.Storage, // While STS credentials cannot be revoked/renewed, we will still create a lease since users are // relying on a non-zero `lease_duration` in order to manage their lease lifecycles manually. // - ttl := tokenResp.Credentials.Expiration.Sub(time.Now()) + ttl := time.Until(*tokenResp.Credentials.Expiration) resp := b.Secret(secretAccessKeyType).Response(map[string]interface{}{ "access_key": *tokenResp.Credentials.AccessKeyId, "secret_key": *tokenResp.Credentials.SecretAccessKey, "security_token": *tokenResp.Credentials.SessionToken, + "session_token": *tokenResp.Credentials.SessionToken, "ttl": uint64(ttl.Seconds()), }, map[string]interface{}{ "username": username, @@ -182,6 +188,55 @@ func (b *backend) getFederationToken(ctx context.Context, s logical.Storage, return resp, nil } +// NOTE: Getting session tokens with or without MFA/TOTP has behavior that can cause confusion. +// When an AWS IAM user has a policy attached requiring an MFA code by use of "aws:MultiFactorAuthPresent": "true", +// then credentials may still be returned without an MFA code provided. +// If a Vault role associated with the IAM user is configured without both an mfa_serial_number and +// the mfa_code is not given, the API call is successful and returns credentials. These credentials +// are scoped to any resources in the policy that do NOT have "aws:MultiFactorAuthPresent": "true" set and +// accessing resources with it set will be denied. +// This is expected behavior, as the policy may have a mix of permissions, some requiring MFA and others not. +// If an mfa_serial_number is set on the Vault role, then a valid mfa_code MUST be provided to succeed. +func (b *backend) getSessionToken(ctx context.Context, s logical.Storage, serialNumber, mfaCode string, lifeTimeInSeconds int64) (*logical.Response, error) { + stsClient, err := b.clientSTS(ctx, s) + if err != nil { + return logical.ErrorResponse(err.Error()), nil + } + + getTokenInput := &sts.GetSessionTokenInput{ + DurationSeconds: &lifeTimeInSeconds, + } + if serialNumber != "" { + getTokenInput.SerialNumber = &serialNumber + } + if mfaCode != "" { + getTokenInput.TokenCode = &mfaCode + } + + tokenResp, err := stsClient.GetSessionToken(getTokenInput) + if err != nil { + return logical.ErrorResponse("Error generating STS keys: %s", err), awsutil.CheckAWSError(err) + } + + ttl := time.Until(*tokenResp.Credentials.Expiration) + resp := b.Secret(secretAccessKeyType).Response(map[string]interface{}{ + "access_key": *tokenResp.Credentials.AccessKeyId, + "secret_key": *tokenResp.Credentials.SecretAccessKey, + "session_token": *tokenResp.Credentials.SessionToken, + "ttl": uint64(ttl.Seconds()), + }, map[string]interface{}{ + "is_sts": true, + }) + + // Set the secret TTL to appropriately match the expiration of the token + resp.Secret.TTL = time.Until(*tokenResp.Credentials.Expiration) + + // STS are purposefully short-lived and aren't renewable + resp.Secret.Renewable = false + + return resp, nil +} + 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, @@ -249,11 +304,12 @@ func (b *backend) assumeRole(ctx context.Context, s logical.Storage, // While STS credentials cannot be revoked/renewed, we will still create a lease since users are // relying on a non-zero `lease_duration` in order to manage their lease lifecycles manually. // - ttl := tokenResp.Credentials.Expiration.Sub(time.Now()) + ttl := time.Until(*tokenResp.Credentials.Expiration) resp := b.Secret(secretAccessKeyType).Response(map[string]interface{}{ "access_key": *tokenResp.Credentials.AccessKeyId, "secret_key": *tokenResp.Credentials.SecretAccessKey, "security_token": *tokenResp.Credentials.SessionToken, + "session_token": *tokenResp.Credentials.SessionToken, "arn": *tokenResp.AssumedRoleUser.Arn, "ttl": uint64(ttl.Seconds()), }, map[string]interface{}{ @@ -420,9 +476,9 @@ func (b *backend) secretAccessKeysCreate( // Return the info! resp := b.Secret(secretAccessKeyType).Response(map[string]interface{}{ - "access_key": *keyResp.AccessKey.AccessKeyId, - "secret_key": *keyResp.AccessKey.SecretAccessKey, - "security_token": nil, + "access_key": *keyResp.AccessKey.AccessKeyId, + "secret_key": *keyResp.AccessKey.SecretAccessKey, + "session_token": nil, }, map[string]interface{}{ "username": username, "policy": role, diff --git a/builtin/logical/aws/stepwise_test.go b/builtin/logical/aws/stepwise_test.go index 4ff3920edd..dff852859f 100644 --- a/builtin/logical/aws/stepwise_test.go +++ b/builtin/logical/aws/stepwise_test.go @@ -70,7 +70,7 @@ func testAccStepwiseRead(t *testing.T, path, name string, credentialTests []cred var d struct { AccessKey string `mapstructure:"access_key"` SecretKey string `mapstructure:"secret_key"` - STSToken string `mapstructure:"security_token"` + STSToken string `mapstructure:"session_token"` } if err := mapstructure.Decode(resp.Data, &d); err != nil { return err diff --git a/changelog/23690.txt b/changelog/23690.txt new file mode 100644 index 0000000000..c0cb0605e1 --- /dev/null +++ b/changelog/23690.txt @@ -0,0 +1,3 @@ +```release-note:feature +secrets/aws: support issuing an STS Session Token directly from the root credential. +``` \ No newline at end of file diff --git a/ui/app/models/aws-credential.js b/ui/app/models/aws-credential.js index cf41ddba3c..3dccb192b2 100644 --- a/ui/app/models/aws-credential.js +++ b/ui/app/models/aws-credential.js @@ -19,6 +19,10 @@ const CREDENTIAL_TYPES = [ value: 'federation_token', displayName: 'Federation Token', }, + { + value: 'session_token', + displayName: 'Session Token', + }, ]; const DISPLAY_FIELDS = ['accessKey', 'secretKey', 'securityToken', 'leaseId', 'renewable', 'leaseDuration']; @@ -47,7 +51,7 @@ export default Model.extend({ setDefault: true, label: 'TTL', helpText: - 'Specifies the TTL for the use of the STS token. Valid only when credential_type is assumed_role or federation_token.', + 'Specifies the TTL for the use of the STS token. Valid only when credential_type is assumed_role, federation_token, or session_token.', }), leaseId: attr('string'), renewable: attr('boolean'), @@ -62,6 +66,7 @@ export default Model.extend({ iam_user: ['credentialType'], assumed_role: ['credentialType', 'ttl', 'roleArn'], federation_token: ['credentialType', 'ttl'], + session_token: ['ttl'], }; if (this.accessKey || this.securityToken) { return expandAttributeMeta(this, DISPLAY_FIELDS.slice(0)); diff --git a/ui/app/models/role-aws.js b/ui/app/models/role-aws.js index f40bee0dab..fc4ae9050a 100644 --- a/ui/app/models/role-aws.js +++ b/ui/app/models/role-aws.js @@ -22,6 +22,10 @@ const CREDENTIAL_TYPES = [ value: 'federation_token', displayName: 'Federation Token', }, + { + value: 'session_token', + displayName: 'Session Token', + }, ]; export default Model.extend({ backend: attr('string', { @@ -62,6 +66,7 @@ export default Model.extend({ iam_user: ['name', 'credentialType', 'policyArns', 'policyDocument'], assumed_role: ['name', 'credentialType', 'roleArns', 'policyDocument'], federation_token: ['name', 'credentialType', 'policyDocument'], + session_token: [], }; return expandAttributeMeta(this, keysForType[credentialType]); diff --git a/ui/tests/unit/adapters/aws-credential-test.js b/ui/tests/unit/adapters/aws-credential-test.js index 84e241f430..5f804fdedf 100644 --- a/ui/tests/unit/adapters/aws-credential-test.js +++ b/ui/tests/unit/adapters/aws-credential-test.js @@ -53,6 +53,17 @@ module('Unit | Adapter | aws credential', function (hooks) { [storeStub, type, makeSnapshot({ credentialType: 'federation_token', roleArn: 'arn' })], 'POST', ], + [ + 'session_token type with ttl', + [storeStub, type, makeSnapshot({ credentialType: 'session_token', ttl: '3h' })], + 'POST', + { ttl: '3h' }, + ], + [ + 'session_token type no ttl', + [storeStub, type, makeSnapshot({ credentialType: 'session_token' })], + 'POST', + ], [ 'assumed_role type no arn, no ttl', [storeStub, type, makeSnapshot({ credentialType: 'assumed_role' })], diff --git a/website/content/api-docs/secret/aws.mdx b/website/content/api-docs/secret/aws.mdx index 9c3b4946a8..5fc33baee4 100644 --- a/website/content/api-docs/secret/aws.mdx +++ b/website/content/api-docs/secret/aws.mdx @@ -250,7 +250,7 @@ updated with the new attributes. - `credential_type` `(string: )` – Specifies the type of credential to be used when retrieving credentials from the role. Must be one of `iam_user`, - `assumed_role`, or `federation_token`. + `assumed_role`, `federation_token`, or `session_token`. - `role_arns` `(list: [])` – Specifies the ARNs of the AWS roles this Vault role is allowed to assume. Required when `credential_type` is `assumed_role` and @@ -263,13 +263,15 @@ updated with the new attributes. credentials can do, similar to `policy_document`. When `credential_type` is `iam_user` or `federation_token`, at least one of `policy_arns` or `policy_document` must be specified. This is a - comma-separated string or JSON array. + comma-separated string or JSON array. When using `session_token`, this field + is disallowed. - `policy_document` `(string)` – The IAM policy document for the role. The behavior depends on the credential type. With `iam_user`, the policy document will be attached to the IAM user generated and augment the permissions the IAM user has. With `assumed_role` and `federation_token`, the policy document will - act as a filter on what the credentials can do, similar to `policy_arns`. + act as a filter on what the credentials can do, similar to `policy_arns`. With + `session_token`, this field is disallowed. - `iam_groups` `(list: [])` - A list of IAM group names. IAM users generated against this vault role will be added to these IAM Groups. For a credential @@ -302,6 +304,10 @@ updated with the new attributes. is `iam_user`. If not specified, then no permissions boundary policy will be attached. +- `mfa_serial_number` `(string)` - The ARN or hardware device number of the device configured + to the IAM user for multi-factor authentication. Only required if the IAM user has an MFA device + set up in AWS. + Legacy parameters: These parameters are supported for backwards compatibility only. They cannot be @@ -334,6 +340,14 @@ Using an inline IAM policy: } ``` +Using a Session Token: + +```json +{ + "credential_type": "session_token" +} +``` + Using an ARN: ```json @@ -545,27 +559,36 @@ credentials retrieved through `/aws/creds` must be of the `iam_user` type. - `name` `(string: )` – Specifies the name of the role to generate credentials against. This is part of the request URL. + - `role_arn` `(string)` – The ARN of the role to assume if `credential_type` on the Vault role is `assumed_role`. Must match one of the allowed role ARNs in the Vault role. Optional if the Vault role only allows a single AWS role ARN; required otherwise. + - `role_session_name` `(string)` - The role session name to attach to the assumed role ARN. `role_session_name` is limited to 64 characters; if exceeded, the `role_session_name` in the assumed role ARN will be truncated to 64 characters. If `role_session_name` is not provided, then it will be generated dynamically by default. + - `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`. When not specified, + `credential_type` is `assumed_role` `federation_token`, or `session_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) - (for `assumed_role` credential types) and + (for `assumed_role` credential types), [GetFederationToken](https://docs.aws.amazon.com/STS/latest/APIReference/API_GetFederationToken.html) - (for `federation_token` credential types) for more details. + (for `federation_token` credential types), or + [GetSessionToken](https://docs.aws.amazon.com/STS/latest/APIReference/API_GetSessionToken.html) + (for `session_token` credential types) for more details. -### Sample request +- `mfa_code` `(string)` - The TOTP generated by the MFA device configured on the IAM user and set + on the Vault role. This is optional based on whether the Vault role has the `mfa_serial_number` + field set or not. Only required if the Vault role has the `mfa_serial_number` set on it. + +### Sample AssumeRole request ```shell-session $ curl \ @@ -580,12 +603,32 @@ $ curl \ "data": { "access_key": "AKIA...", "secret_key": "xlCs...", - "security_token": null, + "session_token": null, "arn": "arn:aws:sts::123456789012:assumed-role/DeveloperRole/some-user-supplied-role-session-name" } } ``` +### Sample Session Token request + +```shell-session +$ curl \ + --header "X-Vault-Token: ..." \ + http://127.0.0.1:8200/v1/aws/creds/example-role?mfa_code=123456 +``` + +### Sample response + +```json +{ + "data": { + "access_key": "AKIA...", + "secret_key": "xlCs...", + "session_token": "FwoG...", + } +} +``` + ## Create/Update static role This endpoint creates or updates static role definitions. A static role is a 1-to-1 mapping with an AWS IAM User, which will be adopted and managed by Vault, including rotating it according @@ -593,9 +636,10 @@ 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. + 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, Vault will rotate the oldest existing credential. + At each rotation, Vault will rotate the oldest existing credential. diff --git a/website/content/docs/commands/pki/health-check.mdx b/website/content/docs/commands/pki/health-check.mdx index 08d8f5b5df..568a9d4078 100644 --- a/website/content/docs/commands/pki/health-check.mdx +++ b/website/content/docs/commands/pki/health-check.mdx @@ -282,11 +282,13 @@ other (potentially dangerous) quirks. Checks each role to see whether `no_store` is set to `false`. - + + Vault will provide warnings and performance will suffer if you have a large number of certificates without temporal CRL auto-rebuilding and set `no_store` to `true`. - + + **Remediation steps**: diff --git a/website/content/docs/deprecation/index.mdx b/website/content/docs/deprecation/index.mdx index 15e5dc3987..9ad32ae3c6 100644 --- a/website/content/docs/deprecation/index.mdx +++ b/website/content/docs/deprecation/index.mdx @@ -36,6 +36,7 @@ This announcement page is maintained and updated periodically to communicate imp | Consul secrets engine parameter changes | v1.11 | N/A | N/A | The `policies` parameter on the Consul secrets engine has been changed in favor of `consul_policies`. The `token_type` and `policy` parameters have been deprecated as the latest versions of Consul no longer support the older ACL system they were used for. | [Consul secrets engine API documentation](/vault/api-docs/secret/consul) | | Vault Agent API proxy support | v1.14 | v1.16 | v1.17 | Migrate to [Vault Proxy](/vault/docs/proxy/index) by v1.17| | Centrify Auth Method | v1.15 | v1.17 | v1.17 | Use as an external plugin, but support will not be available. | | +| AWS secrets engine field change | v1.16 | N/A | N/A | The `security_token` field returned for AssumeRole and FederationToken credentials is deprecated in favor of the current term `session_token`. | [AWS secrets engine API documentation](/vault/api-docs/secret/aws) | *If you use **Standalone DB Engines** or **AppID (Community)**, you should actively plan to migrate away from their usage. If you use these features and upgrade to Release 1.12, Vault will log error messages and shut down, and any attempts to add new mounts will result in an error. This behavior may temporarily be overridden when starting the Vault server by using the `VAULT_ALLOW_PENDING_REMOVAL_MOUNTS` environment variable until they are officially removed in Vault version 1.13. diff --git a/website/content/docs/secrets/aws.mdx b/website/content/docs/secrets/aws.mdx index 4695c6ea7b..10d34d7b37 100644 --- a/website/content/docs/secrets/aws.mdx +++ b/website/content/docs/secrets/aws.mdx @@ -31,6 +31,9 @@ Vault supports three different types of credentials to retrieve from AWS: [sts:GetFederationToken](https://docs.aws.amazon.com/STS/latest/APIReference/API_GetFederationToken.html) passing in the supplied AWS policy document and return the access key, secret key, and session token to the caller. +4. `session_token`: Vault will call + [sts:GetSessionToken](https://docs.aws.amazon.com/STS/latest/APIReference/API_GetSessionToken.html) + and return the access key, secret key, and session token to the caller. ### Static roles The AWS secrets engine supports the concept of "static roles", which are @@ -150,7 +153,7 @@ the proper permission, it can generate credentials. lease_renewable true access_key AKIAIOWQXTLW36DV7IEA secret_key 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 - security_token + session_token ``` Each invocation of the command will generate a new credential. @@ -173,11 +176,14 @@ the proper permission, it can generate credentials. access_key AKIA3ALIVABCDG5XC8H4 ``` - ~> **Note:** Due to AWS eventual consistency, after calling the - `aws/config/rotate-root` endpoint, subsequent calls from Vault to - AWS may fail for a few seconds until AWS becomes consistent again. - See the [AWS secrets engine API](/vault/api-docs/secret/aws#rotate-root-iam-credentials) - for further information on `config/rotate-root` functionality. + + + Calls from Vault to AWS may fail immediately after calling `aws/config/rotate-root` until + AWS becomes consistent again. Refer to + the AWS secrets engine API reference + for additional information on rotating IAM credentials. + + ## Example IAM policy for Vault @@ -267,7 +273,8 @@ them). ## STS credentials The above demonstrated usage with `iam_user` credential types. As mentioned, -Vault also supports `assumed_role` and `federation_token` credential types. +Vault also supports `assumed_role`, `federation_token`, and `session_token` +credential types. ### STS federation tokens @@ -344,7 +351,54 @@ lease_duration 60m0s lease_renewable false access_key ASIAJYYYY2AA5K4WIXXX secret_key HSs0DYYYYYY9W81DXtI0K7X84H+OVZXK5BXXXX -security_token AQoDYXdzEEwasAKwQyZUtZaCjVNDiXXXXXXXXgUgBBVUUbSyujLjsw6jYzboOQ89vUVIehUw/9MreAifXFmfdbjTr3g6zc0me9M+dB95DyhetFItX5QThw0lEsVQWSiIeIotGmg7mjT1//e7CJc4LpxbW707loFX1TYD1ilNnblEsIBKGlRNXZ+QJdguY4VkzXxv2urxIH0Sl14xtqsRPboV7eYruSEZlAuP3FLmqFbmA0AFPCT37cLf/vUHinSbvw49C4c9WQLH7CeFPhDub7/rub/QU/lCjjJ43IqIRo9jYgcEvvdRkQSt70zO8moGCc7pFvmL7XGhISegQpEzudErTE/PdhjlGpAKGR3d5qKrHpPYK/k480wk1Ai/t1dTa/8/3jUYTUeIkaJpNBnupQt7qoaXXXXXXXXXX +session_token AQoDYXdzEEwasAKwQyZUtZaCjVNDiXXXXXXXXgUgBBVUUbSyujLjsw6jYzboOQ89vUVIehUw/9MreAifXFmfdbjTr3g6zc0me9M+dB95DyhetFItX5QThw0lEsVQWSiIeIotGmg7mjT1//e7CJc4LpxbW707loFX1TYD1ilNnblEsIBKGlRNXZ+QJdguY4VkzXxv2urxIH0Sl14xtqsRPboV7eYruSEZlAuP3FLmqFbmA0AFPCT37cLf/vUHinSbvw49C4c9WQLH7CeFPhDub7/rub/QU/lCjjJ43IqIRo9jYgcEvvdRkQSt70zO8moGCc7pFvmL7XGhISegQpEzudErTE/PdhjlGpAKGR3d5qKrHpPYK/k480wk1Ai/t1dTa/8/3jUYTUeIkaJpNBnupQt7qoaXXXXXXXXXX +``` + +### STS Session Tokens + +The `session_token` credential type is used to generate short-lived credentials under the root config. +To create these with Vault and AWS, you must configure Vault to use IAM user credentials. AWS does not +allow temporary credentials, like those from an IAM instance profile, to be used when generating session tokens. + + + + STS session tokens inherit any and all permissions granted to the user configured in `aws/config/root`. + In this expample, the `temp_user` role will obtain a policy with the same `ec2:*` permissions as the + root config. For this reason, assigning a role or policy is disallowed for this credential type. + + + +```shell-session +$ vault write aws/roles/temp_user \ + credential_type=session_token +``` + +To generate a new set of STS federation token credentials, write to the `temp_user` +role using the `aws/creds` endpoint: + +```shell-session +$ vault read aws/sts/temp_user ttl=60m +Key Value +lease_id aws/creds/temp_user/w4eKbMaJOi1xLqG3MWk7y8n6 +lease_duration 60m0s +lease_renewable false +access_key ASIAJYYYY2AA5K4WIXXX +secret_key HSs0DYYYYYY9W81DXtI0K7X84H+OVZXK5BXXXX +session_token AQoDYXdzEEwasAKwQyZUtZaCjVNDiXXXXXXXXgUgBBVUUbSyujLjsw6jYzboOQ89vUVIehUw/9MreAifXFmfdbjTr3g6zc0me9M+dB95DyhetFItX5QThw0lEsVQWSiIeIotGmg7mjT1//e7CJc4LpxbW707loFX1TYD1ilNnblEsIBKGlRNXZ+QJdguY4VkzXxv2urxIH0Sl14xtqsRPboV7eYruSEZlAuP3FLmqFbmA0AFPCT37cLf/vUHinSbvw49C4c9WQLH7CeFPhDub7/rub/QU/lCjjJ43IqIRo9jYgcEvvdRkQSt70zO8moGCc7pFvmL7XGhISegQpEzudErTE/PdhjlGpAKGR3d5qKrHpPYK/k480wk1Ai/t1dTa/8/3jUYTUeIkaJpNBnupQt7qoaXXXXXXXXXX +``` + +Session tokens may also require an MFA-based TOTP to be provided if the IAM user is configured to require it. +If so, the Vault role requires the MFA device serial number to be set, and the TOTP may be provided when +reading credentials from the Vault role. + +```shell-session +$ vault write aws/roles/mfa_user \ + credential_type=session_token \ + mfa_serial_number="arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:mfa/device-name" +``` + +```shell-session +$ vault read aws/creds/mfa_user mfa_code=123456 ``` ### STS AssumeRole @@ -439,7 +493,7 @@ lease_duration 60m0s lease_renewable false access_key ASIAJYYYY2AA5K4WIXXX secret_key HSs0DYYYYYY9W81DXtI0K7X84H+OVZXK5BXXXX -security_token AQoDYXdzEEwasAKwQyZUtZaCjVNDiXXXXXXXXgUgBBVUUbSyujLjsw6jYzboOQ89vUVIehUw/9MreAifXFmfdbjTr3g6zc0me9M+dB95DyhetFItX5QThw0lEsVQWSiIeIotGmg7mjT1//e7CJc4LpxbW707loFX1TYD1ilNnblEsIBKGlRNXZ+QJdguY4VkzXxv2urxIH0Sl14xtqsRPboV7eYruSEZlAuP3FLmqFbmA0AFPCT37cLf/vUHinSbvw49C4c9WQLH7CeFPhDub7/rub/QU/lCjjJ43IqIRo9jYgcEvvdRkQSt70zO8moGCc7pFvmL7XGhISegQpEzudErTE/PdhjlGpAKGR3d5qKrHpPYK/k480wk1Ai/t1dTa/8/3jUYTUeIkaJpNBnupQt7qoaXXXXXXXXXX +session_token AQoDYXdzEEwasAKwQyZUtZaCjVNDiXXXXXXXXgUgBBVUUbSyujLjsw6jYzboOQ89vUVIehUw/9MreAifXFmfdbjTr3g6zc0me9M+dB95DyhetFItX5QThw0lEsVQWSiIeIotGmg7mjT1//e7CJc4LpxbW707loFX1TYD1ilNnblEsIBKGlRNXZ+QJdguY4VkzXxv2urxIH0Sl14xtqsRPboV7eYruSEZlAuP3FLmqFbmA0AFPCT37cLf/vUHinSbvw49C4c9WQLH7CeFPhDub7/rub/QU/lCjjJ43IqIRo9jYgcEvvdRkQSt70zO8moGCc7pFvmL7XGhISegQpEzudErTE/PdhjlGpAKGR3d5qKrHpPYK/k480wk1Ai/t1dTa/8/3jUYTUeIkaJpNBnupQt7qoaXXXXXXXXXX ``` [sts:assumerole]: https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html