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