From 8a115c73d90b7449ed0a43a1c5581f702fb6edca Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Fri, 2 Mar 2018 11:09:14 -0500 Subject: [PATCH] auth/aws: Allow lists in binds (#3907) * auth/aws: Allow lists in binds In the aws auth method, allow a number of binds to take in lists instead of a single string value. The intended semantic is that, for each bind type set, clients must match at least one of each of the bind types set in order to authenticate. --- builtin/credential/aws/backend_test.go | 33 ++- builtin/credential/aws/path_login.go | 103 ++++--- builtin/credential/aws/path_role.go | 345 ++++++++++++++-------- builtin/credential/aws/path_role_test.go | 43 ++- website/source/api/auth/aws/index.html.md | 87 +++--- website/source/docs/auth/aws.html.md | 16 +- 6 files changed, 391 insertions(+), 236 deletions(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 23c81d43d6d5..2e8f0eb3e9d7 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -1084,7 +1084,7 @@ func TestBackendAcc_LoginWithInstanceIdentityDocAndWhitelistIdentity(t *testing. "auth_type": "ec2", "policies": "root", "max_ttl": "120s", - "bound_ami_id": "wrong_ami_id", + "bound_ami_id": []string{"wrong_ami_id", "wrong_ami_id2"}, "bound_account_id": accountID, "bound_iam_role_arn": iamARN, } @@ -1108,10 +1108,10 @@ func TestBackendAcc_LoginWithInstanceIdentityDocAndWhitelistIdentity(t *testing. t.Fatalf("bad: expected error response: resp:%#v\nerr:%v", resp, err) } - // Place the correct AMI ID, but make the AccountID wrong + // Place the correct AMI ID in one of the values, but make the AccountID wrong roleReq.Operation = logical.UpdateOperation - data["bound_ami_id"] = amiID - data["bound_account_id"] = "wrong-account-id" + data["bound_ami_id"] = []string{"wrong_ami_id_1", amiID, "wrong_ami_id_2"} + data["bound_account_id"] = []string{"wrong-account-id", "wrong-account-id-2"} resp, err = b.HandleRequest(context.Background(), roleReq) if err != nil || (resp != nil && resp.IsError()) { t.Fatalf("bad: failed to create role: resp:%#v\nerr:%v", resp, err) @@ -1123,9 +1123,9 @@ func TestBackendAcc_LoginWithInstanceIdentityDocAndWhitelistIdentity(t *testing. t.Fatalf("bad: expected error response: resp:%#v\nerr:%v", resp, err) } - // Place the correct AccountID, but make the wrong IAMRoleARN - data["bound_account_id"] = accountID - data["bound_iam_role_arn"] = "wrong_iam_role_arn" + // Place the correct AccountID in one of the values, but make the wrong IAMRoleARN + data["bound_account_id"] = []string{"wrong-account-id-1", accountID, "wrong-account-id-2"} + data["bound_iam_role_arn"] = []string{"wrong_iam_role_arn", "wrong_iam_role_arn_2"} resp, err = b.HandleRequest(context.Background(), roleReq) if err != nil || (resp != nil && resp.IsError()) { t.Fatalf("bad: failed to create role: resp:%#v\nerr:%v", resp, err) @@ -1137,8 +1137,8 @@ func TestBackendAcc_LoginWithInstanceIdentityDocAndWhitelistIdentity(t *testing. t.Fatalf("bad: expected error response: resp:%#v\nerr:%v", resp, err) } - // place the correct IAM role ARN - data["bound_iam_role_arn"] = iamARN + // place a correct IAM role ARN + data["bound_iam_role_arn"] = []string{"wrong_iam_role_arn_1", iamARN, "wrong_iam_role_arn_2"} resp, err = b.HandleRequest(context.Background(), roleReq) if err != nil || (resp != nil && resp.IsError()) { t.Fatalf("bad: failed to create role: resp:%#v\nerr:%v", resp, err) @@ -1456,7 +1456,7 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { // configuring the valid role we'll be able to login to roleData := map[string]interface{}{ - "bound_iam_principal_arn": entity.canonicalArn(), + "bound_iam_principal_arn": []string{entity.canonicalArn(), "arn:aws:iam::123456789012:role/FakeRoleArn1*"}, // Fake ARN MUST be wildcard terminated because we're resolving unique IDs, and the wildcard termination prevents unique ID resolution "policies": "root", "auth_type": iamAuthType, } @@ -1489,16 +1489,19 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { } fakeArn := "arn:aws:iam::123456789012:role/somePath/FakeRole" + fakeArn2 := "arn:aws:iam::123456789012:role/somePath/FakeRole2" + fakeArnResolverCount := 0 fakeArnResolver := func(ctx context.Context, s logical.Storage, arn string) (string, error) { - if arn == fakeArn { - return fmt.Sprintf("FakeUniqueIdFor%s", fakeArn), nil + if strings.HasPrefix(arn, fakeArn) { + fakeArnResolverCount++ + return fmt.Sprintf("FakeUniqueIdFor%s%d", arn, fakeArnResolverCount), nil } return b.resolveArnToRealUniqueId(context.Background(), s, arn) } b.resolveArnToUniqueIDFunc = fakeArnResolver // now we're creating the invalid role we won't be able to login to - roleData["bound_iam_principal_arn"] = fakeArn + roleData["bound_iam_principal_arn"] = []string{fakeArn, fakeArn2} roleRequest.Path = "role/" + testInvalidRoleName resp, err = b.HandleRequest(context.Background(), roleRequest) if err != nil || (resp != nil && resp.IsError()) { @@ -1630,11 +1633,11 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { wildcardRoleName := "valid_wildcard" wildcardEntity := *entity wildcardEntity.FriendlyName = "*" - roleData["bound_iam_principal_arn"] = wildcardEntity.canonicalArn() + roleData["bound_iam_principal_arn"] = []string{wildcardEntity.canonicalArn(), "arn:aws:iam::123456789012:role/DoesNotExist/Vault_Fake_Role*"} roleRequest.Path = "role/" + wildcardRoleName resp, err = b.HandleRequest(context.Background(), roleRequest) if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: failed to create wildcard role: resp:%#v\nerr:%v", resp, err) + t.Fatalf("bad: failed to create wildcard roles: resp:%#v\nerr:%v", resp, err) } loginData["role"] = wildcardRoleName diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index fb041b87bbe5..c6d98ab86e86 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -386,7 +386,7 @@ func (b *backend) verifyInstanceMeetsRoleRequirements(ctx context.Context, // Verify that the AccountID of the instance trying to login matches the // AccountID specified as a constraint on role - if roleEntry.BoundAccountID != "" && identityDoc.AccountID != roleEntry.BoundAccountID { + if len(roleEntry.BoundAccountIDs) > 0 && !strutil.StrListContains(roleEntry.BoundAccountIDs, identityDoc.AccountID) { return fmt.Errorf("account ID %q does not belong to role %q", identityDoc.AccountID, roleName), nil } @@ -399,31 +399,31 @@ func (b *backend) verifyInstanceMeetsRoleRequirements(ctx context.Context, // already calling the API to validate the Instance ID anyway, so it shouldn't // matter. The benefit is that we have the exact same code whether auth_type // is ec2 or iam. - if roleEntry.BoundAmiID != "" { + if len(roleEntry.BoundAmiIDs) > 0 { if instance.ImageId == nil { return nil, fmt.Errorf("AMI ID in the instance description is nil") } - if roleEntry.BoundAmiID != *instance.ImageId { + if !strutil.StrListContains(roleEntry.BoundAmiIDs, *instance.ImageId) { return fmt.Errorf("AMI ID %q does not belong to role %q", instance.ImageId, roleName), nil } } // Validate the SubnetID if corresponding bound was set on the role - if roleEntry.BoundSubnetID != "" { + if len(roleEntry.BoundSubnetIDs) > 0 { if instance.SubnetId == nil { return nil, fmt.Errorf("subnet ID in the instance description is nil") } - if roleEntry.BoundSubnetID != *instance.SubnetId { + if !strutil.StrListContains(roleEntry.BoundSubnetIDs, *instance.SubnetId) { return fmt.Errorf("subnet ID %q does not satisfy the constraint on role %q", *instance.SubnetId, roleName), nil } } // Validate the VpcID if corresponding bound was set on the role - if roleEntry.BoundVpcID != "" { + if len(roleEntry.BoundVpcIDs) > 0 { if instance.VpcId == nil { return nil, fmt.Errorf("VPC ID in the instance description is nil") } - if roleEntry.BoundVpcID != *instance.VpcId { + if !strutil.StrListContains(roleEntry.BoundVpcIDs, *instance.VpcId) { return fmt.Errorf("VPC ID %q does not satisfy the constraint on role %q", *instance.VpcId, roleName), nil } } @@ -431,7 +431,7 @@ func (b *backend) verifyInstanceMeetsRoleRequirements(ctx context.Context, // Check if the IAM instance profile ARN of the instance trying to // login, matches the IAM instance profile ARN specified as a constraint // on the role - if roleEntry.BoundIamInstanceProfileARN != "" { + if len(roleEntry.BoundIamInstanceProfileARNs) > 0 { if instance.IamInstanceProfile == nil { return nil, fmt.Errorf("IAM instance profile in the instance description is nil") } @@ -439,14 +439,21 @@ func (b *backend) verifyInstanceMeetsRoleRequirements(ctx context.Context, return nil, fmt.Errorf("IAM instance profile ARN in the instance description is nil") } iamInstanceProfileARN := *instance.IamInstanceProfile.Arn - if !strings.HasPrefix(iamInstanceProfileARN, roleEntry.BoundIamInstanceProfileARN) { + matchesInstanceProfile := false + for _, boundInstanceProfileARN := range roleEntry.BoundIamInstanceProfileARNs { + if strings.HasPrefix(iamInstanceProfileARN, boundInstanceProfileARN) { + matchesInstanceProfile = true + break + } + } + if !matchesInstanceProfile { return fmt.Errorf("IAM instance profile ARN %q does not satisfy the constraint role %q", iamInstanceProfileARN, roleName), nil } } // Check if the IAM role ARN of the instance trying to login, matches // the IAM role ARN specified as a constraint on the role. - if roleEntry.BoundIamRoleARN != "" { + if len(roleEntry.BoundIamRoleARNs) > 0 { if instance.IamInstanceProfile == nil { return nil, fmt.Errorf("IAM instance profile in the instance description is nil") } @@ -484,7 +491,14 @@ func (b *backend) verifyInstanceMeetsRoleRequirements(ctx context.Context, return nil, fmt.Errorf("IAM role ARN could not be fetched") } - if !strings.HasPrefix(iamRoleARN, roleEntry.BoundIamRoleARN) { + matchesInstanceRoleARN := false + for _, boundIamRoleARN := range roleEntry.BoundIamRoleARNs { + if strings.HasPrefix(iamRoleARN, boundIamRoleARN) { + matchesInstanceRoleARN = true + break + } + } + if !matchesInstanceRoleARN { return fmt.Errorf("IAM role ARN %q does not satisfy the constraint role %q", iamRoleARN, roleName), nil } } @@ -588,7 +602,7 @@ func (b *backend) pathLoginUpdateEc2(ctx context.Context, req *logical.Request, // Verify that the `Region` of the instance trying to login matches the // `Region` specified as a constraint on role - if roleEntry.BoundRegion != "" && identityDocParsed.Region != roleEntry.BoundRegion { + if len(roleEntry.BoundRegions) > 0 && !strutil.StrListContains(roleEntry.BoundRegions, identityDocParsed.Region) { return logical.ErrorResponse(fmt.Sprintf("Region %q does not satisfy the constraint on role %q", identityDocParsed.Region, roleName)), nil } @@ -939,17 +953,20 @@ func (b *backend) pathLoginRenewIam(ctx context.Context, req *logical.Request, d // read the role directly to know what the bind is. It's a relatively small amount of leakage, in // some fairly corner cases, and in the most likely error case (role has been changed to a new ARN), // the error message is identical. - if roleEntry.BoundIamPrincipalARN != "" { + if len(roleEntry.BoundIamPrincipalARNs) > 0 { // We might not get here if all bindings were on the inferred entity, which we've already validated // above + // As with logins, there are three ways to pass this check: + // 1: clientUserId is in roleEntry.BoundIamPrincipalIDs (entries in roleEntry.BoundIamPrincipalIDs + // implies that roleEntry.ResolveAWSUniqueIDs is true) + // 2: roleEntry.ResolveAWSUniqueIDs is false and canonical_arn is in roleEntry.BoundIamPrincipalARNs + // 3: Full ARN matches one of the wildcard globs in roleEntry.BoundIamPrincipalARNs clientUserId, ok := req.Auth.Metadata["client_user_id"] - if ok && roleEntry.BoundIamPrincipalID != "" { - // Resolving unique IDs is enabled and the auth metadata contains the unique ID, so checking the - // unique ID is authoritative at this stage - if roleEntry.BoundIamPrincipalID != clientUserId { - return nil, fmt.Errorf("role no longer bound to ARN %q", canonicalArn) - } - } else if strings.HasSuffix(roleEntry.BoundIamPrincipalARN, "*") { + switch { + case ok && strutil.StrListContains(roleEntry.BoundIamPrincipalIDs, clientUserId): // check 1 passed + case !roleEntry.ResolveAWSUniqueIDs && strutil.StrListContains(roleEntry.BoundIamPrincipalARNs, canonicalArn): // check 2 passed + default: + // check 3 is a bit more complex, so we do it last fullArn := b.getCachedUserId(clientUserId) if fullArn == "" { entity, err := parseIamArn(canonicalArn) @@ -967,11 +984,16 @@ func (b *backend) pathLoginRenewIam(ctx context.Context, req *logical.Request, d b.setCachedUserId(clientUserId, fullArn) } } - if !strutil.GlobbedStringsMatch(roleEntry.BoundIamPrincipalARN, fullArn) { + matchedWildcardBind := false + for _, principalARN := range roleEntry.BoundIamPrincipalARNs { + if strings.HasSuffix(principalARN, "*") && strutil.GlobbedStringsMatch(principalARN, fullArn) { + matchedWildcardBind = true + break + } + } + if !matchedWildcardBind { return nil, fmt.Errorf("role no longer bound to ARN %q", canonicalArn) } - } else if roleEntry.BoundIamPrincipalARN != canonicalArn { - return nil, fmt.Errorf("role no longer bound to ARN %q", canonicalArn) } } @@ -1189,15 +1211,19 @@ func (b *backend) pathLoginUpdateIam(ctx context.Context, req *logical.Request, // The role creation should ensure that either we're inferring this is an EC2 instance // or that we're binding an ARN - // The only way BoundIamPrincipalID could get set is if BoundIamPrincipalARN was also set and - // resolving to internal IDs was turned on, which can't be turned off. So, there should be no - // way for this to be set and not match BoundIamPrincipalARN - if roleEntry.BoundIamPrincipalID != "" { - if callerUniqueId != roleEntry.BoundIamPrincipalID { - return logical.ErrorResponse(fmt.Sprintf("expected IAM %s %s to resolve to unique AWS ID %q but got %q instead", entity.Type, entity.FriendlyName, roleEntry.BoundIamPrincipalID, callerUniqueId)), nil - } - } else if roleEntry.BoundIamPrincipalARN != "" { - if strings.HasSuffix(roleEntry.BoundIamPrincipalARN, "*") { + if len(roleEntry.BoundIamPrincipalARNs) > 0 { + // As with renews, there are three ways to pass this check: + // 1: callerUniqueId is in roleEntry.BoundIamPrincipalIDs (entries in roleEntry.BoundIamPrincipalIDs + // implies that roleEntry.ResolveAWSUniqueIDs is true) + // 2: roleEntry.ResolveAWSUniqueIDs is false and entity.canonicalArn() is in roleEntry.BoundIamPrincipalARNs + // 3: Full ARN matches one of the wildcard globs in roleEntry.BoundIamPrincipalARNs + // Need to be able to handle pathological configurations such as roleEntry.BoundIamPrincipalARNs looking something like: + // arn:aw:iam::123456789012:{user/UserName,user/path/*,role/RoleName,role/path/*} + switch { + case strutil.StrListContains(roleEntry.BoundIamPrincipalIDs, callerUniqueId): // check 1 passed + case !roleEntry.ResolveAWSUniqueIDs && strutil.StrListContains(roleEntry.BoundIamPrincipalARNs, entity.canonicalArn()): // check 2 passed + default: + // evaluate check 3 fullArn := b.getCachedUserId(callerUniqueId) if fullArn == "" { fullArn, err = b.fullArn(ctx, entity, req.Storage) @@ -1209,13 +1235,16 @@ func (b *backend) pathLoginUpdateIam(ctx context.Context, req *logical.Request, } b.setCachedUserId(callerUniqueId, fullArn) } - if !strutil.GlobbedStringsMatch(roleEntry.BoundIamPrincipalARN, fullArn) { - // Note: Intentionally giving the exact same error message as a few lines below. Otherwise, we might leak information - // about whether the bound IAM principal ARN is a wildcard or not, and what that wildcard is. + matchedWildcardBind := false + for _, principalARN := range roleEntry.BoundIamPrincipalARNs { + if strings.HasSuffix(principalARN, "*") && strutil.GlobbedStringsMatch(principalARN, fullArn) { + matchedWildcardBind = true + break + } + } + if !matchedWildcardBind { return logical.ErrorResponse(fmt.Sprintf("IAM Principal %q does not belong to the role %q", callerID.Arn, roleName)), nil } - } else if roleEntry.BoundIamPrincipalARN != entity.canonicalArn() { - return logical.ErrorResponse(fmt.Sprintf("IAM Principal %q does not belong to the role %q", callerID.Arn, roleName)), nil } } diff --git a/builtin/credential/aws/path_role.go b/builtin/credential/aws/path_role.go index 843624da3dbd..2a1d463dc7ec 100644 --- a/builtin/credential/aws/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -7,11 +7,16 @@ import ( "time" "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/policyutil" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) +var ( + currentRoleStorageVersion = 1 +) + func pathRole(b *backend) *framework.Path { return &framework.Path{ Pattern: "role/" + framework.GenericNameRegex("role"), @@ -26,32 +31,33 @@ func pathRole(b *backend) *framework.Path { iam or ec2 and cannot be changed after role creation.`, }, "bound_ami_id": { - Type: framework.TypeString, + Type: framework.TypeCommaStringSlice, Description: `If set, defines a constraint on the EC2 instances that they should be -using the AMI ID specified by this parameter. This is only applicable when auth_type is ec2 -or inferred_entity_type is ec2_instance.`, +using one of the AMI IDs specified by this parameter. This is only applicable +when auth_type is ec2 or inferred_entity_type is ec2_instance.`, }, "bound_account_id": { - Type: framework.TypeString, + Type: framework.TypeCommaStringSlice, Description: `If set, defines a constraint on the EC2 instances that the account ID -in its identity document to match the one specified by this parameter. This is only -applicable when auth_type is ec2 or inferred_entity_type is ec2_instance.`, +in its identity document to match one of the IDs specified by this parameter. +This is only applicable when auth_type is ec2 or inferred_entity_type is +ec2_instance.`, }, "bound_iam_principal_arn": { - Type: framework.TypeString, - Description: `ARN of the IAM principal to bind to this role. Only applicable when + Type: framework.TypeCommaStringSlice, + Description: `ARN of the IAM principals to bind to this role. Only applicable when auth_type is iam.`, }, "bound_region": { - Type: framework.TypeString, + Type: framework.TypeCommaStringSlice, Description: `If set, defines a constraint on the EC2 instances that the region in -its identity document to match the one specified by this parameter. This is only +its identity document match one of the regions specified by this parameter. This is only applicable when auth_type is ec2.`, }, "bound_iam_role_arn": { - Type: framework.TypeString, + Type: framework.TypeCommaStringSlice, Description: `If set, defines a constraint on the authenticating EC2 instance -that it must match the IAM role ARN specified by this parameter. +that it must match one of the IAM role ARNs specified by this parameter. The value is prefix-matched (as though it were a glob ending in '*'). The configured IAM user or EC2 instance role must be allowed to execute the 'iam:GetInstanceProfile' action if this is specified. This is @@ -59,10 +65,10 @@ only applicable when auth_type is ec2 or inferred_entity_type is ec2_instance.`, }, "bound_iam_instance_profile_arn": { - Type: framework.TypeString, + Type: framework.TypeCommaStringSlice, Description: `If set, defines a constraint on the EC2 instances to be associated with an IAM instance profile ARN which has a prefix that matches -the value specified by this parameter. The value is prefix-matched +one of the values specified by this parameter. The value is prefix-matched (as though it were a glob ending in '*'). This is only applicable when auth_type is ec2 or inferred_entity_type is ec2_instance.`, }, @@ -93,18 +99,19 @@ fail.`, inferred_entity_type is set, the region to assume the inferred entity exists in.`, }, "bound_vpc_id": { - Type: framework.TypeString, + Type: framework.TypeCommaStringSlice, Description: ` -If set, defines a constraint on the EC2 instance to be associated with the VPC -ID that matches the value specified by this parameter. This is only applicable -when auth_type is ec2 or inferred_entity_type is ec2_instance.`, +If set, defines a constraint on the EC2 instance to be associated with a VPC +ID that matches one of the value specified by this parameter. This is only +applicable when auth_type is ec2 or inferred_entity_type is ec2_instance.`, }, "bound_subnet_id": { - Type: framework.TypeString, + Type: framework.TypeCommaStringSlice, Description: ` If set, defines a constraint on the EC2 instance to be associated with the -subnet ID that matches the value specified by this parameter. This is only -applicable when auth_type is ec2 or inferred_entity_type is ec2_instance.`, +subnet ID that matches one of the values specified by this parameter. This is +only applicable when auth_type is ec2 or inferred_entity_type is +ec2_instance.`, }, "role_tag": { Type: framework.TypeString, @@ -232,7 +239,7 @@ func (b *backend) lockedAWSRole(ctx context.Context, s logical.Storage, roleName if err != nil { return nil, fmt.Errorf("error upgrading roleEntry: %v", err) } - if needUpgrade { + if needUpgrade && (b.System().LocalMount() || !b.System().ReplicationState().HasState(consts.ReplicationPerformanceSecondary)) { b.roleMutex.Lock() defer b.roleMutex.Unlock() // Now that we have a R/W lock, we need to re-read the role entry in case it was @@ -307,35 +314,93 @@ func (b *backend) upgradeRoleEntry(ctx context.Context, s logical.Storage, roleE return false, fmt.Errorf("received nil roleEntry") } var upgraded bool - // Check if the value held by role ARN field is actually an instance profile ARN - if roleEntry.BoundIamRoleARN != "" && strings.Contains(roleEntry.BoundIamRoleARN, ":instance-profile/") { - // If yes, move it to the correct field - roleEntry.BoundIamInstanceProfileARN = roleEntry.BoundIamRoleARN + switch roleEntry.Version { + case 0: + // Check if the value held by role ARN field is actually an instance profile ARN + if roleEntry.BoundIamRoleARN != "" && strings.Contains(roleEntry.BoundIamRoleARN, ":instance-profile/") { + // If yes, move it to the correct field + roleEntry.BoundIamInstanceProfileARN = roleEntry.BoundIamRoleARN - // Reset the old field - roleEntry.BoundIamRoleARN = "" + // Reset the old field + roleEntry.BoundIamRoleARN = "" - upgraded = true - } + upgraded = true + } - // Check if there was no pre-existing AuthType set (from older versions) - if roleEntry.AuthType == "" { - // then default to the original behavior of ec2 - roleEntry.AuthType = ec2AuthType - upgraded = true - } + // Check if there was no pre-existing AuthType set (from older versions) + if roleEntry.AuthType == "" { + // then default to the original behavior of ec2 + roleEntry.AuthType = ec2AuthType + upgraded = true + } - if roleEntry.AuthType == iamAuthType && - roleEntry.ResolveAWSUniqueIDs && - roleEntry.BoundIamPrincipalARN != "" && - roleEntry.BoundIamPrincipalID == "" && - !strings.HasSuffix(roleEntry.BoundIamPrincipalARN, "*") { - principalId, err := b.resolveArnToUniqueIDFunc(ctx, s, roleEntry.BoundIamPrincipalARN) - if err != nil { - return false, err + // Check if we need to resolve the unique ID on the role + if roleEntry.AuthType == iamAuthType && + roleEntry.ResolveAWSUniqueIDs && + roleEntry.BoundIamPrincipalARN != "" && + roleEntry.BoundIamPrincipalID == "" && + !strings.HasSuffix(roleEntry.BoundIamPrincipalARN, "*") { + principalId, err := b.resolveArnToUniqueIDFunc(ctx, s, roleEntry.BoundIamPrincipalARN) + if err != nil { + return false, err + } + roleEntry.BoundIamPrincipalID = principalId + // Not setting roleEntry.BoundIamPrincipalARN to "" here so that clients can see the original + // ARN that the role was bound to + upgraded = true + } + + // Check if we need to convert individual string values to lists + if roleEntry.BoundAmiID != "" { + roleEntry.BoundAmiIDs = []string{roleEntry.BoundAmiID} + roleEntry.BoundAmiID = "" + upgraded = true + } + if roleEntry.BoundAccountID != "" { + roleEntry.BoundAccountIDs = []string{roleEntry.BoundAccountID} + roleEntry.BoundAccountID = "" + upgraded = true + } + if roleEntry.BoundIamPrincipalARN != "" { + roleEntry.BoundIamPrincipalARNs = []string{roleEntry.BoundIamPrincipalARN} + roleEntry.BoundIamPrincipalARN = "" + upgraded = true + } + if roleEntry.BoundIamPrincipalID != "" { + roleEntry.BoundIamPrincipalIDs = []string{roleEntry.BoundIamPrincipalID} + roleEntry.BoundIamPrincipalID = "" + upgraded = true + } + if roleEntry.BoundIamRoleARN != "" { + roleEntry.BoundIamRoleARNs = []string{roleEntry.BoundIamRoleARN} + roleEntry.BoundIamRoleARN = "" + upgraded = true + } + if roleEntry.BoundIamInstanceProfileARN != "" { + roleEntry.BoundIamInstanceProfileARNs = []string{roleEntry.BoundIamInstanceProfileARN} + roleEntry.BoundIamInstanceProfileARN = "" + upgraded = true + } + if roleEntry.BoundRegion != "" { + roleEntry.BoundRegions = []string{roleEntry.BoundRegion} + roleEntry.BoundRegion = "" + upgraded = true } - roleEntry.BoundIamPrincipalID = principalId - upgraded = true + if roleEntry.BoundSubnetID != "" { + roleEntry.BoundSubnetIDs = []string{roleEntry.BoundSubnetID} + roleEntry.BoundSubnetID = "" + upgraded = true + } + if roleEntry.BoundVpcID != "" { + roleEntry.BoundVpcIDs = []string{roleEntry.BoundVpcID} + roleEntry.BoundVpcID = "" + upgraded = true + } + roleEntry.Version = 1 + fallthrough + case currentRoleStorageVersion: + default: + return false, fmt.Errorf("unrecognized role version: %q", roleEntry.Version) } return upgraded, nil @@ -405,28 +470,7 @@ func (b *backend) pathRoleRead(ctx context.Context, req *logical.Request, data * } return &logical.Response{ - Data: map[string]interface{}{ - "auth_type": roleEntry.AuthType, - "bound_ami_id": roleEntry.BoundAmiID, - "bound_account_id": roleEntry.BoundAccountID, - "bound_iam_principal_arn": roleEntry.BoundIamPrincipalARN, - "bound_iam_principal_id": roleEntry.BoundIamPrincipalID, - "bound_iam_role_arn": roleEntry.BoundIamRoleARN, - "bound_iam_instance_profile_arn": roleEntry.BoundIamInstanceProfileARN, - "bound_region": roleEntry.BoundRegion, - "bound_subnet_id": roleEntry.BoundSubnetID, - "bound_vpc_id": roleEntry.BoundVpcID, - "inferred_entity_type": roleEntry.InferredEntityType, - "inferred_aws_region": roleEntry.InferredAWSRegion, - "resolve_aws_unique_ids": roleEntry.ResolveAWSUniqueIDs, - "role_tag": roleEntry.RoleTag, - "allow_instance_migration": roleEntry.AllowInstanceMigration, - "ttl": roleEntry.TTL / time.Second, - "max_ttl": roleEntry.MaxTTL / time.Second, - "policies": roleEntry.Policies, - "disallow_reauthentication": roleEntry.DisallowReauthentication, - "period": roleEntry.Period / time.Second, - }, + Data: roleEntry.ToResponseData(), }, nil } @@ -445,7 +489,9 @@ func (b *backend) pathRoleCreateUpdate(ctx context.Context, req *logical.Request return nil, err } if roleEntry == nil { - roleEntry = &awsRoleEntry{} + roleEntry = &awsRoleEntry{ + Version: currentRoleStorageVersion, + } } else { needUpdate, err := b.upgradeRoleEntry(ctx, req.Storage, roleEntry) if err != nil { @@ -462,23 +508,23 @@ func (b *backend) pathRoleCreateUpdate(ctx context.Context, req *logical.Request // Fetch and set the bound parameters. There can't be default values // for these. if boundAmiIDRaw, ok := data.GetOk("bound_ami_id"); ok { - roleEntry.BoundAmiID = boundAmiIDRaw.(string) + roleEntry.BoundAmiIDs = boundAmiIDRaw.([]string) } if boundAccountIDRaw, ok := data.GetOk("bound_account_id"); ok { - roleEntry.BoundAccountID = boundAccountIDRaw.(string) + roleEntry.BoundAccountIDs = boundAccountIDRaw.([]string) } if boundRegionRaw, ok := data.GetOk("bound_region"); ok { - roleEntry.BoundRegion = boundRegionRaw.(string) + roleEntry.BoundRegions = boundRegionRaw.([]string) } if boundVpcIDRaw, ok := data.GetOk("bound_vpc_id"); ok { - roleEntry.BoundVpcID = boundVpcIDRaw.(string) + roleEntry.BoundVpcIDs = boundVpcIDRaw.([]string) } if boundSubnetIDRaw, ok := data.GetOk("bound_subnet_id"); ok { - roleEntry.BoundSubnetID = boundSubnetIDRaw.(string) + roleEntry.BoundSubnetIDs = boundSubnetIDRaw.([]string) } if resolveAWSUniqueIDsRaw, ok := data.GetOk("resolve_aws_unique_ids"); ok { @@ -495,37 +541,29 @@ func (b *backend) pathRoleCreateUpdate(ctx context.Context, req *logical.Request } if boundIamRoleARNRaw, ok := data.GetOk("bound_iam_role_arn"); ok { - roleEntry.BoundIamRoleARN = boundIamRoleARNRaw.(string) + roleEntry.BoundIamRoleARNs = boundIamRoleARNRaw.([]string) } if boundIamInstanceProfileARNRaw, ok := data.GetOk("bound_iam_instance_profile_arn"); ok { - roleEntry.BoundIamInstanceProfileARN = boundIamInstanceProfileARNRaw.(string) + roleEntry.BoundIamInstanceProfileARNs = boundIamInstanceProfileARNRaw.([]string) } if boundIamPrincipalARNRaw, ok := data.GetOk("bound_iam_principal_arn"); ok { - principalARN := boundIamPrincipalARNRaw.(string) - roleEntry.BoundIamPrincipalARN = principalARN - // Explicitly not checking to see if the user has changed the ARN under us - // This allows the user to sumbit an update with the same ARN to force Vault - // to re-resolve the ARN to the unique ID, in case an entity was deleted and - // recreated - if roleEntry.ResolveAWSUniqueIDs && roleEntry.BoundIamPrincipalARN != "" && !strings.HasSuffix(roleEntry.BoundIamPrincipalARN, "*") { - principalID, err := b.resolveArnToUniqueIDFunc(ctx, req.Storage, principalARN) - if err != nil { - return logical.ErrorResponse(fmt.Sprintf("failed updating the unique ID of ARN %#v: %#v", principalARN, err)), nil + principalARNs := boundIamPrincipalARNRaw.([]string) + roleEntry.BoundIamPrincipalARNs = principalARNs + roleEntry.BoundIamPrincipalIDs = []string{} + } + if roleEntry.ResolveAWSUniqueIDs && len(roleEntry.BoundIamPrincipalIDs) == 0 { + // we might be turning on resolution on this role, so ensure we update the IDs + for _, principalARN := range roleEntry.BoundIamPrincipalARNs { + if !strings.HasSuffix(principalARN, "*") { + principalID, err := b.resolveArnToUniqueIDFunc(ctx, req.Storage, principalARN) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("unable to resolve ARN %#v to internal ID: %#v", principalARN, err)), nil + } + roleEntry.BoundIamPrincipalIDs = append(roleEntry.BoundIamPrincipalIDs, principalID) } - roleEntry.BoundIamPrincipalID = principalID - } else { - // Need to handle the case where we're switching from a non-wildcard principal to a wildcard principal - roleEntry.BoundIamPrincipalID = "" } - } else if roleEntry.ResolveAWSUniqueIDs && roleEntry.BoundIamPrincipalARN != "" && !strings.HasSuffix(roleEntry.BoundIamPrincipalARN, "*") { - // we're turning on resolution on this role, so ensure we update it - principalID, err := b.resolveArnToUniqueIDFunc(ctx, req.Storage, roleEntry.BoundIamPrincipalARN) - if err != nil { - return logical.ErrorResponse(fmt.Sprintf("unable to resolve ARN %#v to internal ID: %#v", roleEntry.BoundIamPrincipalARN, err)), nil - } - roleEntry.BoundIamPrincipalID = principalID } if inferRoleTypeRaw, ok := data.GetOk("inferred_entity_type"); ok { @@ -581,56 +619,56 @@ func (b *backend) pathRoleCreateUpdate(ctx context.Context, req *logical.Request numBinds := 0 - if roleEntry.BoundAccountID != "" { + if len(roleEntry.BoundAccountIDs) > 0 { if !allowEc2Binds { return logical.ErrorResponse(fmt.Sprintf("specified bound_account_id but not allowing ec2 auth_type or inferring %s", ec2EntityType)), nil } numBinds++ } - if roleEntry.BoundRegion != "" { + if len(roleEntry.BoundRegions) > 0 { if roleEntry.AuthType != ec2AuthType { return logical.ErrorResponse("specified bound_region but not allowing ec2 auth_type"), nil } numBinds++ } - if roleEntry.BoundAmiID != "" { + if len(roleEntry.BoundAmiIDs) > 0 { if !allowEc2Binds { return logical.ErrorResponse(fmt.Sprintf("specified bound_ami_id but not allowing ec2 auth_type or inferring %s", ec2EntityType)), nil } numBinds++ } - if roleEntry.BoundIamInstanceProfileARN != "" { + if len(roleEntry.BoundIamInstanceProfileARNs) > 0 { if !allowEc2Binds { return logical.ErrorResponse(fmt.Sprintf("specified bound_iam_instance_profile_arn but not allowing ec2 auth_type or inferring %s", ec2EntityType)), nil } numBinds++ } - if roleEntry.BoundIamRoleARN != "" { + if len(roleEntry.BoundIamRoleARNs) > 0 { if !allowEc2Binds { return logical.ErrorResponse(fmt.Sprintf("specified bound_iam_role_arn but not allowing ec2 auth_type or inferring %s", ec2EntityType)), nil } numBinds++ } - if roleEntry.BoundIamPrincipalARN != "" { + if len(roleEntry.BoundIamPrincipalARNs) > 0 { if roleEntry.AuthType != iamAuthType { return logical.ErrorResponse("specified bound_iam_principal_arn but not allowing iam auth_type"), nil } numBinds++ } - if roleEntry.BoundVpcID != "" { + if len(roleEntry.BoundVpcIDs) > 0 { if !allowEc2Binds { return logical.ErrorResponse(fmt.Sprintf("specified bound_vpc_id but not allowing ec2 auth_type or inferring %s", ec2EntityType)), nil } numBinds++ } - if roleEntry.BoundSubnetID != "" { + if len(roleEntry.BoundSubnetIDs) > 0 { if !allowEc2Binds { return logical.ErrorResponse(fmt.Sprintf("specified bound_subnet_id but not allowing ec2 auth_type or inferring %s", ec2EntityType)), nil } @@ -751,29 +789,82 @@ func (b *backend) pathRoleCreateUpdate(ctx context.Context, req *logical.Request return &resp, nil } -// Struct to hold the information associated with an AMI ID in Vault. +// Struct to hold the information associated with a Vault role type awsRoleEntry struct { - AuthType string `json:"auth_type"` - BoundAmiID string `json:"bound_ami_id"` - BoundAccountID string `json:"bound_account_id"` - BoundIamPrincipalARN string `json:"bound_iam_principal_arn"` - BoundIamPrincipalID string `json:"bound_iam_principal_id"` - BoundIamRoleARN string `json:"bound_iam_role_arn"` - BoundIamInstanceProfileARN string `json:"bound_iam_instance_profile_arn"` - BoundRegion string `json:"bound_region"` - BoundSubnetID string `json:"bound_subnet_id"` - BoundVpcID string `json:"bound_vpc_id"` - InferredEntityType string `json:"inferred_entity_type"` - InferredAWSRegion string `json:"inferred_aws_region"` - ResolveAWSUniqueIDs bool `json:"resolve_aws_unique_ids"` - RoleTag string `json:"role_tag"` - AllowInstanceMigration bool `json:"allow_instance_migration"` - TTL time.Duration `json:"ttl"` - MaxTTL time.Duration `json:"max_ttl"` - Policies []string `json:"policies"` - DisallowReauthentication bool `json:"disallow_reauthentication"` - HMACKey string `json:"hmac_key"` - Period time.Duration `json:"period"` + AuthType string `json:"auth_type" ` + BoundAmiIDs []string `json:"bound_ami_id_list"` + BoundAccountIDs []string `json:"bound_account_id_list"` + BoundIamPrincipalARNs []string `json:"bound_iam_principal_arn_list"` + BoundIamPrincipalIDs []string `json:"bound_iam_principal_id_list"` + BoundIamRoleARNs []string `json:"bound_iam_role_arn_list"` + BoundIamInstanceProfileARNs []string `json:"bound_iam_instance_profile_arn_list"` + BoundRegions []string `json:"bound_region_list"` + BoundSubnetIDs []string `json:"bound_subnet_id_list"` + BoundVpcIDs []string `json:"bound_vpc_id_list"` + InferredEntityType string `json:"inferred_entity_type"` + InferredAWSRegion string `json:"inferred_aws_region"` + ResolveAWSUniqueIDs bool `json:"resolve_aws_unique_ids"` + RoleTag string `json:"role_tag"` + AllowInstanceMigration bool `json:"allow_instance_migration"` + TTL time.Duration `json:"ttl"` + MaxTTL time.Duration `json:"max_ttl"` + Policies []string `json:"policies"` + DisallowReauthentication bool `json:"disallow_reauthentication"` + HMACKey string `json:"hmac_key"` + Period time.Duration `json:"period"` + Version int `json:"version"` + // DEPRECATED -- these are the old fields before we supported lists and exist for backwards compatibility + BoundAmiID string `json:"bound_ami_id,omitempty" ` + BoundAccountID string `json:"bound_account_id,omitempty"` + BoundIamPrincipalARN string `json:"bound_iam_principal_arn,omitempty"` + BoundIamPrincipalID string `json:"bound_iam_principal_id,omitempty"` + BoundIamRoleARN string `json:"bound_iam_role_arn,omitempty"` + BoundIamInstanceProfileARN string `json:"bound_iam_instance_profile_arn,omitempty"` + BoundRegion string `json:"bound_region,omitempty"` + BoundSubnetID string `json:"bound_subnet_id,omitempty"` + BoundVpcID string `json:"bound_vpc_id,omitempty"` +} + +func (r *awsRoleEntry) ToResponseData() map[string]interface{} { + responseData := map[string]interface{}{ + "auth_type": r.AuthType, + "bound_ami_id": r.BoundAmiIDs, + "bound_account_id": r.BoundAccountIDs, + "bound_iam_principal_arn": r.BoundIamPrincipalARNs, + "bound_iam_principal_id": r.BoundIamPrincipalIDs, + "bound_iam_role_arn": r.BoundIamRoleARNs, + "bound_iam_instance_profile_arn": r.BoundIamInstanceProfileARNs, + "bound_region": r.BoundRegions, + "bound_subnet_id": r.BoundSubnetIDs, + "bound_vpc_id": r.BoundVpcIDs, + "inferred_entity_type": r.InferredEntityType, + "inferred_aws_region": r.InferredAWSRegion, + "resolve_aws_unique_ids": r.ResolveAWSUniqueIDs, + "role_tag": r.RoleTag, + "allow_instance_migration": r.AllowInstanceMigration, + "ttl": r.TTL / time.Second, + "max_ttl": r.MaxTTL / time.Second, + "policies": r.Policies, + "disallow_reauthentication": r.DisallowReauthentication, + "period": r.Period / time.Second, + } + + convertNilToEmptySlice := func(data map[string]interface{}, field string) { + if data[field] == nil || len(data[field].([]string)) == 0 { + data[field] = []string{} + } + } + convertNilToEmptySlice(responseData, "bound_ami_id") + convertNilToEmptySlice(responseData, "bound_account_id") + convertNilToEmptySlice(responseData, "bound_iam_principal_arn") + convertNilToEmptySlice(responseData, "bound_iam_principal_id") + convertNilToEmptySlice(responseData, "bound_iam_role_arn") + convertNilToEmptySlice(responseData, "bound_iam_instance_profile_arn") + convertNilToEmptySlice(responseData, "bound_region") + convertNilToEmptySlice(responseData, "bound_subnet_id") + convertNilToEmptySlice(responseData, "bound_vpc_id") + + return responseData } const pathRoleSyn = ` diff --git a/builtin/credential/aws/path_role_test.go b/builtin/credential/aws/path_role_test.go index 9af052257a17..18f6a3e40aad 100644 --- a/builtin/credential/aws/path_role_test.go +++ b/builtin/credential/aws/path_role_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/hashicorp/vault/helper/policyutil" + "github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/logical" ) @@ -187,10 +188,11 @@ func Test_enableIamIDResolution(t *testing.T) { b.resolveArnToUniqueIDFunc = resolveArnToFakeUniqueId + boundIamRoleARNs := []string{"arn:aws:iam::123456789012:role/MyRole", "arn:aws:iam::123456789012:role/path/*"} data := map[string]interface{}{ "auth_type": iamAuthType, "policies": "p,q", - "bound_iam_principal_arn": "arn:aws:iam::123456789012:role/MyRole", + "bound_iam_principal_arn": boundIamRoleARNs, "resolve_aws_unique_ids": false, } @@ -218,7 +220,7 @@ func Test_enableIamIDResolution(t *testing.T) { if resp == nil || resp.IsError() { t.Fatalf("failed to read role: resp:%#v,\nerr:%#v", resp, err) } - if resp.Data["bound_iam_principal_id"] != "" { + if resp.Data["bound_iam_principal_id"] != nil && len(resp.Data["bound_iam_principal_id"].([]string)) > 0 { t.Fatalf("expected to get no unique ID in role, but got %q", resp.Data["bound_iam_principal_id"]) } @@ -240,9 +242,14 @@ func Test_enableIamIDResolution(t *testing.T) { if resp == nil || resp.IsError() { t.Fatalf("failed to read role: resp:%#v,\nerr:%#v", resp, err) } - if resp.Data["bound_iam_principal_id"] != "FakeUniqueId1" { + principalIDs := resp.Data["bound_iam_principal_id"].([]string) + if len(principalIDs) != 1 || principalIDs[0] != "FakeUniqueId1" { t.Fatalf("bad: expected upgrade of role resolve principal ID to %q, but got %q instead", "FakeUniqueId1", resp.Data["bound_iam_principal_id"]) } + returnedARNs := resp.Data["bound_iam_principal_arn"].([]string) + if !strutil.EquivalentSlices(returnedARNs, boundIamRoleARNs) { + t.Fatalf("bad: expected to return bound_iam_principal_arn of %q, but got %q instead", boundIamRoleARNs, returnedARNs) + } } func TestBackend_pathIam(t *testing.T) { @@ -466,7 +473,8 @@ func TestBackend_pathRoleMixedTypes(t *testing.T) { data["auth_type"] = iamAuthType delete(data, "bound_ami_id") - data["bound_iam_principal_arn"] = "arn:aws:iam::123456789012:role/MyRole" + boundIamPrincipalARNs := []string{"arn:aws:iam::123456789012:role/MyRole", "arn:aws:iam::123456789012:role/path/*"} + data["bound_iam_principal_arn"] = boundIamPrincipalARNs resp, err = submitRequest("ec2_to_iam", logical.UpdateOperation) if resp == nil || !resp.IsError() { t.Fatalf("changed auth type on the role") @@ -499,9 +507,14 @@ func TestBackend_pathRoleMixedTypes(t *testing.T) { if err != nil { t.Fatal(err) } - if resp.Data["bound_iam_principal_id"] != "FakeUniqueId1" { + principalIDs := resp.Data["bound_iam_principal_id"].([]string) + if len(principalIDs) != 1 || principalIDs[0] != "FakeUniqueId1" { t.Fatalf("expected fake unique ID of FakeUniqueId1, got %q", resp.Data["bound_iam_principal_id"]) } + returnedARNs := resp.Data["bound_iam_principal_arn"].([]string) + if !strutil.EquivalentSlices(returnedARNs, boundIamPrincipalARNs) { + t.Fatalf("bad: expected to return bound_iam_principal_arn of %q, but got %q instead", boundIamPrincipalARNs, returnedARNs) + } data["resolve_aws_unique_ids"] = false resp, err = submitRequest("withInternalIdResolution", logical.UpdateOperation) if err != nil { @@ -584,15 +597,15 @@ func TestAwsEc2_RoleCrud(t *testing.T) { expected := map[string]interface{}{ "auth_type": ec2AuthType, - "bound_ami_id": "testamiid", - "bound_account_id": "testaccountid", - "bound_region": "testregion", - "bound_iam_principal_arn": "", - "bound_iam_principal_id": "", - "bound_iam_role_arn": "arn:aws:iam::123456789012:role/MyRole", - "bound_iam_instance_profile_arn": "arn:aws:iam::123456789012:instance-profile/MyInstanceProfile", - "bound_subnet_id": "testsubnetid", - "bound_vpc_id": "testvpcid", + "bound_ami_id": []string{"testamiid"}, + "bound_account_id": []string{"testaccountid"}, + "bound_region": []string{"testregion"}, + "bound_iam_principal_arn": []string{}, + "bound_iam_principal_id": []string{}, + "bound_iam_role_arn": []string{"arn:aws:iam::123456789012:role/MyRole"}, + "bound_iam_instance_profile_arn": []string{"arn:aws:iam::123456789012:instance-profile/MyInstanceProfile"}, + "bound_subnet_id": []string{"testsubnetid"}, + "bound_vpc_id": []string{"testvpcid"}, "inferred_entity_type": "", "inferred_aws_region": "", "resolve_aws_unique_ids": false, @@ -624,7 +637,7 @@ func TestAwsEc2_RoleCrud(t *testing.T) { t.Fatalf("resp: %#v, err: %v", resp, err) } - expected["bound_vpc_id"] = "newvpcid" + expected["bound_vpc_id"] = []string{"newvpcid"} if !reflect.DeepEqual(expected, resp.Data) { t.Fatalf("bad: role data: expected: %#v\n actual: %#v", expected, resp.Data) diff --git a/website/source/api/auth/aws/index.html.md b/website/source/api/auth/aws/index.html.md index 551abaeec87f..23a63b1bae56 100644 --- a/website/source/api/auth/aws/index.html.md +++ b/website/source/api/auth/aws/index.html.md @@ -529,11 +529,13 @@ Registers a role in the method. Only those instances or principals which are using the role registered using this endpoint, will be able to perform the login operation. Contraints can be specified on the role, that are applied on the instances or principals attempting to login. At least one -constraint should be specified on the role. The available constraints you +constraint must be specified on the role. The available constraints you can choose are dependent on the `auth_type` of the role and, if the `auth_type` is `iam`, then whether inferencing is enabled. A role will not let you configure a constraint if it is not checked by the `auth_type` and -inferencing configuration of that role. +inferencing configuration of that role. For the constraints which accept a list +of values, the authenticating instance/principal must match any one value in the +list in order to satisfy that constraint. | Method | Path | Produces | | :------- | :--------------------------- | :--------------------- | @@ -547,53 +549,64 @@ inferencing configuration of that role. "iam" (except for legacy `aws-ec2` auth types, for which it will default to "ec2"). Only those bindings applicable to the auth type chosen will be allowed to be configured on the role. -- `bound_ami_id` `(string: "")` - If set, defines a constraint on the EC2 - instances that they should be using the AMI ID specified by this parameter. +- `bound_ami_id` `(list: [])` - If set, defines a constraint on the EC2 + instances that they should be using one of the AMI ID specified by this parameter. This constraint is checked during ec2 auth as well as the iam auth method only - when inferring an EC2 instance. -- `bound_account_id` `(string: "")` - If set, defines a constraint on the EC2 - instances that the account ID in its identity document to match the one + when inferring an EC2 instance. This is a comma-separated string or JSON + array. +- `bound_account_id` `(list: [])` - If set, defines a constraint on the EC2 + instances that the account ID in its identity document to match one of the ones specified by this parameter. This constraint is checked during ec2 auth as - well as the iam auth method only when inferring an EC2 instance. -- `bound_region` `(string: "")` - If set, defines a constraint on the EC2 - instances that the region in its identity document must match the one - specified by this parameter. This constraint is only checked by the ec2 auth + well as the iam auth method only when inferring an EC2 instance. This is a + comma-separated string or JSON array. +- `bound_region` `(list: [])` - If set, defines a constraint on the EC2 + instances that the region in its identity document must match one of the + regions specified by this parameter. This constraint is only checked by the ec2 auth method as well as the iam auth method only when inferring an ec2 instance. -- `bound_vpc_id` `(string: "")` - If set, defines a constraint on the EC2 - instance to be associated with the VPC ID that matches the value specified by + This is a comma-separated string or JSON array. +- `bound_vpc_id` `(list: [])` - If set, defines a constraint on the EC2 + instance to be associated with a VPC ID that matches one of the values specified by this parameter. This constraint is only checked by the ec2 auth method as well - as the iam auth method only when inferring an ec2 instance. -- `bound_subnet_id` `(string: "")` - If set, defines a constraint on the EC2 - instance to be associated with the subnet ID that matches the value specified + as the iam auth method only when inferring an ec2 instance. This is a + comma-separated string or JSON array. +- `bound_subnet_id` `(list: [])` - If set, defines a constraint on the EC2 + instance to be associated with a subnet ID that matches one of the values specified by this parameter. This constraint is only checked by the ec2 auth method as - well as the iam auth method only when inferring an ec2 instance. -- `bound_iam_role_arn` `(string: "")` - If set, defines a constraint on the - authenticating EC2 instance that it must match the IAM role ARN specified by + well as the iam auth method only when inferring an ec2 instance. This is a + comma-separated string or a JSON array. +- `bound_iam_role_arn` `(list: [])` - If set, defines a constraint on the + authenticating EC2 instance that it must match one of the IAM role ARNs specified by this parameter. The value is refix-matched (as though it were a glob ending in `*`). The configured IAM user or EC2 instance role must be allowed to execute the `iam:GetInstanceProfile` action if this is specified. This constraint is checked by the ec2 auth method as well as the iam auth method - only when inferring an EC2 instance. -- `bound_iam_instance_profile_arn` `(string: "")` - If set, defines a constraint + only when inferring an EC2 instance. This is a comma-separated string or a + JSON array. +- `bound_iam_instance_profile_arn` `(list: [])` - If set, defines a constraint on the EC2 instances to be associated with an IAM instance profile ARN which - has a prefix that matches the value specified by this parameter. The value is + has a prefix that matches one of the values specified by this parameter. The value is prefix-matched (as though it were a glob ending in `*`). This constraint is checked by the ec2 auth method as well as the iam auth method only when - inferring an ec2 instance. + inferring an ec2 instance. This is a comma-separated string or a JSON array. - `role_tag` `(string: "")` - If set, enables the role tags for this role. The value set for this field should be the 'key' of the tag on the EC2 instance. The 'value' of the tag should be generated using `role//tag` endpoint. Defaults to an empty string, meaning that role tags are disabled. This - constraint is valid only with the ec2 auth method and is not allowed when an - auth_type is iam. -- `bound_iam_principal_arn` `(string: "")` - Defines the IAM principal that must - be authenticated using the iam auth method. It should look like - "arn:aws:iam::123456789012:user/MyUserName" or + constraint is valid only with the ec2 auth method and is not allowed when + `auth_type` is iam. +- `bound_iam_principal_arn` `(list: [])` - Defines the list of IAM principals + that are permitted to login to the role using the iam auth method. Individual + values should look like "arn:aws:iam::123456789012:user/MyUserName" or "arn:aws:iam::123456789012:role/MyRoleName". Wildcards are supported at the end of the ARN, e.g., "arn:aws:iam::123456789012:\*" will match any IAM - principal in the AWS account 123456789012. This constraint is only checked by + principal in the AWS account 123456789012. When `resolve_aws_unique_ids` is + `false` and you are binding to IAM roles (as opposed to users) and you are not + using a wildcard at the end, then you must specify the ARN by ommitting any + path component; see the documentation for `resolve_aws_unique_ids` below. + This constraint is only checked by the iam auth method. Wildcards are supported at the end of the ARN, e.g., "arn:aws:iam::123456789012:role/\*" will match all roles in the AWS account. + This is a comma-separated string or JSON array. - `inferred_entity_type` `(string: "")` - When set, instructs Vault to turn on inferencing. The only current valid value is "ec2\_instance" instructing Vault to infer that the role comes from an EC2 instance in an IAM instance profile. @@ -631,11 +644,13 @@ inferencing configuration of that role. Vault still has the necessary IAM permissions to resolve the unique ID, Vault will update the unique ID. (If it does not have the necessary permissions to resolve the unique ID, then it will fail to update.) If this option is set to - false, then you MUST leave out the path component in bound_iam_principal_arn - for **roles** only, but not IAM users. That is, if your IAM role ARN is of the - form `arn:aws:iam::123456789012:role/some/path/to/MyRoleName`, you **must** - specify a bound_iam_principal_arn of - `arn:aws:iam::123456789012:role/MyRoleName` for authentication to work. + false, then you MUST leave out the path component in `bound_iam_principal_arn` + for **roles** that do not specify a wildcard at the end, but not IAM users or + role bindings that have a wildcard. That is, if your IAM role ARN is of the + form `arn:aws:iam::123456789012:role/some/path/to/MyRoleName`, and + `resolve_aws_unique_ids` is `false`, you **must** specify a + `bound_iam_principal_arn` of `arn:aws:iam::123456789012:role/MyRoleName` for + authentication to work. - `ttl` `(string: "")` - The TTL period of tokens issued using this role, provided as "1h", where hour is the largest suffix. - `max_ttl` `(string: "")` - The maximum allowed lifetime of tokens issued using @@ -665,7 +680,7 @@ inferencing configuration of that role. ```json { - "bound_ami_id": "ami-fce36987", + "bound_ami_id": ["ami-fce36987"], "role_tag": "", "policies": [ "default", @@ -713,7 +728,7 @@ $ curl \ ```json { "data": { - "bound_ami_id": "ami-fce36987", + "bound_ami_id": ["ami-fce36987"], "role_tag": "", "policies": [ "default", diff --git a/website/source/docs/auth/aws.html.md b/website/source/docs/auth/aws.html.md index 71517113c377..a52d6fb76a69 100644 --- a/website/source/docs/auth/aws.html.md +++ b/website/source/docs/auth/aws.html.md @@ -115,12 +115,16 @@ method and associated with a specific authentication type that cannot be changed once the role has been created. Roles can also be associated with various optional restrictions, such as the set of allowed policies and max TTLs on the generated tokens. Each role can be specified with the constraints that -are to be met during the login. For example, one such constraint that is -supported is to bind against AMI ID. A role which is bound to a specific AMI, -can only be used for login by EC2 instances that are deployed on the same AMI. - -The iam auth method allows you to specify a bound IAM principal ARN. -Clients authenticating to Vault must have an ARN that matches the ARN bound to +are to be met during the login. Many of these contraints accept lists of +required values. For any constraint which accepts a list of values, that +constraint will be considered satisfied if any one of the values is matched +during the login process. For example, one such constraint that is +supported is to bind against a list of AMI IDs. A role which is bound to a +specific list of AMIs can only be used for login by EC2 instances that are +deployed to one of the AMIs that the role is bound to. + +The iam auth method allows you to specify bound IAM principal ARNs. +Clients authenticating to Vault must have an ARN that matches one of the ARNs bound to the role they are attempting to login to. The bound ARN allows specifying a wildcard at the end of the bound ARN. For example, if the bound ARN were `arn:aws:iam::123456789012:*` it would allow any principal in AWS account