2015-03-20 16:59:48 +00:00
package aws
import (
2018-01-08 18:31:38 +00:00
"context"
2015-03-20 16:59:48 +00:00
"fmt"
2018-08-16 10:38:13 +00:00
"strings"
logical/aws: Harden WAL entry creation (#5202)
* logical/aws: Harden WAL entry creation
If AWS IAM user creation failed in any way, the WAL corresponding to the
IAM user would get left around and Vault would try to roll it back.
However, because the user never existed, the rollback failed. Thus, the
WAL would essentially get "stuck" and Vault would continually attempt to
roll it back, failing every time. A similar situation could arise if the
IAM user that Vault created got deleted out of band, or if Vault deleted
it but was unable to write the lease revocation back to storage (e.g., a
storage failure).
This attempts to harden it in two ways. One is by deleting the WAL log
entry if the IAM user creation fails. However, the WAL deletion could
still fail, and this wouldn't help where the user is deleted out of
band, so second, consider the user rolled back if the user just doesn't
exist, under certain circumstances.
Fixes #5190
* Fix segfault in expiration unit tests
TestExpiration_Tidy was passing in a leaseEntry that had a nil Secret,
which then caused a segfault as the changes to revokeEntry didn't check
whether Secret was nil; this is probably unlikely to occur in real life,
but good to be extra cautious.
* Fix potential segfault
Missed the else...
* Respond to PR feedback
2018-09-27 14:54:59 +00:00
"time"
2015-03-20 16:59:48 +00:00
2015-08-06 16:26:41 +00:00
"github.com/aws/aws-sdk-go/aws"
logical/aws: Harden WAL entry creation (#5202)
* logical/aws: Harden WAL entry creation
If AWS IAM user creation failed in any way, the WAL corresponding to the
IAM user would get left around and Vault would try to roll it back.
However, because the user never existed, the rollback failed. Thus, the
WAL would essentially get "stuck" and Vault would continually attempt to
roll it back, failing every time. A similar situation could arise if the
IAM user that Vault created got deleted out of band, or if Vault deleted
it but was unable to write the lease revocation back to storage (e.g., a
storage failure).
This attempts to harden it in two ways. One is by deleting the WAL log
entry if the IAM user creation fails. However, the WAL deletion could
still fail, and this wouldn't help where the user is deleted out of
band, so second, consider the user rolled back if the user just doesn't
exist, under certain circumstances.
Fixes #5190
* Fix segfault in expiration unit tests
TestExpiration_Tidy was passing in a leaseEntry that had a nil Secret,
which then caused a segfault as the changes to revokeEntry didn't check
whether Secret was nil; this is probably unlikely to occur in real life,
but good to be extra cautious.
* Fix potential segfault
Missed the else...
* Respond to PR feedback
2018-09-27 14:54:59 +00:00
"github.com/aws/aws-sdk-go/aws/awserr"
2015-08-06 16:26:41 +00:00
"github.com/aws/aws-sdk-go/service/iam"
2021-07-16 00:17:31 +00:00
"github.com/hashicorp/go-secure-stdlib/strutil"
2019-04-13 07:44:06 +00:00
"github.com/hashicorp/vault/sdk/framework"
2019-04-12 21:54:35 +00:00
"github.com/hashicorp/vault/sdk/logical"
2015-03-21 10:18:46 +00:00
"github.com/mitchellh/mapstructure"
2015-03-20 16:59:48 +00:00
)
func pathUser ( b * backend ) * framework . Path {
return & framework . Path {
2019-10-25 16:01:01 +00:00
Pattern : "(creds|sts)/" + framework . GenericNameWithAtRegex ( "name" ) ,
2015-03-20 16:59:48 +00:00
Fields : map [ string ] * framework . FieldSchema {
2021-04-08 16:43:39 +00:00
"name" : {
2015-03-20 16:59:48 +00:00
Type : framework . TypeString ,
2015-04-27 21:20:28 +00:00
Description : "Name of the role" ,
2015-03-20 16:59:48 +00:00
} ,
2021-04-08 16:43:39 +00:00
"role_arn" : {
2018-08-16 10:38:13 +00:00
Type : framework . TypeString ,
Description : "ARN of role to assume when credential_type is " + assumedRoleCred ,
} ,
2021-04-08 16:43:39 +00:00
"ttl" : {
2018-08-16 10:38:13 +00:00
Type : framework . TypeDurationSecond ,
Description : "Lifetime of the returned credentials in seconds" ,
Default : 3600 ,
} ,
2022-01-27 18:06:34 +00:00
"role_session_name" : {
2021-05-17 18:03:09 +00:00
Type : framework . TypeString ,
Description : "Session name to use when assuming role. Max chars: 64" ,
} ,
2015-03-20 16:59:48 +00:00
} ,
Callbacks : map [ logical . Operation ] framework . OperationFunc {
2018-08-16 10:38:13 +00:00
logical . ReadOperation : b . pathCredsRead ,
logical . UpdateOperation : b . pathCredsRead ,
2015-03-20 16:59:48 +00:00
} ,
2015-04-04 04:10:54 +00:00
HelpSynopsis : pathUserHelpSyn ,
HelpDescription : pathUserHelpDesc ,
2015-03-20 16:59:48 +00:00
}
}
2018-08-16 10:38:13 +00:00
func ( b * backend ) pathCredsRead ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
roleName := d . Get ( "name" ) . ( string )
2015-03-20 16:59:48 +00:00
// Read the policy
2018-08-16 10:38:13 +00:00
role , err := b . roleRead ( ctx , req . Storage , roleName , true )
2015-03-20 16:59:48 +00:00
if err != nil {
2021-04-22 15:20:59 +00:00
return nil , fmt . Errorf ( "error retrieving role: %w" , err )
2015-03-20 16:59:48 +00:00
}
2018-08-16 10:38:13 +00:00
if role == nil {
2015-03-20 16:59:48 +00:00
return logical . ErrorResponse ( fmt . Sprintf (
2022-08-03 18:32:45 +00:00
"Role %q not found" , roleName ) ) , nil
2018-08-16 10:38:13 +00:00
}
2018-10-02 14:14:16 +00:00
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 ) )
}
2018-10-20 14:36:47 +00:00
var maxTTL int64
if role . MaxSTSTTL > 0 {
maxTTL = int64 ( role . MaxSTSTTL . Seconds ( ) )
} else {
maxTTL = int64 ( b . System ( ) . MaxLeaseTTL ( ) . Seconds ( ) )
}
if ttl > maxTTL {
ttl = maxTTL
}
2018-08-16 10:38:13 +00:00
roleArn := d . Get ( "role_arn" ) . ( string )
2021-05-17 18:03:09 +00:00
roleSessionName := d . Get ( "role_session_name" ) . ( string )
2018-08-16 10:38:13 +00:00
var credentialType string
switch {
case len ( role . CredentialTypes ) == 1 :
credentialType = role . CredentialTypes [ 0 ]
// There is only one way for the CredentialTypes to contain more than one entry, and that's an upgrade path
// where it contains iamUserCred and federationTokenCred
// This ambiguity can be resolved based on req.Path, so resolve it assuming CredentialTypes only has those values
case len ( role . CredentialTypes ) > 1 :
if strings . HasPrefix ( req . Path , "creds" ) {
credentialType = iamUserCred
} else {
credentialType = federationTokenCred
}
// sanity check on the assumption above
if ! strutil . StrListContains ( role . CredentialTypes , credentialType ) {
return logical . ErrorResponse ( fmt . Sprintf ( "requested credential type %q not in allowed credential types %#v" , credentialType , role . CredentialTypes ) ) , nil
}
2015-03-21 10:18:46 +00:00
}
2018-08-16 10:38:13 +00:00
// creds requested through the sts path shouldn't be allowed to get iamUserCred type creds
// when the role is created from legacy data because they might have more privileges in AWS.
// See https://github.com/hashicorp/vault/issues/4229#issuecomment-380316788 for details.
if role . ProhibitFlexibleCredPath {
if credentialType == iamUserCred && strings . HasPrefix ( req . Path , "sts" ) {
return logical . ErrorResponse ( fmt . Sprintf ( "attempted to retrieve %s credentials through the sts path; this is not allowed for legacy roles" , iamUserCred ) ) , nil
}
if credentialType != iamUserCred && strings . HasPrefix ( req . Path , "creds" ) {
return logical . ErrorResponse ( fmt . Sprintf ( "attempted to retrieve %s credentials through the creds path; this is not allowed for legacy roles" , credentialType ) ) , nil
}
}
switch credentialType {
case iamUserCred :
return b . secretAccessKeysCreate ( ctx , req . Storage , req . DisplayName , roleName , role )
case assumedRoleCred :
switch {
case roleArn == "" :
if len ( role . RoleArns ) != 1 {
return logical . ErrorResponse ( "did not supply a role_arn parameter and unable to determine one" ) , nil
}
roleArn = role . RoleArns [ 0 ]
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
}
2021-05-17 18:03:09 +00:00
return b . assumeRole ( ctx , req . Storage , req . DisplayName , roleName , roleArn , role . PolicyDocument , role . PolicyArns , role . IAMGroups , ttl , roleSessionName )
2018-08-16 10:38:13 +00:00
case federationTokenCred :
2020-06-09 23:56:12 +00:00
return b . getFederationToken ( ctx , req . Storage , req . DisplayName , roleName , role . PolicyDocument , role . PolicyArns , role . IAMGroups , ttl )
2018-08-16 10:38:13 +00:00
default :
return logical . ErrorResponse ( fmt . Sprintf ( "unknown credential_type: %q" , credentialType ) ) , nil
}
2015-03-20 16:59:48 +00:00
}
2015-03-21 10:18:46 +00:00
2018-09-26 14:10:00 +00:00
func ( b * backend ) pathUserRollback ( ctx context . Context , req * logical . Request , _kind string , data interface { } ) error {
2015-03-21 10:18:46 +00:00
var entry walUser
if err := mapstructure . Decode ( data , & entry ) ; err != nil {
return err
}
username := entry . UserName
// Get the client
2018-09-26 14:10:00 +00:00
client , err := b . clientIAM ( ctx , req . Storage )
2015-03-21 10:18:46 +00:00
if err != nil {
return err
}
// Get information about this user
2015-08-06 16:37:08 +00:00
groupsResp , err := client . ListGroupsForUser ( & iam . ListGroupsForUserInput {
2015-03-21 10:18:46 +00:00
UserName : aws . String ( username ) ,
2015-08-06 16:37:08 +00:00
MaxItems : aws . Int64 ( 1000 ) ,
2015-03-21 10:18:46 +00:00
} )
if err != nil {
logical/aws: Harden WAL entry creation (#5202)
* logical/aws: Harden WAL entry creation
If AWS IAM user creation failed in any way, the WAL corresponding to the
IAM user would get left around and Vault would try to roll it back.
However, because the user never existed, the rollback failed. Thus, the
WAL would essentially get "stuck" and Vault would continually attempt to
roll it back, failing every time. A similar situation could arise if the
IAM user that Vault created got deleted out of band, or if Vault deleted
it but was unable to write the lease revocation back to storage (e.g., a
storage failure).
This attempts to harden it in two ways. One is by deleting the WAL log
entry if the IAM user creation fails. However, the WAL deletion could
still fail, and this wouldn't help where the user is deleted out of
band, so second, consider the user rolled back if the user just doesn't
exist, under certain circumstances.
Fixes #5190
* Fix segfault in expiration unit tests
TestExpiration_Tidy was passing in a leaseEntry that had a nil Secret,
which then caused a segfault as the changes to revokeEntry didn't check
whether Secret was nil; this is probably unlikely to occur in real life,
but good to be extra cautious.
* Fix potential segfault
Missed the else...
* Respond to PR feedback
2018-09-27 14:54:59 +00:00
// This isn't guaranteed to be perfect; for example, an IAM user
// might have gotten put into the WAL but then the IAM user creation
// failed (e.g., Vault didn't have permissions) and then the WAL
// deletion failed as well. Then, if Vault doesn't have access to
// call iam:ListGroupsForUser, AWS will return an access denied error
// and the WAL will never get cleaned up. But this is better than
// just having Vault "forget" about a user it actually created.
//
// BEWARE a potential race condition -- where this is called
// immediately after a user is created. AWS eventual consistency
// might say the user doesn't exist when the user does in fact
// exist, and this could cause Vault to forget about the user.
// This won't happen if the user creation fails (because the WAL
// minimum age is 5 minutes, and AWS eventual consistency is, in
// practice, never that long), but it could happen if a lease holder
// asks immediately after getting a user to revoke the lease, causing
// Vault to leak the secret, which would be a Very Bad Thing to allow.
// So we make sure that, if there's an associated lease, it must be at
// least 5 minutes old as well.
if aerr , ok := err . ( awserr . Error ) ; ok {
acceptMissingIamUsers := false
if req . Secret == nil || time . Since ( req . Secret . IssueTime ) > time . Duration ( minAwsUserRollbackAge ) {
// WAL rollback
acceptMissingIamUsers = true
}
if aerr . Code ( ) == iam . ErrCodeNoSuchEntityException && acceptMissingIamUsers {
return nil
}
}
2015-03-21 10:18:46 +00:00
return err
}
groups := groupsResp . Groups
2015-12-30 18:05:54 +00:00
// Inline (user) policies
2015-08-06 16:37:08 +00:00
policiesResp , err := client . ListUserPolicies ( & iam . ListUserPoliciesInput {
2015-03-21 10:18:46 +00:00
UserName : aws . String ( username ) ,
2015-08-06 16:37:08 +00:00
MaxItems : aws . Int64 ( 1000 ) ,
2015-03-21 10:18:46 +00:00
} )
if err != nil {
return err
}
policies := policiesResp . PolicyNames
2015-12-30 18:05:54 +00:00
// Attached managed policies
manPoliciesResp , err := client . ListAttachedUserPolicies ( & iam . ListAttachedUserPoliciesInput {
UserName : aws . String ( username ) ,
MaxItems : aws . Int64 ( 1000 ) ,
} )
if err != nil {
return err
}
manPolicies := manPoliciesResp . AttachedPolicies
2015-08-06 16:37:08 +00:00
keysResp , err := client . ListAccessKeys ( & iam . ListAccessKeysInput {
2015-03-21 10:18:46 +00:00
UserName : aws . String ( username ) ,
2015-08-06 16:37:08 +00:00
MaxItems : aws . Int64 ( 1000 ) ,
2015-03-21 10:18:46 +00:00
} )
if err != nil {
return err
}
keys := keysResp . AccessKeyMetadata
// Revoke all keys
for _ , k := range keys {
2015-08-06 16:37:08 +00:00
_ , err = client . DeleteAccessKey ( & iam . DeleteAccessKeyInput {
2015-08-19 01:12:51 +00:00
AccessKeyId : k . AccessKeyId ,
2015-03-21 10:18:46 +00:00
UserName : aws . String ( username ) ,
} )
if err != nil {
return err
}
}
2015-12-30 18:05:54 +00:00
// Detach managed policies
for _ , p := range manPolicies {
_ , err = client . DetachUserPolicy ( & iam . DetachUserPolicyInput {
UserName : aws . String ( username ) ,
PolicyArn : p . PolicyArn ,
} )
if err != nil {
return err
}
}
// Delete any inline (user) policies
2015-03-21 10:18:46 +00:00
for _ , p := range policies {
2015-08-06 16:37:08 +00:00
_ , err = client . DeleteUserPolicy ( & iam . DeleteUserPolicyInput {
2015-03-21 10:18:46 +00:00
UserName : aws . String ( username ) ,
2015-08-06 16:37:08 +00:00
PolicyName : p ,
2015-03-21 10:18:46 +00:00
} )
if err != nil {
return err
}
}
// Remove the user from all their groups
for _ , g := range groups {
2015-08-06 16:37:08 +00:00
_ , err = client . RemoveUserFromGroup ( & iam . RemoveUserFromGroupInput {
2015-03-21 10:18:46 +00:00
GroupName : g . GroupName ,
UserName : aws . String ( username ) ,
} )
if err != nil {
return err
}
}
// Delete the user
2015-08-06 16:37:08 +00:00
_ , err = client . DeleteUser ( & iam . DeleteUserInput {
2015-03-21 10:18:46 +00:00
UserName : aws . String ( username ) ,
} )
if err != nil {
return err
}
return nil
}
type walUser struct {
UserName string
}
2015-04-04 04:10:54 +00:00
const pathUserHelpSyn = `
2018-08-16 10:38:13 +00:00
Generate AWS credentials from a specific Vault role .
2015-04-04 04:10:54 +00:00
`
const pathUserHelpDesc = `
2018-08-16 10:38:13 +00:00
This path will generate new , never before used AWS credentials for
2015-04-04 04:10:54 +00:00
accessing AWS . The IAM policy used to back this key pair will be
the "name" parameter . For example , if this backend is mounted at "aws" ,
2015-04-27 21:20:28 +00:00
then "aws/creds/deploy" would generate access keys for the "deploy" role .
2015-04-04 04:10:54 +00:00
The access keys will have a lease associated with them . The access keys
2018-08-16 10:38:13 +00:00
can be revoked by using the lease ID when using the iam_user credential type .
When using AWS STS credential types ( assumed_role or federation_token ) ,
revoking the lease does not revoke the access keys .
2015-04-04 04:10:54 +00:00
`