From 9439d2dd4e4b1251e9ce80023109167a2874e363 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Sat, 3 Feb 2018 23:10:28 -0500 Subject: [PATCH 01/12] 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/path_login.go | 103 ++++--- builtin/credential/aws/path_role.go | 345 +++++++++++++++------- builtin/credential/aws/path_role_test.go | 28 +- website/source/api/auth/aws/index.html.md | 12 +- 4 files changed, 321 insertions(+), 167 deletions(-) diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 43ef00ba194c..68ef9d42b8e1 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 a1903724ed17..2d5d53136324 100644 --- a/builtin/credential/aws/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -6,13 +6,16 @@ import ( "strings" "time" - "github.com/fatih/structs" "github.com/hashicorp/go-uuid" "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"), @@ -27,32 +30,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 @@ -60,10 +64,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.`, }, @@ -94,18 +98,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.`, +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, @@ -308,35 +313,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,19 +468,19 @@ func (b *backend) pathRoleRead(ctx context.Context, req *logical.Request, data * return nil, nil } - // Prepare the map of all the entries in the roleEntry. - respData := structs.New(roleEntry).Map() + // // Prepare the map of all the entries in the roleEntry. + // respData := structs.New(roleEntry).Map() - // HMAC key belonging to the role should NOT be exported. - delete(respData, "hmac_key") + // // HMAC key belonging to the role should NOT be exported. + // delete(respData, "hmac_key") - // Display all the durations in seconds - respData["ttl"] = roleEntry.TTL / time.Second - respData["max_ttl"] = roleEntry.MaxTTL / time.Second - respData["period"] = roleEntry.Period / time.Second + // // Display all the durations in seconds + // respData["ttl"] = roleEntry.TTL / time.Second + // respData["max_ttl"] = roleEntry.MaxTTL / time.Second + // respData["period"] = roleEntry.Period / time.Second return &logical.Response{ - Data: respData, + Data: roleEntry.ToResponseData(), }, nil } @@ -436,7 +499,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 { @@ -453,23 +518,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 { @@ -486,37 +551,41 @@ 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{} + for _, principalARN := range principalARNs { + // 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 && !strings.HasSuffix(principalARN, "*") { + 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 + } + 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 + } else 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 } if inferRoleTypeRaw, ok := data.GetOk("inferred_entity_type"); ok { @@ -572,56 +641,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 } @@ -742,29 +811,81 @@ 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" structs:"auth_type" mapstructure:"auth_type"` - BoundAmiID string `json:"bound_ami_id" structs:"bound_ami_id" mapstructure:"bound_ami_id"` - BoundAccountID string `json:"bound_account_id" structs:"bound_account_id" mapstructure:"bound_account_id"` - BoundIamPrincipalARN string `json:"bound_iam_principal_arn" structs:"bound_iam_principal_arn" mapstructure:"bound_iam_principal_arn"` - BoundIamPrincipalID string `json:"bound_iam_principal_id" structs:"bound_iam_principal_id" mapstructure:"bound_iam_principal_id"` - BoundIamRoleARN string `json:"bound_iam_role_arn" structs:"bound_iam_role_arn" mapstructure:"bound_iam_role_arn"` - BoundIamInstanceProfileARN string `json:"bound_iam_instance_profile_arn" structs:"bound_iam_instance_profile_arn" mapstructure:"bound_iam_instance_profile_arn"` - BoundRegion string `json:"bound_region" structs:"bound_region" mapstructure:"bound_region"` - BoundSubnetID string `json:"bound_subnet_id" structs:"bound_subnet_id" mapstructure:"bound_subnet_id"` - BoundVpcID string `json:"bound_vpc_id" structs:"bound_vpc_id" mapstructure:"bound_vpc_id"` - InferredEntityType string `json:"inferred_entity_type" structs:"inferred_entity_type" mapstructure:"inferred_entity_type"` - InferredAWSRegion string `json:"inferred_aws_region" structs:"inferred_aws_region" mapstructure:"inferred_aws_region"` - ResolveAWSUniqueIDs bool `json:"resolve_aws_unique_ids" structs:"resolve_aws_unique_ids" mapstructure:"resolve_aws_unique_ids"` - RoleTag string `json:"role_tag" structs:"role_tag" mapstructure:"role_tag"` - AllowInstanceMigration bool `json:"allow_instance_migration" structs:"allow_instance_migration" mapstructure:"allow_instance_migration"` - TTL time.Duration `json:"ttl" structs:"ttl" mapstructure:"ttl"` - MaxTTL time.Duration `json:"max_ttl" structs:"max_ttl" mapstructure:"max_ttl"` - Policies []string `json:"policies" structs:"policies" mapstructure:"policies"` - DisallowReauthentication bool `json:"disallow_reauthentication" structs:"disallow_reauthentication" mapstructure:"disallow_reauthentication"` - HMACKey string `json:"hmac_key" structs:"hmac_key" mapstructure:"hmac_key"` - Period time.Duration `json:"period" mapstructure:"period" structs:"period"` + AuthType string `json:"auth_type" structs:"auth_type" mapstructure:"auth_type"` + BoundAmiID string `json:"bound_ami_id,omitempty" structs:"bound_ami_id" mapstructure:"bound_ami_id"` + BoundAmiIDs []string `json:"bound_ami_id_list" structs:"bound_ami_id_list" mapstructure:"bound_ami_id_list"` + BoundAccountID string `json:"bound_account_id,omitempty" structs:"bound_account_id" mapstructure:"bound_account_id"` + BoundAccountIDs []string `json:"bound_account_id_list" structs:"bound_account_id_list" mapstructure:"bound_account_id_list"` + BoundIamPrincipalARN string `json:"bound_iam_principal_arn,omitempty" structs:"bound_iam_principal_arn" mapstructure:"bound_iam_principal_arn"` + BoundIamPrincipalARNs []string `json:"bound_iam_principal_arn_list" structs:"bound_iam_principal_arn_list" mapstructure:"bound_iam_principal_arn_list"` + BoundIamPrincipalID string `json:"bound_iam_principal_id,omitempty" structs:"bound_iam_principal_id" mapstructure:"bound_iam_principal_id"` + BoundIamPrincipalIDs []string `json:"bound_iam_principal_id_list" structs:"bound_iam_principal_id_list" mapstructure:"bound_iam_principal_id_list"` + BoundIamRoleARN string `json:"bound_iam_role_arn,omitempty" structs:"bound_iam_role_arn" mapstructure:"bound_iam_role_arn"` + BoundIamRoleARNs []string `json:"bound_iam_role_arn_list" structs:"bound_iam_role_arn_list" mapstructure:"bound_iam_role_arn_list"` + BoundIamInstanceProfileARN string `json:"bound_iam_instance_profile_arn,omitempty" structs:"bound_iam_instance_profile_arn" mapstructure:"bound_iam_instance_profile_arn"` + BoundIamInstanceProfileARNs []string `json:"bound_iam_instance_profile_arn_list" structs:"bound_iam_instance_profile_arn_list" mapstructure:"bound_iam_instance_profile_arn_list"` + BoundRegion string `json:"bound_region,omitempty" structs:"bound_region" mapstructure:"bound_region"` + BoundRegions []string `json:"bound_region_list" structs:"bound_region_list" mapstructure:"bound_region_list"` + BoundSubnetID string `json:"bound_subnet_id,omitempty" structs:"bound_subnet_id" mapstructure:"bound_subnet_id"` + BoundSubnetIDs []string `json:"bound_subnet_id_list" structs:"bound_subnet_id_list" mapstructure:"bound_subnet_id_list"` + BoundVpcID string `json:"bound_vpc_id,omitempty" structs:"bound_vpc_id" mapstructure:"bound_vpc_id"` + BoundVpcIDs []string `json:"bound_vpc_id_list" structs:"bound_vpc_id_list" mapstructure:"bound_vpc_id_list"` + InferredEntityType string `json:"inferred_entity_type" structs:"inferred_entity_type" mapstructure:"inferred_entity_type"` + InferredAWSRegion string `json:"inferred_aws_region" structs:"inferred_aws_region" mapstructure:"inferred_aws_region"` + ResolveAWSUniqueIDs bool `json:"resolve_aws_unique_ids" structs:"resolve_aws_unique_ids" mapstructure:"resolve_aws_unique_ids"` + RoleTag string `json:"role_tag" structs:"role_tag" mapstructure:"role_tag"` + AllowInstanceMigration bool `json:"allow_instance_migration" structs:"allow_instance_migration" mapstructure:"allow_instance_migration"` + TTL time.Duration `json:"ttl" structs:"ttl" mapstructure:"ttl"` + MaxTTL time.Duration `json:"max_ttl" structs:"max_ttl" mapstructure:"max_ttl"` + Policies []string `json:"policies" structs:"policies" mapstructure:"policies"` + DisallowReauthentication bool `json:"disallow_reauthentication" structs:"disallow_reauthentication" mapstructure:"disallow_reauthentication"` + HMACKey string `json:"hmac_key" structs:"hmac_key" mapstructure:"hmac_key"` + Period time.Duration `json:"period" mapstructure:"period" structs:"period"` + version int `json:"version" mapstructure:"version" structs:"versoin"` +} + +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..928c85740a9d 100644 --- a/builtin/credential/aws/path_role_test.go +++ b/builtin/credential/aws/path_role_test.go @@ -218,7 +218,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,7 +240,8 @@ 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" { + principal_ids := resp.Data["bound_iam_principal_id"].([]string) + if len(principal_ids) != 1 || principal_ids[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"]) } } @@ -499,7 +500,8 @@ func TestBackend_pathRoleMixedTypes(t *testing.T) { if err != nil { t.Fatal(err) } - if resp.Data["bound_iam_principal_id"] != "FakeUniqueId1" { + principal_ids := resp.Data["bound_iam_principal_id"].([]string) + if len(principal_ids) != 1 || principal_ids[0] != "FakeUniqueId1" { t.Fatalf("expected fake unique ID of FakeUniqueId1, got %q", resp.Data["bound_iam_principal_id"]) } data["resolve_aws_unique_ids"] = false @@ -584,15 +586,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 +626,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 9182ec41eef2..306bd3d644ee 100644 --- a/website/source/api/auth/aws/index.html.md +++ b/website/source/api/auth/aws/index.html.md @@ -628,11 +628,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 From 8c8c00119755efd1b9a51c43aa0b25533f335940 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Mon, 5 Feb 2018 20:46:59 -0500 Subject: [PATCH 02/12] Add guard around upgrading role entry --- builtin/credential/aws/path_role.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/credential/aws/path_role.go b/builtin/credential/aws/path_role.go index 2d5d53136324..572ffed4f9bd 100644 --- a/builtin/credential/aws/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -238,7 +238,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 From 2233abaed7e89588af9513fb765d245bc6134dd1 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Thu, 8 Feb 2018 01:34:54 -0500 Subject: [PATCH 03/12] Respond to PR feedback --- builtin/credential/aws/path_role.go | 82 +++++++++++++---------------- 1 file changed, 36 insertions(+), 46 deletions(-) diff --git a/builtin/credential/aws/path_role.go b/builtin/credential/aws/path_role.go index 572ffed4f9bd..2868d96d563c 100644 --- a/builtin/credential/aws/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -7,6 +7,7 @@ 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" @@ -313,7 +314,7 @@ func (b *backend) upgradeRoleEntry(ctx context.Context, s logical.Storage, roleE return false, fmt.Errorf("received nil roleEntry") } var upgraded bool - switch roleEntry.version { + 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/") { @@ -395,11 +396,11 @@ func (b *backend) upgradeRoleEntry(ctx context.Context, s logical.Storage, roleE roleEntry.BoundVpcID = "" upgraded = true } - roleEntry.version = 1 + roleEntry.Version = 1 fallthrough case currentRoleStorageVersion: default: - return false, fmt.Errorf("unrecognized role version: %q", roleEntry.version) + return false, fmt.Errorf("unrecognized role version: %q", roleEntry.Version) } return upgraded, nil @@ -468,17 +469,6 @@ func (b *backend) pathRoleRead(ctx context.Context, req *logical.Request, data * return nil, nil } - // // Prepare the map of all the entries in the roleEntry. - // respData := structs.New(roleEntry).Map() - - // // HMAC key belonging to the role should NOT be exported. - // delete(respData, "hmac_key") - - // // Display all the durations in seconds - // respData["ttl"] = roleEntry.TTL / time.Second - // respData["max_ttl"] = roleEntry.MaxTTL / time.Second - // respData["period"] = roleEntry.Period / time.Second - return &logical.Response{ Data: roleEntry.ToResponseData(), }, nil @@ -500,7 +490,7 @@ func (b *backend) pathRoleCreateUpdate(ctx context.Context, req *logical.Request } if roleEntry == nil { roleEntry = &awsRoleEntry{ - version: currentRoleStorageVersion, + Version: currentRoleStorageVersion, } } else { needUpdate, err := b.upgradeRoleEntry(ctx, req.Storage, roleEntry) @@ -813,37 +803,37 @@ func (b *backend) pathRoleCreateUpdate(ctx context.Context, req *logical.Request // Struct to hold the information associated with a Vault role type awsRoleEntry struct { - AuthType string `json:"auth_type" structs:"auth_type" mapstructure:"auth_type"` - BoundAmiID string `json:"bound_ami_id,omitempty" structs:"bound_ami_id" mapstructure:"bound_ami_id"` - BoundAmiIDs []string `json:"bound_ami_id_list" structs:"bound_ami_id_list" mapstructure:"bound_ami_id_list"` - BoundAccountID string `json:"bound_account_id,omitempty" structs:"bound_account_id" mapstructure:"bound_account_id"` - BoundAccountIDs []string `json:"bound_account_id_list" structs:"bound_account_id_list" mapstructure:"bound_account_id_list"` - BoundIamPrincipalARN string `json:"bound_iam_principal_arn,omitempty" structs:"bound_iam_principal_arn" mapstructure:"bound_iam_principal_arn"` - BoundIamPrincipalARNs []string `json:"bound_iam_principal_arn_list" structs:"bound_iam_principal_arn_list" mapstructure:"bound_iam_principal_arn_list"` - BoundIamPrincipalID string `json:"bound_iam_principal_id,omitempty" structs:"bound_iam_principal_id" mapstructure:"bound_iam_principal_id"` - BoundIamPrincipalIDs []string `json:"bound_iam_principal_id_list" structs:"bound_iam_principal_id_list" mapstructure:"bound_iam_principal_id_list"` - BoundIamRoleARN string `json:"bound_iam_role_arn,omitempty" structs:"bound_iam_role_arn" mapstructure:"bound_iam_role_arn"` - BoundIamRoleARNs []string `json:"bound_iam_role_arn_list" structs:"bound_iam_role_arn_list" mapstructure:"bound_iam_role_arn_list"` - BoundIamInstanceProfileARN string `json:"bound_iam_instance_profile_arn,omitempty" structs:"bound_iam_instance_profile_arn" mapstructure:"bound_iam_instance_profile_arn"` - BoundIamInstanceProfileARNs []string `json:"bound_iam_instance_profile_arn_list" structs:"bound_iam_instance_profile_arn_list" mapstructure:"bound_iam_instance_profile_arn_list"` - BoundRegion string `json:"bound_region,omitempty" structs:"bound_region" mapstructure:"bound_region"` - BoundRegions []string `json:"bound_region_list" structs:"bound_region_list" mapstructure:"bound_region_list"` - BoundSubnetID string `json:"bound_subnet_id,omitempty" structs:"bound_subnet_id" mapstructure:"bound_subnet_id"` - BoundSubnetIDs []string `json:"bound_subnet_id_list" structs:"bound_subnet_id_list" mapstructure:"bound_subnet_id_list"` - BoundVpcID string `json:"bound_vpc_id,omitempty" structs:"bound_vpc_id" mapstructure:"bound_vpc_id"` - BoundVpcIDs []string `json:"bound_vpc_id_list" structs:"bound_vpc_id_list" mapstructure:"bound_vpc_id_list"` - InferredEntityType string `json:"inferred_entity_type" structs:"inferred_entity_type" mapstructure:"inferred_entity_type"` - InferredAWSRegion string `json:"inferred_aws_region" structs:"inferred_aws_region" mapstructure:"inferred_aws_region"` - ResolveAWSUniqueIDs bool `json:"resolve_aws_unique_ids" structs:"resolve_aws_unique_ids" mapstructure:"resolve_aws_unique_ids"` - RoleTag string `json:"role_tag" structs:"role_tag" mapstructure:"role_tag"` - AllowInstanceMigration bool `json:"allow_instance_migration" structs:"allow_instance_migration" mapstructure:"allow_instance_migration"` - TTL time.Duration `json:"ttl" structs:"ttl" mapstructure:"ttl"` - MaxTTL time.Duration `json:"max_ttl" structs:"max_ttl" mapstructure:"max_ttl"` - Policies []string `json:"policies" structs:"policies" mapstructure:"policies"` - DisallowReauthentication bool `json:"disallow_reauthentication" structs:"disallow_reauthentication" mapstructure:"disallow_reauthentication"` - HMACKey string `json:"hmac_key" structs:"hmac_key" mapstructure:"hmac_key"` - Period time.Duration `json:"period" mapstructure:"period" structs:"period"` - version int `json:"version" mapstructure:"version" structs:"versoin"` + AuthType string `json:"auth_type" mapstructure:"auth_type"` + BoundAmiID string `json:"bound_ami_id,omitempty" mapstructure:"bound_ami_id"` + BoundAmiIDs []string `json:"bound_ami_id_list" mapstructure:"bound_ami_id_list"` + BoundAccountID string `json:"bound_account_id,omitempty" mapstructure:"bound_account_id"` + BoundAccountIDs []string `json:"bound_account_id_list" mapstructure:"bound_account_id_list"` + BoundIamPrincipalARN string `json:"bound_iam_principal_arn,omitempty" mapstructure:"bound_iam_principal_arn"` + BoundIamPrincipalARNs []string `json:"bound_iam_principal_arn_list" mapstructure:"bound_iam_principal_arn_list"` + BoundIamPrincipalID string `json:"bound_iam_principal_id,omitempty" mapstructure:"bound_iam_principal_id"` + BoundIamPrincipalIDs []string `json:"bound_iam_principal_id_list" mapstructure:"bound_iam_principal_id_list"` + BoundIamRoleARN string `json:"bound_iam_role_arn,omitempty" mapstructure:"bound_iam_role_arn"` + BoundIamRoleARNs []string `json:"bound_iam_role_arn_list" mapstructure:"bound_iam_role_arn_list"` + BoundIamInstanceProfileARN string `json:"bound_iam_instance_profile_arn,omitempty" mapstructure:"bound_iam_instance_profile_arn"` + BoundIamInstanceProfileARNs []string `json:"bound_iam_instance_profile_arn_list" mapstructure:"bound_iam_instance_profile_arn_list"` + BoundRegion string `json:"bound_region,omitempty" mapstructure:"bound_region"` + BoundRegions []string `json:"bound_region_list" mapstructure:"bound_region_list"` + BoundSubnetID string `json:"bound_subnet_id,omitempty" mapstructure:"bound_subnet_id"` + BoundSubnetIDs []string `json:"bound_subnet_id_list" mapstructure:"bound_subnet_id_list"` + BoundVpcID string `json:"bound_vpc_id,omitempty" mapstructure:"bound_vpc_id"` + BoundVpcIDs []string `json:"bound_vpc_id_list" mapstructure:"bound_vpc_id_list"` + InferredEntityType string `json:"inferred_entity_type" mapstructure:"inferred_entity_type"` + InferredAWSRegion string `json:"inferred_aws_region" mapstructure:"inferred_aws_region"` + ResolveAWSUniqueIDs bool `json:"resolve_aws_unique_ids" mapstructure:"resolve_aws_unique_ids"` + RoleTag string `json:"role_tag" mapstructure:"role_tag"` + AllowInstanceMigration bool `json:"allow_instance_migration" mapstructure:"allow_instance_migration"` + TTL time.Duration `json:"ttl" mapstructure:"ttl"` + MaxTTL time.Duration `json:"max_ttl" mapstructure:"max_ttl"` + Policies []string `json:"policies" mapstructure:"policies"` + DisallowReauthentication bool `json:"disallow_reauthentication" mapstructure:"disallow_reauthentication"` + HMACKey string `json:"hmac_key" mapstructure:"hmac_key"` + Period time.Duration `json:"period" mapstructure:"period"` + Version int `json:"version" mapstructure:"version"` } func (r *awsRoleEntry) ToResponseData() map[string]interface{} { From c10947746c581d3ed571fcf3bae6a47736281406 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Thu, 22 Feb 2018 00:08:03 -0500 Subject: [PATCH 04/12] Respond to PR feedback --- builtin/credential/aws/path_role.go | 98 +++++++++--------------- builtin/credential/aws/path_role_test.go | 4 +- 2 files changed, 37 insertions(+), 65 deletions(-) diff --git a/builtin/credential/aws/path_role.go b/builtin/credential/aws/path_role.go index 2868d96d563c..949959077c60 100644 --- a/builtin/credential/aws/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -552,20 +552,8 @@ func (b *backend) pathRoleCreateUpdate(ctx context.Context, req *logical.Request principalARNs := boundIamPrincipalARNRaw.([]string) roleEntry.BoundIamPrincipalARNs = principalARNs roleEntry.BoundIamPrincipalIDs = []string{} - for _, principalARN := range principalARNs { - // 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 && !strings.HasSuffix(principalARN, "*") { - 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 - } - roleEntry.BoundIamPrincipalIDs = append(roleEntry.BoundIamPrincipalIDs, principalID) - } - } - } else if roleEntry.ResolveAWSUniqueIDs && len(roleEntry.BoundIamPrincipalIDs) == 0 { + } + 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, "*") { @@ -803,41 +791,42 @@ func (b *backend) pathRoleCreateUpdate(ctx context.Context, req *logical.Request // Struct to hold the information associated with a Vault role type awsRoleEntry struct { - AuthType string `json:"auth_type" mapstructure:"auth_type"` - BoundAmiID string `json:"bound_ami_id,omitempty" mapstructure:"bound_ami_id"` - BoundAmiIDs []string `json:"bound_ami_id_list" mapstructure:"bound_ami_id_list"` - BoundAccountID string `json:"bound_account_id,omitempty" mapstructure:"bound_account_id"` - BoundAccountIDs []string `json:"bound_account_id_list" mapstructure:"bound_account_id_list"` - BoundIamPrincipalARN string `json:"bound_iam_principal_arn,omitempty" mapstructure:"bound_iam_principal_arn"` - BoundIamPrincipalARNs []string `json:"bound_iam_principal_arn_list" mapstructure:"bound_iam_principal_arn_list"` - BoundIamPrincipalID string `json:"bound_iam_principal_id,omitempty" mapstructure:"bound_iam_principal_id"` - BoundIamPrincipalIDs []string `json:"bound_iam_principal_id_list" mapstructure:"bound_iam_principal_id_list"` - BoundIamRoleARN string `json:"bound_iam_role_arn,omitempty" mapstructure:"bound_iam_role_arn"` - BoundIamRoleARNs []string `json:"bound_iam_role_arn_list" mapstructure:"bound_iam_role_arn_list"` - BoundIamInstanceProfileARN string `json:"bound_iam_instance_profile_arn,omitempty" mapstructure:"bound_iam_instance_profile_arn"` - BoundIamInstanceProfileARNs []string `json:"bound_iam_instance_profile_arn_list" mapstructure:"bound_iam_instance_profile_arn_list"` - BoundRegion string `json:"bound_region,omitempty" mapstructure:"bound_region"` - BoundRegions []string `json:"bound_region_list" mapstructure:"bound_region_list"` - BoundSubnetID string `json:"bound_subnet_id,omitempty" mapstructure:"bound_subnet_id"` - BoundSubnetIDs []string `json:"bound_subnet_id_list" mapstructure:"bound_subnet_id_list"` - BoundVpcID string `json:"bound_vpc_id,omitempty" mapstructure:"bound_vpc_id"` - BoundVpcIDs []string `json:"bound_vpc_id_list" mapstructure:"bound_vpc_id_list"` - InferredEntityType string `json:"inferred_entity_type" mapstructure:"inferred_entity_type"` - InferredAWSRegion string `json:"inferred_aws_region" mapstructure:"inferred_aws_region"` - ResolveAWSUniqueIDs bool `json:"resolve_aws_unique_ids" mapstructure:"resolve_aws_unique_ids"` - RoleTag string `json:"role_tag" mapstructure:"role_tag"` - AllowInstanceMigration bool `json:"allow_instance_migration" mapstructure:"allow_instance_migration"` - TTL time.Duration `json:"ttl" mapstructure:"ttl"` - MaxTTL time.Duration `json:"max_ttl" mapstructure:"max_ttl"` - Policies []string `json:"policies" mapstructure:"policies"` - DisallowReauthentication bool `json:"disallow_reauthentication" mapstructure:"disallow_reauthentication"` - HMACKey string `json:"hmac_key" mapstructure:"hmac_key"` - Period time.Duration `json:"period" mapstructure:"period"` - Version int `json:"version" mapstructure:"version"` + 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{}{ + return map[string]interface{}{ "auth_type": r.AuthType, "bound_ami_id": r.BoundAmiIDs, "bound_account_id": r.BoundAccountIDs, @@ -859,23 +848,6 @@ func (r *awsRoleEntry) ToResponseData() map[string]interface{} { "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 928c85740a9d..3c8baf961fb0 100644 --- a/builtin/credential/aws/path_role_test.go +++ b/builtin/credential/aws/path_role_test.go @@ -589,8 +589,8 @@ func TestAwsEc2_RoleCrud(t *testing.T) { "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_principal_arn": []string(nil), + "bound_iam_principal_id": []string(nil), "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"}, From c3426917cbe8fb9adbe8e7da2a7a9f5eafb6962a Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Thu, 22 Feb 2018 01:44:46 -0500 Subject: [PATCH 05/12] Fix acceptance test to use identity doc and RSA sig Acceptance tests were failing due to #4014 so, as a workaround for now, passing in the identity document and the RSA signature rather than the PKCS7 document. --- builtin/credential/aws/backend_test.go | 50 +++++++++++++++++--------- builtin/credential/aws/path_login.go | 4 +-- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 438de50f472f..6c90ab526761 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -958,18 +958,29 @@ func TestBackend_PathBlacklistRoleTag(t *testing.T) { } } -// This is an acceptance test. -// Requires the following env vars: -// TEST_AWS_EC2_PKCS7 -// TEST_AWS_EC2_AMI_ID -// TEST_AWS_EC2_ACCOUNT_ID -// TEST_AWS_EC2_IAM_ROLE_ARN -// -// If the test is not being run on an EC2 instance that has access to -// credentials using EC2RoleProvider, on top of the above vars, following -// needs to be set: -// TEST_AWS_SECRET_KEY -// TEST_AWS_ACCESS_KEY +/* This is an acceptance test. + Requires the following env vars: + TEST_AWS_EC2_IDENTITY_DOCUMENT + TEST_AWS_EC2_IDENTITY_DOCUMENT_SIG + TEST_AWS_EC2_AMI_ID + TEST_AWS_EC2_ACCOUNT_ID + TEST_AWS_EC2_IAM_ROLE_ARN + + If this is being run on an EC2 instance, you can set the environment vars using this bash snippet: + + export TEST_AWS_EC2_IDENTITY_DOCUMENT=$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/document | base64 -w 0) + export TEST_AWS_EC2_IDENTITY_DOCUMENT_SIG=$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/signature | tr -d '\n') + export TEST_AWS_EC2_AMI_ID=$(curl -s http://169.254.169.254/latest/meta-data/ami-id) + export TEST_AWS_EC2_IAM_ROLE_ARN=$(aws iam get-role --role-name $(curl -q http://169.254.169.254/latest/meta-data/iam/security-credentials/ -S -s) --query Role.Arn --output text) + export TEST_AWS_EC2_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) + make testacc TEST=./builtin/credential/aws/ + + If the test is not being run on an EC2 instance that has access to + credentials using EC2RoleProvider, on top of the above vars, following + needs to be set: + TEST_AWS_SECRET_KEY + TEST_AWS_ACCESS_KEY +*/ func TestBackendAcc_LoginWithInstanceIdentityDocAndWhitelistIdentity(t *testing.T) { // This test case should be run only when certain env vars are set and // executed as an acceptance test. @@ -978,9 +989,13 @@ func TestBackendAcc_LoginWithInstanceIdentityDocAndWhitelistIdentity(t *testing. return } - pkcs7 := os.Getenv("TEST_AWS_EC2_PKCS7") - if pkcs7 == "" { - t.Fatalf("env var TEST_AWS_EC2_PKCS7 not set") + identityDoc := os.Getenv("TEST_AWS_EC2_IDENTITY_DOCUMENT") + if identityDoc == "" { + t.Fatalf("env var TEST_AWS_EC2_IDENTITY_DOCUMENT not set") + } + identityDocSig := os.Getenv("TEST_AWS_EC2_IDENTITY_DOCUMENT_SIG") + if identityDoc == "" { + t.Fatalf("env var TEST_AWS_EC2_IDENTITY_DOCUMENT_SIG not set") } amiID := os.Getenv("TEST_AWS_EC2_AMI_ID") @@ -1044,8 +1059,9 @@ func TestBackendAcc_LoginWithInstanceIdentityDocAndWhitelistIdentity(t *testing. } loginInput := map[string]interface{}{ - "pkcs7": pkcs7, - "nonce": "vault-client-nonce", + "identity": identityDoc, + "signature": identityDocSig, + "nonce": "vault-client-nonce", } // Perform the login operation with a AMI ID that is not matching diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index f59e4f119091..2e2d120921c5 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -330,8 +330,8 @@ func (b *backend) parseIdentityDocument(ctx context.Context, s logical.Storage, // Verify extracts the authenticated attributes in the PKCS#7 signature, and verifies // the authenticity of the content using 'dsa.PublicKey' embedded in the public certificate. - if pkcs7Data.Verify() != nil { - return nil, fmt.Errorf("failed to verify the signature") + if err = pkcs7Data.Verify(); err != nil { + return nil, fmt.Errorf("failed to verify the signature: %v", err) } // Check if the signature has content inside of it From 71bd5e132c17fca73096acc5be180a7264ebe1b4 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Thu, 22 Feb 2018 03:12:22 -0500 Subject: [PATCH 06/12] Add some tests for aws auth list binds --- builtin/credential/aws/backend_test.go | 31 +++++++++++++------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 6c90ab526761..45f2230807d2 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -1078,7 +1078,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, } @@ -1102,10 +1102,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{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) @@ -1117,9 +1117,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{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) @@ -1131,8 +1131,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{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) @@ -1438,7 +1438,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()}, // not filling in a fake ARN here as we're resolving unique IDs, so that would fail "policies": "root", "auth_type": iamAuthType, } @@ -1471,16 +1471,17 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { } fakeArn := "arn:aws:iam::123456789012:role/somePath/FakeRole" + fakeArn2 := "arn:aws:iam::123456789012:role/somePath/FakeRole2" 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) { + return fmt.Sprintf("FakeUniqueIdFor%s", arn), 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()) { @@ -1612,11 +1613,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 From 41c364fede4617521a3017ccb3e1b305746c3801 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Thu, 22 Feb 2018 21:05:07 -0500 Subject: [PATCH 07/12] Revert "Fix acceptance test to use identity doc and RSA sig" This reverts commit c3426917cbe8fb9adbe8e7da2a7a9f5eafb6962a. The underlying issue causing the need for the workaround has been fixed, and additional testing changes have been added in #4031 so this is no longer necessary. --- builtin/credential/aws/backend_test.go | 50 +++++++++----------------- builtin/credential/aws/path_login.go | 4 +-- 2 files changed, 19 insertions(+), 35 deletions(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 45f2230807d2..189e99445b12 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -958,29 +958,18 @@ func TestBackend_PathBlacklistRoleTag(t *testing.T) { } } -/* This is an acceptance test. - Requires the following env vars: - TEST_AWS_EC2_IDENTITY_DOCUMENT - TEST_AWS_EC2_IDENTITY_DOCUMENT_SIG - TEST_AWS_EC2_AMI_ID - TEST_AWS_EC2_ACCOUNT_ID - TEST_AWS_EC2_IAM_ROLE_ARN - - If this is being run on an EC2 instance, you can set the environment vars using this bash snippet: - - export TEST_AWS_EC2_IDENTITY_DOCUMENT=$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/document | base64 -w 0) - export TEST_AWS_EC2_IDENTITY_DOCUMENT_SIG=$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/signature | tr -d '\n') - export TEST_AWS_EC2_AMI_ID=$(curl -s http://169.254.169.254/latest/meta-data/ami-id) - export TEST_AWS_EC2_IAM_ROLE_ARN=$(aws iam get-role --role-name $(curl -q http://169.254.169.254/latest/meta-data/iam/security-credentials/ -S -s) --query Role.Arn --output text) - export TEST_AWS_EC2_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) - make testacc TEST=./builtin/credential/aws/ - - If the test is not being run on an EC2 instance that has access to - credentials using EC2RoleProvider, on top of the above vars, following - needs to be set: - TEST_AWS_SECRET_KEY - TEST_AWS_ACCESS_KEY -*/ +// This is an acceptance test. +// Requires the following env vars: +// TEST_AWS_EC2_PKCS7 +// TEST_AWS_EC2_AMI_ID +// TEST_AWS_EC2_ACCOUNT_ID +// TEST_AWS_EC2_IAM_ROLE_ARN +// +// If the test is not being run on an EC2 instance that has access to +// credentials using EC2RoleProvider, on top of the above vars, following +// needs to be set: +// TEST_AWS_SECRET_KEY +// TEST_AWS_ACCESS_KEY func TestBackendAcc_LoginWithInstanceIdentityDocAndWhitelistIdentity(t *testing.T) { // This test case should be run only when certain env vars are set and // executed as an acceptance test. @@ -989,13 +978,9 @@ func TestBackendAcc_LoginWithInstanceIdentityDocAndWhitelistIdentity(t *testing. return } - identityDoc := os.Getenv("TEST_AWS_EC2_IDENTITY_DOCUMENT") - if identityDoc == "" { - t.Fatalf("env var TEST_AWS_EC2_IDENTITY_DOCUMENT not set") - } - identityDocSig := os.Getenv("TEST_AWS_EC2_IDENTITY_DOCUMENT_SIG") - if identityDoc == "" { - t.Fatalf("env var TEST_AWS_EC2_IDENTITY_DOCUMENT_SIG not set") + pkcs7 := os.Getenv("TEST_AWS_EC2_PKCS7") + if pkcs7 == "" { + t.Fatalf("env var TEST_AWS_EC2_PKCS7 not set") } amiID := os.Getenv("TEST_AWS_EC2_AMI_ID") @@ -1059,9 +1044,8 @@ func TestBackendAcc_LoginWithInstanceIdentityDocAndWhitelistIdentity(t *testing. } loginInput := map[string]interface{}{ - "identity": identityDoc, - "signature": identityDocSig, - "nonce": "vault-client-nonce", + "pkcs7": pkcs7, + "nonce": "vault-client-nonce", } // Perform the login operation with a AMI ID that is not matching diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 2e2d120921c5..f59e4f119091 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -330,8 +330,8 @@ func (b *backend) parseIdentityDocument(ctx context.Context, s logical.Storage, // Verify extracts the authenticated attributes in the PKCS#7 signature, and verifies // the authenticity of the content using 'dsa.PublicKey' embedded in the public certificate. - if err = pkcs7Data.Verify(); err != nil { - return nil, fmt.Errorf("failed to verify the signature: %v", err) + if pkcs7Data.Verify() != nil { + return nil, fmt.Errorf("failed to verify the signature") } // Check if the signature has content inside of it From a9d039a8eeb5576ef9bf1dbcf9e186510941c40d Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Thu, 22 Feb 2018 23:16:03 -0500 Subject: [PATCH 08/12] Improve tests --- builtin/credential/aws/backend_test.go | 12 +++++++----- builtin/credential/aws/path_role_test.go | 23 +++++++++++++++++------ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 1b55c659e50f..2e8f0eb3e9d7 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -1110,7 +1110,7 @@ func TestBackendAcc_LoginWithInstanceIdentityDocAndWhitelistIdentity(t *testing. // Place the correct AMI ID in one of the values, but make the AccountID wrong roleReq.Operation = logical.UpdateOperation - data["bound_ami_id"] = []string{amiID, "wrong_ami_id_2"} + 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()) { @@ -1124,7 +1124,7 @@ func TestBackendAcc_LoginWithInstanceIdentityDocAndWhitelistIdentity(t *testing. } // Place the correct AccountID in one of the values, but make the wrong IAMRoleARN - data["bound_account_id"] = []string{accountID, "wrong-account-id-2"} + 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()) { @@ -1138,7 +1138,7 @@ func TestBackendAcc_LoginWithInstanceIdentityDocAndWhitelistIdentity(t *testing. } // place a correct IAM role ARN - data["bound_iam_role_arn"] = []string{iamARN, "wrong_iam_role_arn_2"} + 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": []string{entity.canonicalArn()}, // not filling in a fake ARN here as we're resolving unique IDs, so that would fail + "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, } @@ -1490,9 +1490,11 @@ 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 strings.HasPrefix(arn, fakeArn) { - return fmt.Sprintf("FakeUniqueIdFor%s", arn), nil + fakeArnResolverCount++ + return fmt.Sprintf("FakeUniqueIdFor%s%d", arn, fakeArnResolverCount), nil } return b.resolveArnToRealUniqueId(context.Background(), s, arn) } diff --git a/builtin/credential/aws/path_role_test.go b/builtin/credential/aws/path_role_test.go index 3c8baf961fb0..01827ff0c6eb 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, } @@ -240,10 +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) } - principal_ids := resp.Data["bound_iam_principal_id"].([]string) - if len(principal_ids) != 1 || principal_ids[0] != "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) { @@ -467,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") @@ -500,10 +507,14 @@ func TestBackend_pathRoleMixedTypes(t *testing.T) { if err != nil { t.Fatal(err) } - principal_ids := resp.Data["bound_iam_principal_id"].([]string) - if len(principal_ids) != 1 || principal_ids[0] != "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 { From 4047948c5d59885dc8382b3019b9e1514057dff2 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Thu, 22 Feb 2018 23:22:03 -0500 Subject: [PATCH 09/12] Return empty slices instead of null in aws auth roles --- builtin/credential/aws/path_role.go | 20 +++++++++++++++++++- builtin/credential/aws/path_role_test.go | 4 ++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/builtin/credential/aws/path_role.go b/builtin/credential/aws/path_role.go index 949959077c60..3ee667c61066 100644 --- a/builtin/credential/aws/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -826,7 +826,7 @@ type awsRoleEntry struct { } func (r *awsRoleEntry) ToResponseData() map[string]interface{} { - return map[string]interface{}{ + responseData := map[string]interface{}{ "auth_type": r.AuthType, "bound_ami_id": r.BoundAmiIDs, "bound_account_id": r.BoundAccountIDs, @@ -848,6 +848,24 @@ func (r *awsRoleEntry) ToResponseData() map[string]interface{} { "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 01827ff0c6eb..18f6a3e40aad 100644 --- a/builtin/credential/aws/path_role_test.go +++ b/builtin/credential/aws/path_role_test.go @@ -600,8 +600,8 @@ func TestAwsEc2_RoleCrud(t *testing.T) { "bound_ami_id": []string{"testamiid"}, "bound_account_id": []string{"testaccountid"}, "bound_region": []string{"testregion"}, - "bound_iam_principal_arn": []string(nil), - "bound_iam_principal_id": []string(nil), + "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"}, From 1f3b7f674ef745b707c0bc008290a588795b817c Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Thu, 22 Feb 2018 23:49:23 -0500 Subject: [PATCH 10/12] Update docs --- website/source/api/auth/aws/index.html.md | 61 ++++++++++++++--------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/website/source/api/auth/aws/index.html.md b/website/source/api/auth/aws/index.html.md index 4338a1716532..f912606e9f45 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 | | :------- | :--------------------------- | :--------------------- | @@ -548,52 +550,63 @@ inferencing configuration of that role. "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. + 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. + when inferring an EC2 instance. This is a comma-separated string or JSON + array. - `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 + 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. + well as the iam auth method only when inferring an EC2 instance. This is a + comma-separated string or JSON array. - `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 + 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. + This is a comma-separated string or JSON array. - `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 + 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. + as the iam auth method only when inferring an ec2 instance. This is a + comma-separated string or JSON array. - `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 + 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. + 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` `(string: "")` - If set, defines a constraint on the - authenticating EC2 instance that it must match the IAM role ARN specified by + 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. + only when inferring an EC2 instance. This is a comma-separated string or a + JSON array. - `bound_iam_instance_profile_arn` `(string: "")` - 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` `(string: "")` - 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. @@ -667,7 +680,7 @@ inferencing configuration of that role. ```json { - "bound_ami_id": "ami-fce36987", + "bound_ami_id": ["ami-fce36987"], "role_tag": "", "policies": [ "default", @@ -715,7 +728,7 @@ $ curl \ ```json { "data": { - "bound_ami_id": "ami-fce36987", + "bound_ami_id": ["ami-fce36987"], "role_tag": "", "policies": [ "default", From 0eb5757ce5beb38d1e1f7a473019916c58bd3174 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Fri, 23 Feb 2018 00:12:35 -0500 Subject: [PATCH 11/12] Add more docs improvements Looks like these changes were dropped from prior commit --- builtin/credential/aws/path_role.go | 2 +- website/source/docs/auth/aws.html.md | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/builtin/credential/aws/path_role.go b/builtin/credential/aws/path_role.go index 3ee667c61066..42ea891b3831 100644 --- a/builtin/credential/aws/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -101,7 +101,7 @@ inferred_entity_type is set, the region to assume the inferred entity exists in. "bound_vpc_id": { Type: framework.TypeCommaStringSlice, Description: ` -If set, defines a constraint on the EC2 instance to be associated with the VPC +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.`, }, 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 From 0a71e7bc59d84bcdb935ab63898249f5685064c4 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Fri, 23 Feb 2018 20:49:51 -0500 Subject: [PATCH 12/12] Update docs --- website/source/api/auth/aws/index.html.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/website/source/api/auth/aws/index.html.md b/website/source/api/auth/aws/index.html.md index f912606e9f45..23a63b1bae56 100644 --- a/website/source/api/auth/aws/index.html.md +++ b/website/source/api/auth/aws/index.html.md @@ -549,32 +549,32 @@ list in order to satisfy that constraint. "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 +- `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. This is a comma-separated string or JSON array. -- `bound_account_id` `(string: "")` - If set, defines a constraint on the EC2 +- `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. This is a comma-separated string or JSON array. -- `bound_region` `(string: "")` - If set, defines a constraint on the EC2 +- `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. This is a comma-separated string or JSON array. -- `bound_vpc_id` `(string: "")` - If set, defines a constraint on the EC2 +- `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. This is a comma-separated string or JSON array. -- `bound_subnet_id` `(string: "")` - If set, defines a constraint on the EC2 +- `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. This is a comma-separated string or a JSON array. -- `bound_iam_role_arn` `(string: "")` - If set, defines a constraint on the +- `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 @@ -582,7 +582,7 @@ list in order to satisfy that constraint. constraint is checked by the ec2 auth method as well as the iam auth method only when inferring an EC2 instance. This is a comma-separated string or a JSON array. -- `bound_iam_instance_profile_arn` `(string: "")` - If set, defines a constraint +- `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 one of the values specified by this parameter. The value is prefix-matched (as though it were a glob ending in `*`). This constraint is @@ -594,7 +594,7 @@ list in order to satisfy that constraint. 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 `auth_type` is iam. -- `bound_iam_principal_arn` `(string: "")` - Defines the list of IAM principals +- `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