1package aws 2 3import ( 4 "context" 5 "fmt" 6 "math/rand" 7 "regexp" 8 "time" 9 10 "github.com/aws/aws-sdk-go/aws" 11 "github.com/aws/aws-sdk-go/service/iam" 12 "github.com/aws/aws-sdk-go/service/sts" 13 "github.com/hashicorp/errwrap" 14 "github.com/hashicorp/vault/helper/awsutil" 15 "github.com/hashicorp/vault/sdk/framework" 16 "github.com/hashicorp/vault/sdk/logical" 17) 18 19const secretAccessKeyType = "access_keys" 20 21func secretAccessKeys(b *backend) *framework.Secret { 22 return &framework.Secret{ 23 Type: secretAccessKeyType, 24 Fields: map[string]*framework.FieldSchema{ 25 "access_key": &framework.FieldSchema{ 26 Type: framework.TypeString, 27 Description: "Access Key", 28 }, 29 30 "secret_key": &framework.FieldSchema{ 31 Type: framework.TypeString, 32 Description: "Secret Key", 33 }, 34 "security_token": &framework.FieldSchema{ 35 Type: framework.TypeString, 36 Description: "Security Token", 37 }, 38 }, 39 40 Renew: b.secretAccessKeysRenew, 41 Revoke: b.secretAccessKeysRevoke, 42 } 43} 44 45func genUsername(displayName, policyName, userType string) (ret string, warning string) { 46 var midString string 47 48 switch userType { 49 case "iam_user": 50 // IAM users are capped at 64 chars; this leaves, after the beginning and 51 // end added below, 42 chars to play with. 52 midString = fmt.Sprintf("%s-%s-", 53 normalizeDisplayName(displayName), 54 normalizeDisplayName(policyName)) 55 if len(midString) > 42 { 56 midString = midString[0:42] 57 warning = "the calling token display name/IAM policy name were truncated to fit into IAM username length limits" 58 } 59 case "sts": 60 // Capped at 32 chars, which leaves only a couple of characters to play 61 // with, so don't insert display name or policy name at all 62 } 63 64 ret = fmt.Sprintf("vault-%s%d-%d", midString, time.Now().Unix(), rand.Int31n(10000)) 65 return 66} 67 68func (b *backend) secretTokenCreate(ctx context.Context, s logical.Storage, 69 displayName, policyName, policy string, 70 lifeTimeInSeconds int64) (*logical.Response, error) { 71 stsClient, err := b.clientSTS(ctx, s) 72 if err != nil { 73 return logical.ErrorResponse(err.Error()), nil 74 } 75 76 username, usernameWarning := genUsername(displayName, policyName, "sts") 77 78 tokenResp, err := stsClient.GetFederationToken( 79 &sts.GetFederationTokenInput{ 80 Name: aws.String(username), 81 Policy: aws.String(policy), 82 DurationSeconds: &lifeTimeInSeconds, 83 }) 84 85 if err != nil { 86 return logical.ErrorResponse(fmt.Sprintf( 87 "Error generating STS keys: %s", err)), awsutil.CheckAWSError(err) 88 } 89 90 resp := b.Secret(secretAccessKeyType).Response(map[string]interface{}{ 91 "access_key": *tokenResp.Credentials.AccessKeyId, 92 "secret_key": *tokenResp.Credentials.SecretAccessKey, 93 "security_token": *tokenResp.Credentials.SessionToken, 94 }, map[string]interface{}{ 95 "username": username, 96 "policy": policy, 97 "is_sts": true, 98 }) 99 100 // Set the secret TTL to appropriately match the expiration of the token 101 resp.Secret.TTL = tokenResp.Credentials.Expiration.Sub(time.Now()) 102 103 // STS are purposefully short-lived and aren't renewable 104 resp.Secret.Renewable = false 105 106 if usernameWarning != "" { 107 resp.AddWarning(usernameWarning) 108 } 109 110 return resp, nil 111} 112 113func (b *backend) assumeRole(ctx context.Context, s logical.Storage, 114 displayName, roleName, roleArn, policy string, 115 lifeTimeInSeconds int64) (*logical.Response, error) { 116 stsClient, err := b.clientSTS(ctx, s) 117 if err != nil { 118 return logical.ErrorResponse(err.Error()), nil 119 } 120 121 username, usernameWarning := genUsername(displayName, roleName, "iam_user") 122 123 assumeRoleInput := &sts.AssumeRoleInput{ 124 RoleSessionName: aws.String(username), 125 RoleArn: aws.String(roleArn), 126 DurationSeconds: &lifeTimeInSeconds, 127 } 128 if policy != "" { 129 assumeRoleInput.SetPolicy(policy) 130 } 131 tokenResp, err := stsClient.AssumeRole(assumeRoleInput) 132 133 if err != nil { 134 return logical.ErrorResponse(fmt.Sprintf( 135 "Error assuming role: %s", err)), awsutil.CheckAWSError(err) 136 } 137 138 resp := b.Secret(secretAccessKeyType).Response(map[string]interface{}{ 139 "access_key": *tokenResp.Credentials.AccessKeyId, 140 "secret_key": *tokenResp.Credentials.SecretAccessKey, 141 "security_token": *tokenResp.Credentials.SessionToken, 142 }, map[string]interface{}{ 143 "username": username, 144 "policy": roleArn, 145 "is_sts": true, 146 }) 147 148 // Set the secret TTL to appropriately match the expiration of the token 149 resp.Secret.TTL = tokenResp.Credentials.Expiration.Sub(time.Now()) 150 151 // STS are purposefully short-lived and aren't renewable 152 resp.Secret.Renewable = false 153 154 if usernameWarning != "" { 155 resp.AddWarning(usernameWarning) 156 } 157 158 return resp, nil 159} 160 161func (b *backend) secretAccessKeysCreate( 162 ctx context.Context, 163 s logical.Storage, 164 displayName, policyName string, role *awsRoleEntry) (*logical.Response, error) { 165 iamClient, err := b.clientIAM(ctx, s) 166 if err != nil { 167 return logical.ErrorResponse(err.Error()), nil 168 } 169 170 username, usernameWarning := genUsername(displayName, policyName, "iam_user") 171 172 // Write to the WAL that this user will be created. We do this before 173 // the user is created because if switch the order then the WAL put 174 // can fail, which would put us in an awkward position: we have a user 175 // we need to rollback but can't put the WAL entry to do the rollback. 176 walID, err := framework.PutWAL(ctx, s, "user", &walUser{ 177 UserName: username, 178 }) 179 if err != nil { 180 return nil, errwrap.Wrapf("error writing WAL entry: {{err}}", err) 181 } 182 183 userPath := role.UserPath 184 if userPath == "" { 185 userPath = "/" 186 } 187 188 // Create the user 189 _, err = iamClient.CreateUser(&iam.CreateUserInput{ 190 UserName: aws.String(username), 191 Path: aws.String(userPath), 192 }) 193 if err != nil { 194 if walErr := framework.DeleteWAL(ctx, s, walID); walErr != nil { 195 iamErr := errwrap.Wrapf("error creating IAM user: {{err}}", err) 196 return nil, errwrap.Wrap(errwrap.Wrapf("failed to delete WAL entry: {{err}}", walErr), iamErr) 197 } 198 return logical.ErrorResponse(fmt.Sprintf( 199 "Error creating IAM user: %s", err)), awsutil.CheckAWSError(err) 200 } 201 202 for _, arn := range role.PolicyArns { 203 // Attach existing policy against user 204 _, err = iamClient.AttachUserPolicy(&iam.AttachUserPolicyInput{ 205 UserName: aws.String(username), 206 PolicyArn: aws.String(arn), 207 }) 208 if err != nil { 209 return logical.ErrorResponse(fmt.Sprintf( 210 "Error attaching user policy: %s", err)), awsutil.CheckAWSError(err) 211 } 212 213 } 214 if role.PolicyDocument != "" { 215 // Add new inline user policy against user 216 _, err = iamClient.PutUserPolicy(&iam.PutUserPolicyInput{ 217 UserName: aws.String(username), 218 PolicyName: aws.String(policyName), 219 PolicyDocument: aws.String(role.PolicyDocument), 220 }) 221 if err != nil { 222 return logical.ErrorResponse(fmt.Sprintf( 223 "Error putting user policy: %s", err)), awsutil.CheckAWSError(err) 224 } 225 } 226 227 // Create the keys 228 keyResp, err := iamClient.CreateAccessKey(&iam.CreateAccessKeyInput{ 229 UserName: aws.String(username), 230 }) 231 if err != nil { 232 return logical.ErrorResponse(fmt.Sprintf( 233 "Error creating access keys: %s", err)), awsutil.CheckAWSError(err) 234 } 235 236 // Remove the WAL entry, we succeeded! If we fail, we don't return 237 // the secret because it'll get rolled back anyways, so we have to return 238 // an error here. 239 if err := framework.DeleteWAL(ctx, s, walID); err != nil { 240 return nil, errwrap.Wrapf("failed to commit WAL entry: {{err}}", err) 241 } 242 243 // Return the info! 244 resp := b.Secret(secretAccessKeyType).Response(map[string]interface{}{ 245 "access_key": *keyResp.AccessKey.AccessKeyId, 246 "secret_key": *keyResp.AccessKey.SecretAccessKey, 247 "security_token": nil, 248 }, map[string]interface{}{ 249 "username": username, 250 "policy": role, 251 "is_sts": false, 252 }) 253 254 lease, err := b.Lease(ctx, s) 255 if err != nil || lease == nil { 256 lease = &configLease{} 257 } 258 259 resp.Secret.TTL = lease.Lease 260 resp.Secret.MaxTTL = lease.LeaseMax 261 262 if usernameWarning != "" { 263 resp.AddWarning(usernameWarning) 264 } 265 266 return resp, nil 267} 268 269func (b *backend) secretAccessKeysRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 270 // STS already has a lifetime, and we don't support renewing it 271 isSTSRaw, ok := req.Secret.InternalData["is_sts"] 272 if ok { 273 isSTS, ok := isSTSRaw.(bool) 274 if ok { 275 if isSTS { 276 return nil, nil 277 } 278 } 279 } 280 281 lease, err := b.Lease(ctx, req.Storage) 282 if err != nil { 283 return nil, err 284 } 285 if lease == nil { 286 lease = &configLease{} 287 } 288 289 resp := &logical.Response{Secret: req.Secret} 290 resp.Secret.TTL = lease.Lease 291 resp.Secret.MaxTTL = lease.LeaseMax 292 return resp, nil 293} 294 295func (b *backend) secretAccessKeysRevoke(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 296 297 // STS cleans up after itself so we can skip this if is_sts internal data 298 // element set to true. If is_sts is not set, assumes old version 299 // and defaults to the IAM approach. 300 isSTSRaw, ok := req.Secret.InternalData["is_sts"] 301 if ok { 302 isSTS, ok := isSTSRaw.(bool) 303 if ok { 304 if isSTS { 305 return nil, nil 306 } 307 } else { 308 return nil, fmt.Errorf("secret has is_sts but value could not be understood") 309 } 310 } 311 312 // Get the username from the internal data 313 usernameRaw, ok := req.Secret.InternalData["username"] 314 if !ok { 315 return nil, fmt.Errorf("secret is missing username internal data") 316 } 317 username, ok := usernameRaw.(string) 318 if !ok { 319 return nil, fmt.Errorf("secret is missing username internal data") 320 } 321 322 // Use the user rollback mechanism to delete this user 323 err := b.pathUserRollback(ctx, req, "user", map[string]interface{}{ 324 "username": username, 325 }) 326 if err != nil { 327 return nil, err 328 } 329 330 return nil, nil 331} 332 333func normalizeDisplayName(displayName string) string { 334 re := regexp.MustCompile("[^a-zA-Z0-9+=,.@_-]") 335 return re.ReplaceAllString(displayName, "_") 336} 337