1package awsauth 2 3import ( 4 "context" 5 "crypto/hmac" 6 "crypto/sha256" 7 "crypto/subtle" 8 "encoding/base64" 9 "fmt" 10 "strconv" 11 "strings" 12 "time" 13 14 uuid "github.com/hashicorp/go-uuid" 15 "github.com/hashicorp/vault/helper/policyutil" 16 "github.com/hashicorp/vault/helper/strutil" 17 "github.com/hashicorp/vault/logical" 18 "github.com/hashicorp/vault/logical/framework" 19) 20 21const roleTagVersion = "v1" 22 23func pathRoleTag(b *backend) *framework.Path { 24 return &framework.Path{ 25 Pattern: "role/" + framework.GenericNameRegex("role") + "/tag$", 26 Fields: map[string]*framework.FieldSchema{ 27 "role": &framework.FieldSchema{ 28 Type: framework.TypeString, 29 Description: "Name of the role.", 30 }, 31 32 "instance_id": &framework.FieldSchema{ 33 Type: framework.TypeString, 34 Description: `Instance ID for which this tag is intended for. 35If set, the created tag can only be used by the instance with the given ID.`, 36 }, 37 38 "policies": &framework.FieldSchema{ 39 Type: framework.TypeCommaStringSlice, 40 Description: "Policies to be associated with the tag. If set, must be a subset of the role's policies. If set, but set to an empty value, only the 'default' policy will be given to issued tokens.", 41 }, 42 43 "max_ttl": &framework.FieldSchema{ 44 Type: framework.TypeDurationSecond, 45 Default: 0, 46 Description: "If set, specifies the maximum allowed token lifetime.", 47 }, 48 49 "allow_instance_migration": &framework.FieldSchema{ 50 Type: framework.TypeBool, 51 Default: false, 52 Description: "If set, allows migration of the underlying instance where the client resides. This keys off of pendingTime in the metadata document, so essentially, this disables the client nonce check whenever the instance is migrated to a new host and pendingTime is newer than the previously-remembered time. Use with caution.", 53 }, 54 55 "disallow_reauthentication": &framework.FieldSchema{ 56 Type: framework.TypeBool, 57 Default: false, 58 Description: "If set, only allows a single token to be granted per instance ID. In order to perform a fresh login, the entry in whitelist for the instance ID needs to be cleared using the 'auth/aws-ec2/identity-whitelist/<instance_id>' endpoint.", 59 }, 60 }, 61 62 Callbacks: map[logical.Operation]framework.OperationFunc{ 63 logical.UpdateOperation: b.pathRoleTagUpdate, 64 }, 65 66 HelpSynopsis: pathRoleTagSyn, 67 HelpDescription: pathRoleTagDesc, 68 } 69} 70 71// pathRoleTagUpdate is used to create an EC2 instance tag which will 72// identify the Vault resources that the instance will be authorized for. 73func (b *backend) pathRoleTagUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 74 roleName := strings.ToLower(data.Get("role").(string)) 75 if roleName == "" { 76 return logical.ErrorResponse("missing role"), nil 77 } 78 79 // Fetch the role entry 80 roleEntry, err := b.lockedAWSRole(ctx, req.Storage, roleName) 81 if err != nil { 82 return nil, err 83 } 84 if roleEntry == nil { 85 return logical.ErrorResponse(fmt.Sprintf("entry not found for role %s", roleName)), nil 86 } 87 88 // If RoleTag is empty, disallow creation of tag. 89 if roleEntry.RoleTag == "" { 90 return logical.ErrorResponse("tag creation is not enabled for this role"), nil 91 } 92 93 // There should be a HMAC key present in the role entry 94 if roleEntry.HMACKey == "" { 95 // Not being able to find the HMACKey is an internal error 96 return nil, fmt.Errorf("failed to find the HMAC key") 97 } 98 99 resp := &logical.Response{} 100 101 // Instance ID is an optional field. 102 instanceID := strings.ToLower(data.Get("instance_id").(string)) 103 104 // If no policies field was not supplied, then the tag should inherit all the policies 105 // on the role. But, it was provided, but set to empty explicitly, only "default" policy 106 // should be inherited. So, by leaving the policies var unset to anything when it is not 107 // supplied, we ensure that it inherits all the policies on the role. 108 var policies []string 109 policiesRaw, ok := data.GetOk("policies") 110 if ok { 111 policies = policyutil.ParsePolicies(policiesRaw) 112 } 113 if !strutil.StrListSubset(roleEntry.Policies, policies) { 114 resp.AddWarning("Policies on the tag are not a subset of the policies set on the role. Login will not be allowed with this tag unless the role policies are updated.") 115 } 116 117 // This is an optional field. 118 disallowReauthentication := data.Get("disallow_reauthentication").(bool) 119 120 // This is an optional field. 121 allowInstanceMigration := data.Get("allow_instance_migration").(bool) 122 if allowInstanceMigration && !roleEntry.AllowInstanceMigration { 123 resp.AddWarning("Role does not allow instance migration. Login will not be allowed with this tag unless the role value is updated.") 124 } 125 126 if disallowReauthentication && allowInstanceMigration { 127 return logical.ErrorResponse("cannot set both disallow_reauthentication and allow_instance_migration"), nil 128 } 129 130 // max_ttl for the role tag should be less than the max_ttl set on the role. 131 maxTTL := time.Duration(data.Get("max_ttl").(int)) * time.Second 132 133 // max_ttl on the tag should not be greater than the system view's max_ttl value. 134 if maxTTL > b.System().MaxLeaseTTL() { 135 resp.AddWarning(fmt.Sprintf("Given max TTL of %d is greater than the mount maximum of %d seconds, and will be capped at login time.", maxTTL/time.Second, b.System().MaxLeaseTTL()/time.Second)) 136 } 137 // If max_ttl is set for the role, check the bounds for tag's max_ttl value using that. 138 if roleEntry.MaxTTL != time.Duration(0) && maxTTL > roleEntry.MaxTTL { 139 resp.AddWarning(fmt.Sprintf("Given max TTL of %d is greater than the role maximum of %d seconds, and will be capped at login time.", maxTTL/time.Second, roleEntry.MaxTTL/time.Second)) 140 } 141 142 if maxTTL < time.Duration(0) { 143 return logical.ErrorResponse("max_ttl cannot be negative"), nil 144 } 145 146 // Create a random nonce. 147 nonce, err := createRoleTagNonce() 148 if err != nil { 149 return nil, err 150 } 151 152 // Create a role tag out of all the information provided. 153 rTagValue, err := createRoleTagValue(&roleTag{ 154 Version: roleTagVersion, 155 Role: roleName, 156 Nonce: nonce, 157 Policies: policies, 158 MaxTTL: maxTTL, 159 InstanceID: instanceID, 160 DisallowReauthentication: disallowReauthentication, 161 AllowInstanceMigration: allowInstanceMigration, 162 }, roleEntry) 163 if err != nil { 164 return nil, err 165 } 166 167 // Return the key to be used for the tag and the value to be used for that tag key. 168 // This key value pair should be set on the EC2 instance. 169 resp.Data = map[string]interface{}{ 170 "tag_key": roleEntry.RoleTag, 171 "tag_value": rTagValue, 172 } 173 174 return resp, nil 175} 176 177// createRoleTagValue prepares the plaintext version of the role tag, 178// and appends a HMAC of the plaintext value to it, before returning. 179func createRoleTagValue(rTag *roleTag, roleEntry *awsRoleEntry) (string, error) { 180 if rTag == nil { 181 return "", fmt.Errorf("nil role tag") 182 } 183 184 if roleEntry == nil { 185 return "", fmt.Errorf("nil role entry") 186 } 187 188 // Attach version, nonce, policies and maxTTL to the role tag value. 189 rTagPlaintext, err := prepareRoleTagPlaintextValue(rTag) 190 if err != nil { 191 return "", err 192 } 193 194 // Attach HMAC to tag's plaintext and return. 195 return appendHMAC(rTagPlaintext, roleEntry) 196} 197 198// Takes in the plaintext part of the role tag, creates a HMAC of it and returns 199// a role tag value containing both the plaintext part and the HMAC part. 200func appendHMAC(rTagPlaintext string, roleEntry *awsRoleEntry) (string, error) { 201 if rTagPlaintext == "" { 202 return "", fmt.Errorf("empty role tag plaintext string") 203 } 204 205 if roleEntry == nil { 206 return "", fmt.Errorf("nil role entry") 207 } 208 209 // Create the HMAC of the value 210 hmacB64, err := createRoleTagHMACBase64(roleEntry.HMACKey, rTagPlaintext) 211 if err != nil { 212 return "", err 213 } 214 215 // attach the HMAC to the value 216 rTagValue := fmt.Sprintf("%s:%s", rTagPlaintext, hmacB64) 217 218 // This limit of 255 is enforced on the EC2 instance. Hence complying to that here. 219 if len(rTagValue) > 255 { 220 return "", fmt.Errorf("role tag 'value' exceeding the limit of 255 characters") 221 } 222 223 return rTagValue, nil 224} 225 226// verifyRoleTagValue rebuilds the role tag's plaintext part, computes the HMAC 227// from it using the role specific HMAC key and compares it with the received HMAC. 228func verifyRoleTagValue(rTag *roleTag, roleEntry *awsRoleEntry) (bool, error) { 229 if rTag == nil { 230 return false, fmt.Errorf("nil role tag") 231 } 232 233 if roleEntry == nil { 234 return false, fmt.Errorf("nil role entry") 235 } 236 237 // Fetch the plaintext part of role tag 238 rTagPlaintext, err := prepareRoleTagPlaintextValue(rTag) 239 if err != nil { 240 return false, err 241 } 242 243 // Compute the HMAC of the plaintext 244 hmacB64, err := createRoleTagHMACBase64(roleEntry.HMACKey, rTagPlaintext) 245 if err != nil { 246 return false, err 247 } 248 249 return subtle.ConstantTimeCompare([]byte(rTag.HMAC), []byte(hmacB64)) == 1, nil 250} 251 252// prepareRoleTagPlaintextValue builds the role tag value without the HMAC in it. 253func prepareRoleTagPlaintextValue(rTag *roleTag) (string, error) { 254 if rTag == nil { 255 return "", fmt.Errorf("nil role tag") 256 } 257 if rTag.Version == "" { 258 return "", fmt.Errorf("missing version") 259 } 260 if rTag.Nonce == "" { 261 return "", fmt.Errorf("missing nonce") 262 } 263 if rTag.Role == "" { 264 return "", fmt.Errorf("missing role") 265 } 266 267 // Attach Version, Nonce, Role, DisallowReauthentication and AllowInstanceMigration 268 // fields to the role tag. 269 value := fmt.Sprintf("%s:%s:r=%s:d=%s:m=%s", rTag.Version, rTag.Nonce, rTag.Role, strconv.FormatBool(rTag.DisallowReauthentication), strconv.FormatBool(rTag.AllowInstanceMigration)) 270 271 // Attach the policies only if they are specified. 272 if len(rTag.Policies) != 0 { 273 value = fmt.Sprintf("%s:p=%s", value, strings.Join(rTag.Policies, ",")) 274 } 275 276 // Attach instance_id if set. 277 if rTag.InstanceID != "" { 278 value = fmt.Sprintf("%s:i=%s", value, rTag.InstanceID) 279 } 280 281 // Attach max_ttl if it is provided. 282 if int(rTag.MaxTTL.Seconds()) > 0 { 283 value = fmt.Sprintf("%s:t=%d", value, int(rTag.MaxTTL.Seconds())) 284 } 285 286 return value, nil 287} 288 289// Parses the tag from string form into a struct form. This method 290// also verifies the correctness of the parsed role tag. 291func (b *backend) parseAndVerifyRoleTagValue(ctx context.Context, s logical.Storage, tag string) (*roleTag, error) { 292 tagItems := strings.Split(tag, ":") 293 294 // Tag must contain version, nonce, policies and HMAC 295 if len(tagItems) < 4 { 296 return nil, fmt.Errorf("invalid tag") 297 } 298 299 rTag := &roleTag{} 300 301 // Cache the HMAC value. The last item in the collection. 302 rTag.HMAC = tagItems[len(tagItems)-1] 303 304 // Remove the HMAC from the list. 305 tagItems = tagItems[:len(tagItems)-1] 306 307 // Version will be the first element. 308 rTag.Version = tagItems[0] 309 if rTag.Version != roleTagVersion { 310 return nil, fmt.Errorf("invalid role tag version") 311 } 312 313 // Nonce will be the second element. 314 rTag.Nonce = tagItems[1] 315 316 // Delete the version and nonce from the list. 317 tagItems = tagItems[2:] 318 319 for _, tagItem := range tagItems { 320 var err error 321 switch { 322 case strings.HasPrefix(tagItem, "i="): 323 rTag.InstanceID = strings.TrimPrefix(tagItem, "i=") 324 case strings.HasPrefix(tagItem, "r="): 325 rTag.Role = strings.TrimPrefix(tagItem, "r=") 326 case strings.HasPrefix(tagItem, "p="): 327 rTag.Policies = strings.Split(strings.TrimPrefix(tagItem, "p="), ",") 328 case strings.HasPrefix(tagItem, "d="): 329 rTag.DisallowReauthentication, err = strconv.ParseBool(strings.TrimPrefix(tagItem, "d=")) 330 if err != nil { 331 return nil, err 332 } 333 case strings.HasPrefix(tagItem, "m="): 334 rTag.AllowInstanceMigration, err = strconv.ParseBool(strings.TrimPrefix(tagItem, "m=")) 335 if err != nil { 336 return nil, err 337 } 338 case strings.HasPrefix(tagItem, "t="): 339 rTag.MaxTTL, err = time.ParseDuration(fmt.Sprintf("%ss", strings.TrimPrefix(tagItem, "t="))) 340 if err != nil { 341 return nil, err 342 } 343 default: 344 return nil, fmt.Errorf("unrecognized item %q in tag", tagItem) 345 } 346 } 347 348 if rTag.Role == "" { 349 return nil, fmt.Errorf("missing role name") 350 } 351 352 roleEntry, err := b.lockedAWSRole(ctx, s, rTag.Role) 353 if err != nil { 354 return nil, err 355 } 356 if roleEntry == nil { 357 return nil, fmt.Errorf("entry not found for %q", rTag.Role) 358 } 359 360 // Create a HMAC of the plaintext value of role tag and compare it with the given value. 361 verified, err := verifyRoleTagValue(rTag, roleEntry) 362 if err != nil { 363 return nil, err 364 } 365 if !verified { 366 return nil, fmt.Errorf("role tag signature verification failed") 367 } 368 369 return rTag, nil 370} 371 372// Creates base64 encoded HMAC using a per-role key. 373func createRoleTagHMACBase64(key, value string) (string, error) { 374 if key == "" { 375 return "", fmt.Errorf("invalid HMAC key") 376 } 377 hm := hmac.New(sha256.New, []byte(key)) 378 hm.Write([]byte(value)) 379 380 // base64 encode the hmac bytes. 381 return base64.StdEncoding.EncodeToString(hm.Sum(nil)), nil 382} 383 384// Creates a base64 encoded random nonce. 385func createRoleTagNonce() (string, error) { 386 if uuidBytes, err := uuid.GenerateRandomBytes(8); err != nil { 387 return "", err 388 } else { 389 return base64.StdEncoding.EncodeToString(uuidBytes), nil 390 } 391} 392 393// Struct roleTag represents a role tag in a struct form. 394type roleTag struct { 395 Version string `json:"version"` 396 InstanceID string `json:"instance_id"` 397 Nonce string `json:"nonce"` 398 Policies []string `json:"policies"` 399 MaxTTL time.Duration `json:"max_ttl"` 400 Role string `json:"role"` 401 HMAC string `json:"hmac"` 402 DisallowReauthentication bool `json:"disallow_reauthentication"` 403 AllowInstanceMigration bool `json:"allow_instance_migration"` 404} 405 406func (rTag1 *roleTag) Equal(rTag2 *roleTag) bool { 407 return rTag1 != nil && 408 rTag2 != nil && 409 rTag1.Version == rTag2.Version && 410 rTag1.Nonce == rTag2.Nonce && 411 policyutil.EquivalentPolicies(rTag1.Policies, rTag2.Policies) && 412 rTag1.MaxTTL == rTag2.MaxTTL && 413 rTag1.Role == rTag2.Role && 414 rTag1.HMAC == rTag2.HMAC && 415 rTag1.InstanceID == rTag2.InstanceID && 416 rTag1.DisallowReauthentication == rTag2.DisallowReauthentication && 417 rTag1.AllowInstanceMigration == rTag2.AllowInstanceMigration 418} 419 420const pathRoleTagSyn = ` 421Create a tag on a role in order to be able to further restrict the capabilities of a role. 422` 423 424const pathRoleTagDesc = ` 425If there are needs to apply only a subset of role's capabilities to any specific 426instance, create a role tag using this endpoint and attach the tag on the instance 427before performing login. 428 429To be able to create a role tag, the 'role_tag' option on the role should be 430enabled via the endpoint 'role/<role>'. Also, the policies to be associated 431with the tag should be a subset of the policies associated with the registered role. 432 433This endpoint will return both the 'key' and the 'value' of the tag to be set 434on the EC2 instance. 435` 436