diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go index 50e1e61e2f7f..4ffec58f07b4 100644 --- a/builtin/credential/aws/backend.go +++ b/builtin/credential/aws/backend.go @@ -1,9 +1,11 @@ package awsauth import ( + "fmt" "sync" "time" + "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/iam" "github.com/hashicorp/vault/logical" @@ -54,6 +56,15 @@ type backend struct { // When the credentials are modified or deleted, all the cached client objects // will be flushed. The empty STS role signifies the master account IAMClientsMap map[string]map[string]*iam.IAM + + // AWS Account ID of the "default" AWS credentials + // This cache avoids the need to call GetCallerIdentity repeatedly to learn it + // We can't store this because, in certain pathological cases, it could change + // out from under us, such as a standby and active Vault server in different AWS + // accounts using their IAM instance profile to get their credentials. + defaultAWSAccountID string + + resolveArnToUniqueIDFunc func(logical.Storage, string) (string, error) } func Backend(conf *logical.BackendConfig) (*backend, error) { @@ -65,6 +76,8 @@ func Backend(conf *logical.BackendConfig) (*backend, error) { IAMClientsMap: make(map[string]map[string]*iam.IAM), } + b.resolveArnToUniqueIDFunc = b.resolveArnToRealUniqueId + b.Backend = &framework.Backend{ PeriodicFunc: b.periodicFunc, AuthRenew: b.pathLoginRenew, @@ -171,9 +184,86 @@ func (b *backend) invalidate(key string) { defer b.configMutex.Unlock() b.flushCachedEC2Clients() b.flushCachedIAMClients() + b.defaultAWSAccountID = "" + } +} + +// Putting this here so we can inject a fake resolver into the backend for unit testing +// purposes +func (b *backend) resolveArnToRealUniqueId(s logical.Storage, arn string) (string, error) { + entity, err := parseIamArn(arn) + if err != nil { + return "", err + } + // This odd-looking code is here because IAM is an inherently global service. IAM and STS ARNs + // don't have regions in them, and there is only a single global endpoint for IAM; see + // http://docs.aws.amazon.com/general/latest/gr/rande.html#iam_region + // However, the ARNs do have a partition in them, because the GovCloud and China partitions DO + // have their own separate endpoints, and the partition is encoded in the ARN. If Amazon's Go SDK + // would allow us to pass a partition back to the IAM client, it would be much simpler. But it + // doesn't appear that's possible, so in order to properly support GovCloud and China, we do a + // circular dance of extracting the partition from the ARN, finding any arbitrary region in the + // partition, and passing that region back back to the SDK, so that the SDK can figure out the + // proper partition from the arbitrary region we passed in to look up the endpoint. + // Sigh + region := getAnyRegionForAwsPartition(entity.Partition) + if region == nil { + return "", fmt.Errorf("Unable to resolve partition %q to a region", entity.Partition) + } + iamClient, err := b.clientIAM(s, region.ID(), entity.AccountNumber) + if err != nil { + return "", err + } + + switch entity.Type { + case "user": + userInfo, err := iamClient.GetUser(&iam.GetUserInput{UserName: &entity.FriendlyName}) + if err != nil { + return "", err + } + if userInfo == nil { + return "", fmt.Errorf("got nil result from GetUser") + } + return *userInfo.User.UserId, nil + case "role": + roleInfo, err := iamClient.GetRole(&iam.GetRoleInput{RoleName: &entity.FriendlyName}) + if err != nil { + return "", err + } + if roleInfo == nil { + return "", fmt.Errorf("got nil result from GetRole") + } + return *roleInfo.Role.RoleId, nil + case "instance-profile": + profileInfo, err := iamClient.GetInstanceProfile(&iam.GetInstanceProfileInput{InstanceProfileName: &entity.FriendlyName}) + if err != nil { + return "", err + } + if profileInfo == nil { + return "", fmt.Errorf("got nil result from GetInstanceProfile") + } + return *profileInfo.InstanceProfile.InstanceProfileId, nil + default: + return "", fmt.Errorf("unrecognized error type %#v", entity.Type) } } +// Adapted from https://docs.aws.amazon.com/sdk-for-go/api/aws/endpoints/ +// the "Enumerating Regions and Endpoint Metadata" section +func getAnyRegionForAwsPartition(partitionId string) *endpoints.Region { + resolver := endpoints.DefaultResolver() + partitions := resolver.(endpoints.EnumPartitions).Partitions() + + for _, p := range partitions { + if p.ID() == partitionId { + for _, r := range p.Regions() { + return &r + } + } + } + return nil +} + const backendHelp = ` aws-ec2 auth backend takes in PKCS#7 signature of an AWS EC2 instance and a client created nonce to authenticates the EC2 instance with Vault. diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index a539fbac18be..3b1f3c8ff40b 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -9,11 +9,13 @@ import ( "os" "strings" "testing" + "time" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/sts" "github.com/hashicorp/vault/helper/policyutil" "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" logicaltest "github.com/hashicorp/vault/logical/testing" ) @@ -1346,7 +1348,7 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { if err != nil { t.Fatalf("Received error retrieving identity: %s", err) } - testIdentityArn, _, _, err := parseIamArn(*testIdentity.Arn) + entity, err := parseIamArn(*testIdentity.Arn) if err != nil { t.Fatal(err) } @@ -1385,7 +1387,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": testIdentityArn, + "bound_iam_principal_arn": entity.canonicalArn(), "policies": "root", "auth_type": iamAuthType, } @@ -1417,8 +1419,17 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { t.Fatalf("bad: failed to create role; resp:%#v\nerr:%v", resp, err) } + fakeArn := "arn:aws:iam::123456789012:role/FakeRole" + fakeArnResolver := func(s logical.Storage, arn string) (string, error) { + if arn == fakeArn { + return fmt.Sprintf("FakeUniqueIdFor%s", fakeArn), nil + } + return b.resolveArnToRealUniqueId(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"] = "arn:aws:iam::123456789012:role/FakeRole" + roleData["bound_iam_principal_arn"] = fakeArn roleRequest.Path = "role/" + testInvalidRoleName resp, err = b.HandleRequest(roleRequest) if err != nil || (resp != nil && resp.IsError()) { @@ -1491,7 +1502,7 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { t.Errorf("bad: expected failed login due to bad auth type: resp:%#v\nerr:%v", resp, err) } - // finally, the happy path tests :) + // finally, the happy path test :) loginData["role"] = testValidRoleName resp, err = b.HandleRequest(loginRequest) @@ -1501,4 +1512,52 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { if resp == nil || resp.Auth == nil || resp.IsError() { t.Errorf("bad: expected valid login: resp:%#v", resp) } + + renewReq := &logical.Request{ + Storage: storage, + Auth: &logical.Auth{}, + } + empty_login_fd := &framework.FieldData{ + Raw: map[string]interface{}{}, + Schema: pathLogin(b).Fields, + } + renewReq.Auth.InternalData = resp.Auth.InternalData + renewReq.Auth.Metadata = resp.Auth.Metadata + renewReq.Auth.LeaseOptions = resp.Auth.LeaseOptions + renewReq.Auth.Policies = resp.Auth.Policies + renewReq.Auth.IssueTime = time.Now() + // ensure we can renew + resp, err = b.pathLoginRenew(renewReq, empty_login_fd) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("got nil response from renew") + } + if resp.IsError() { + t.Fatalf("got error when renewing: %#v", *resp) + } + + // Now, fake out the unique ID resolver to ensure we fail login if the unique ID + // changes from under us + b.resolveArnToUniqueIDFunc = resolveArnToFakeUniqueId + // First, we need to update the role to force Vault to use our fake resolver to + // pick up the fake user ID + roleData["bound_iam_principal_arn"] = entity.canonicalArn() + roleRequest.Path = "role/" + testValidRoleName + resp, err = b.HandleRequest(roleRequest) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("bad: failed to recreate role: resp:%#v\nerr:%v", resp, err) + } + resp, err = b.HandleRequest(loginRequest) + if err != nil || resp == nil || !resp.IsError() { + t.Errorf("bad: expected failed login due to changed AWS role ID: resp: %#v\nerr:%v", resp, err) + } + + // and ensure a renew no longer works + resp, err = b.pathLoginRenew(renewReq, empty_login_fd) + if err == nil || (resp != nil && !resp.IsError()) { + t.Errorf("bad: expected failed renew due to changed AWS role ID: resp: %#v", resp, err) + } + } diff --git a/builtin/credential/aws/client.go b/builtin/credential/aws/client.go index 1647f4527b7f..3dc9327a3731 100644 --- a/builtin/credential/aws/client.go +++ b/builtin/credential/aws/client.go @@ -8,6 +8,7 @@ import ( "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/iam" + "github.com/aws/aws-sdk-go/service/sts" "github.com/hashicorp/go-cleanhttp" "github.com/hashicorp/vault/helper/awsutil" "github.com/hashicorp/vault/logical" @@ -70,7 +71,7 @@ func (b *backend) getRawClientConfig(s logical.Storage, region, clientType strin // It uses getRawClientConfig to obtain config for the runtime environemnt, and if // stsRole is a non-empty string, it will use AssumeRole to obtain a set of assumed // credentials. The credentials will expire after 15 minutes but will auto-refresh. -func (b *backend) getClientConfig(s logical.Storage, region, stsRole, clientType string) (*aws.Config, error) { +func (b *backend) getClientConfig(s logical.Storage, region, stsRole, accountID, clientType string) (*aws.Config, error) { config, err := b.getRawClientConfig(s, region, clientType) if err != nil { @@ -80,20 +81,39 @@ func (b *backend) getClientConfig(s logical.Storage, region, stsRole, clientType return nil, fmt.Errorf("could not compile valid credentials through the default provider chain") } + stsConfig, err := b.getRawClientConfig(s, region, "sts") + if stsConfig == nil { + return nil, fmt.Errorf("could not configure STS client") + } + if err != nil { + return nil, err + } if stsRole != "" { - assumeRoleConfig, err := b.getRawClientConfig(s, region, "sts") - if err != nil { - return nil, err - } - if assumeRoleConfig == nil { - return nil, fmt.Errorf("could not configure STS client") - } - assumedCredentials := stscreds.NewCredentials(session.New(assumeRoleConfig), stsRole) + assumedCredentials := stscreds.NewCredentials(session.New(stsConfig), stsRole) // Test that we actually have permissions to assume the role if _, err = assumedCredentials.Get(); err != nil { return nil, err } config.Credentials = assumedCredentials + } else { + if b.defaultAWSAccountID == "" { + client := sts.New(session.New(stsConfig)) + if client == nil { + return nil, fmt.Errorf("could not obtain sts client: %v", err) + } + inputParams := &sts.GetCallerIdentityInput{} + identity, err := client.GetCallerIdentity(inputParams) + if err != nil { + return nil, fmt.Errorf("unable to fetch current caller: %v", err) + } + if identity == nil { + return nil, fmt.Errorf("got nil result from GetCallerIdentity") + } + b.defaultAWSAccountID = *identity.Account + } + if b.defaultAWSAccountID != accountID { + return nil, fmt.Errorf("unable to fetch client for account ID %s -- default client is for account %s", accountID, b.defaultAWSAccountID) + } } return config, nil @@ -121,8 +141,25 @@ func (b *backend) flushCachedIAMClients() { } } +func (b *backend) stsRoleForAccount(s logical.Storage, accountID string) (string, error) { + // Check if an STS configuration exists for the AWS account + sts, err := b.lockedAwsStsEntry(s, accountID) + if err != nil { + return "", fmt.Errorf("error fetching STS config for account ID %q: %q\n", accountID, err) + } + // An empty STS role signifies the master account + if sts != nil { + return sts.StsRole, nil + } + return "", nil +} + // clientEC2 creates a client to interact with AWS EC2 API -func (b *backend) clientEC2(s logical.Storage, region string, stsRole string) (*ec2.EC2, error) { +func (b *backend) clientEC2(s logical.Storage, region, accountID string) (*ec2.EC2, error) { + stsRole, err := b.stsRoleForAccount(s, accountID) + if err != nil { + return nil, err + } b.configMutex.RLock() if b.EC2ClientsMap[region] != nil && b.EC2ClientsMap[region][stsRole] != nil { defer b.configMutex.RUnlock() @@ -142,8 +179,7 @@ func (b *backend) clientEC2(s logical.Storage, region string, stsRole string) (* // Create an AWS config object using a chain of providers var awsConfig *aws.Config - var err error - awsConfig, err = b.getClientConfig(s, region, stsRole, "ec2") + awsConfig, err = b.getClientConfig(s, region, stsRole, accountID, "ec2") if err != nil { return nil, err @@ -168,7 +204,11 @@ func (b *backend) clientEC2(s logical.Storage, region string, stsRole string) (* } // clientIAM creates a client to interact with AWS IAM API -func (b *backend) clientIAM(s logical.Storage, region string, stsRole string) (*iam.IAM, error) { +func (b *backend) clientIAM(s logical.Storage, region, accountID string) (*iam.IAM, error) { + stsRole, err := b.stsRoleForAccount(s, accountID) + if err != nil { + return nil, err + } b.configMutex.RLock() if b.IAMClientsMap[region] != nil && b.IAMClientsMap[region][stsRole] != nil { defer b.configMutex.RUnlock() @@ -188,8 +228,7 @@ func (b *backend) clientIAM(s logical.Storage, region string, stsRole string) (* // Create an AWS config object using a chain of providers var awsConfig *aws.Config - var err error - awsConfig, err = b.getClientConfig(s, region, stsRole, "iam") + awsConfig, err = b.getClientConfig(s, region, stsRole, accountID, "iam") if err != nil { return nil, err diff --git a/builtin/credential/aws/path_config_client.go b/builtin/credential/aws/path_config_client.go index 3787aed3b1a6..df1a6234d611 100644 --- a/builtin/credential/aws/path_config_client.go +++ b/builtin/credential/aws/path_config_client.go @@ -129,6 +129,9 @@ func (b *backend) pathConfigClientDelete( // Remove all the cached EC2 client objects in the backend. b.flushCachedIAMClients() + // unset the cached default AWS account ID + b.defaultAWSAccountID = "" + return nil, nil } @@ -234,6 +237,7 @@ func (b *backend) pathConfigClientCreateUpdate( if changedCreds { b.flushCachedEC2Clients() b.flushCachedIAMClients() + b.defaultAWSAccountID = "" } return nil, nil diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 2b7ae9bac9cd..2878167a8526 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -151,20 +151,8 @@ func (b *backend) instanceIamRoleARN(iamClient *iam.IAM, instanceProfileName str // validateInstance queries the status of the EC2 instance using AWS EC2 API // and checks if the instance is running and is healthy func (b *backend) validateInstance(s logical.Storage, instanceID, region, accountID string) (*ec2.Instance, error) { - - // Check if an STS configuration exists for the AWS account - sts, err := b.lockedAwsStsEntry(s, accountID) - if err != nil { - return nil, fmt.Errorf("error fetching STS config for account ID %q: %q\n", accountID, err) - } - // An empty STS role signifies the master account - stsRole := "" - if sts != nil { - stsRole = sts.StsRole - } - // Create an EC2 client to pull the instance information - ec2Client, err := b.clientEC2(s, region, stsRole) + ec2Client, err := b.clientEC2(s, region, accountID) if err != nil { return nil, err } @@ -472,32 +460,20 @@ func (b *backend) verifyInstanceMeetsRoleRequirements( // Extract out the instance profile name from the instance // profile ARN - iamInstanceProfileARNSlice := strings.SplitAfter(iamInstanceProfileARN, "/") - iamInstanceProfileName := iamInstanceProfileARNSlice[len(iamInstanceProfileARNSlice)-1] + iamInstanceProfileEntity, err := parseIamArn(iamInstanceProfileARN) - if iamInstanceProfileName == "" { - return nil, fmt.Errorf("failed to extract out IAM instance profile name from IAM instance profile ARN") - } - - // Check if an STS configuration exists for the AWS account - sts, err := b.lockedAwsStsEntry(s, identityDoc.AccountID) if err != nil { - return fmt.Errorf("error fetching STS config for account ID %q: %q\n", identityDoc.AccountID, err), nil - } - // An empty STS role signifies the master account - stsRole := "" - if sts != nil { - stsRole = sts.StsRole + return nil, fmt.Errorf("failed to parse IAM instance profile ARN %q; error: %v", iamInstanceProfileARN, err) } // Use instance profile ARN to fetch the associated role ARN - iamClient, err := b.clientIAM(s, identityDoc.Region, stsRole) + iamClient, err := b.clientIAM(s, identityDoc.Region, identityDoc.AccountID) if err != nil { return nil, fmt.Errorf("could not fetch IAM client: %v", err) } else if iamClient == nil { return nil, fmt.Errorf("received a nil iamClient") } - iamRoleARN, err := b.instanceIamRoleARN(iamClient, iamInstanceProfileName) + iamRoleARN, err := b.instanceIamRoleARN(iamClient, iamInstanceProfileEntity.FriendlyName) if err != nil { return nil, fmt.Errorf("IAM role ARN could not be fetched: %v", err) } @@ -959,6 +935,19 @@ func (b *backend) pathLoginRenewIam( if roleEntry.BoundIamPrincipalARN != "" && roleEntry.BoundIamPrincipalARN != canonicalArn { return nil, fmt.Errorf("role no longer bound to arn %q", canonicalArn) } + // Need to hanndle the case where an auth token was generated before we put client_user_id in the metadata + // Basic goal here is: + // 1. If no client_user_id metadata exists, then skip the check (it might be nice to fill it in later, but + // could be complicated) + // 2. If role is not bound to an ID, that means that checking the unique ID has been disabled, so skip the + // check + // 3. Otherwise, ensure that the stored client_user_id matches the bound IAM principal ID. If an IAM user + // or role is deleted and recreated, then existing clients will NOT be able to renew and they'll need + // to reauthenticate to Vault with updated IAM credentials + originalUserId, ok := req.Auth.Metadata["client_user_id"] + if ok && roleEntry.BoundIamPrincipalID != "" && roleEntry.BoundIamPrincipalID != req.Auth.Metadata["client_user_id"] { + return nil, fmt.Errorf("role no longer bound to ID %q", originalUserId) + } return framework.LeaseExtend(roleEntry.TTL, roleEntry.MaxTTL, b.System())(req, data) @@ -1134,18 +1123,21 @@ func (b *backend) pathLoginUpdateIam( } } - clientArn, accountID, err := submitCallerIdentityRequest(method, endpoint, parsedUrl, body, headers) + callerID, err := submitCallerIdentityRequest(method, endpoint, parsedUrl, body, headers) if err != nil { return logical.ErrorResponse(fmt.Sprintf("error making upstream request: %v", err)), nil } - canonicalArn, principalName, sessionName, err := parseIamArn(clientArn) + // This could either be a "userID:SessionID" (in the case of an assumed role) or just a "userID" + // (in the case of an IAM user). + callerUniqueId := strings.Split(callerID.UserId, ":")[0] + entity, err := parseIamArn(callerID.Arn) if err != nil { return logical.ErrorResponse(fmt.Sprintf("Error parsing arn: %v", err)), nil } roleName := data.Get("role").(string) if roleName == "" { - roleName = principalName + roleName = entity.FriendlyName } roleEntry, err := b.lockedAWSRole(req.Storage, roleName) @@ -1162,8 +1154,15 @@ func (b *backend) pathLoginUpdateIam( // The role creation should ensure that either we're inferring this is an EC2 instance // or that we're binding an ARN - if roleEntry.BoundIamPrincipalARN != "" && roleEntry.BoundIamPrincipalARN != canonicalArn { - return logical.ErrorResponse(fmt.Sprintf("IAM Principal %q does not belong to the role %q", clientArn, roleName)), nil + // 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 != "" && roleEntry.BoundIamPrincipalARN != entity.canonicalArn() { + return logical.ErrorResponse(fmt.Sprintf("IAM Principal %q does not belong to the role %q", callerID.Arn, roleName)), nil } policies := roleEntry.Policies @@ -1171,9 +1170,9 @@ func (b *backend) pathLoginUpdateIam( inferredEntityType := "" inferredEntityId := "" if roleEntry.InferredEntityType == ec2EntityType { - instance, err := b.validateInstance(req.Storage, sessionName, roleEntry.InferredAWSRegion, accountID) + instance, err := b.validateInstance(req.Storage, entity.SessionInfo, roleEntry.InferredAWSRegion, callerID.Account) if err != nil { - return logical.ErrorResponse(fmt.Sprintf("failed to verify %s as a valid EC2 instance in region %s", sessionName, roleEntry.InferredAWSRegion)), nil + return logical.ErrorResponse(fmt.Sprintf("failed to verify %s as a valid EC2 instance in region %s", entity.SessionInfo, roleEntry.InferredAWSRegion)), nil } // build a fake identity doc to pass on metadata about the instance to verifyInstanceMeetsRoleRequirements @@ -1181,7 +1180,7 @@ func (b *backend) pathLoginUpdateIam( Tags: nil, // Don't really need the tags, so not doing the work of converting them from Instance.Tags to identityDocument.Tags InstanceID: *instance.InstanceId, AmiID: *instance.ImageId, - AccountID: accountID, + AccountID: callerID.Account, Region: roleEntry.InferredAWSRegion, PendingTime: instance.LaunchTime.Format(time.RFC3339), } @@ -1191,11 +1190,11 @@ func (b *backend) pathLoginUpdateIam( return nil, err } if validationError != nil { - return logical.ErrorResponse(fmt.Sprintf("Error validating instance: %s", validationError)), nil + return logical.ErrorResponse(fmt.Sprintf("error validating instance: %s", validationError)), nil } inferredEntityType = ec2EntityType - inferredEntityId = sessionName + inferredEntityId = entity.SessionInfo } resp := &logical.Response{ @@ -1203,18 +1202,19 @@ func (b *backend) pathLoginUpdateIam( Period: roleEntry.Period, Policies: policies, Metadata: map[string]string{ - "client_arn": clientArn, - "canonical_arn": canonicalArn, + "client_arn": callerID.Arn, + "canonical_arn": entity.canonicalArn(), + "client_user_id": callerUniqueId, "auth_type": iamAuthType, "inferred_entity_type": inferredEntityType, "inferred_entity_id": inferredEntityId, "inferred_aws_region": roleEntry.InferredAWSRegion, - "account_id": accountID, + "account_id": entity.AccountNumber, }, InternalData: map[string]interface{}{ "role_name": roleName, }, - DisplayName: principalName, + DisplayName: entity.FriendlyName, LeaseOptions: logical.LeaseOptions{ Renewable: true, TTL: roleEntry.TTL, @@ -1267,29 +1267,44 @@ func hasValuesForIamAuth(data *framework.FieldData) (bool, bool) { (hasRequestMethod || hasRequestUrl || hasRequestBody || hasRequestHeaders) } -func parseIamArn(iamArn string) (string, string, string, error) { +func parseIamArn(iamArn string) (*iamEntity, error) { // iamArn should look like one of the following: - // 1. arn:aws:iam:::user/ + // 1. arn:aws:iam:::/ // 2. arn:aws:sts:::assumed-role// // if we get something like 2, then we want to transform that back to what // most people would expect, which is arn:aws:iam:::role/ + var entity iamEntity fullParts := strings.Split(iamArn, ":") - principalFullName := fullParts[5] - // principalFullName would now be something like user/ or assumed-role// - parts := strings.Split(principalFullName, "/") - principalName := parts[1] - // now, principalName should either be or - transformedArn := iamArn - sessionName := "" - if parts[0] == "assumed-role" { - transformedArn = fmt.Sprintf("arn:aws:iam::%s:role/%s", fullParts[4], principalName) - // fullParts[4] is the - sessionName = parts[2] - // sessionName is - } else if parts[0] != "user" { - return "", "", "", fmt.Errorf("unrecognized principal type: %q", parts[0]) - } - return transformedArn, principalName, sessionName, nil + if fullParts[0] != "arn" { + return nil, fmt.Errorf("unrecognized arn: does not begin with arn:") + } + // normally aws, but could be aws-cn or aws-us-gov + entity.Partition = fullParts[1] + if fullParts[2] != "iam" && fullParts[2] != "sts" { + return nil, fmt.Errorf("unrecognized service: %v, not one of iam or sts", fullParts[2]) + } + // fullParts[3] is the region, which doesn't matter for AWS IAM entities + entity.AccountNumber = fullParts[4] + // fullParts[5] would now be something like user/ or assumed-role// + parts := strings.Split(fullParts[5], "/") + entity.Type = parts[0] + entity.Path = strings.Join(parts[1:len(parts)-1], "/") + entity.FriendlyName = parts[len(parts)-1] + // now, entity.FriendlyName should either be or + switch entity.Type { + case "assumed-role": + // Assumed roles don't have paths and have a slightly different format + // parts[2] is + entity.Path = "" + entity.FriendlyName = parts[1] + entity.SessionInfo = parts[2] + case "user": + case "role": + case "instance-profile": + default: + return &iamEntity{}, fmt.Errorf("unrecognized principal type: %q", entity.Type) + } + return &entity, nil } func validateVaultHeaderValue(headers http.Header, requestUrl *url.URL, requiredHeaderValue string) error { @@ -1392,7 +1407,7 @@ func parseGetCallerIdentityResponse(response string) (GetCallerIdentityResponse, return result, err } -func submitCallerIdentityRequest(method, endpoint string, parsedUrl *url.URL, body string, headers http.Header) (string, string, error) { +func submitCallerIdentityRequest(method, endpoint string, parsedUrl *url.URL, body string, headers http.Header) (*GetCallerIdentityResult, error) { // NOTE: We need to ensure we're calling STS, instead of acting as an unintended network proxy // The protection against this is that this method will only call the endpoint specified in the // client config (defaulting to sts.amazonaws.com), so it would require a Vault admin to override @@ -1401,7 +1416,7 @@ func submitCallerIdentityRequest(method, endpoint string, parsedUrl *url.URL, bo client := cleanhttp.DefaultClient() response, err := client.Do(request) if err != nil { - return "", "", fmt.Errorf("error making request: %v", err) + return nil, fmt.Errorf("error making request: %v", err) } if response != nil { defer response.Body.Close() @@ -1409,17 +1424,13 @@ func submitCallerIdentityRequest(method, endpoint string, parsedUrl *url.URL, bo // we check for status code afterwards to also print out response body responseBody, err := ioutil.ReadAll(response.Body) if response.StatusCode != 200 { - return "", "", fmt.Errorf("received error code %s from STS: %s", response.StatusCode, string(responseBody)) + return nil, fmt.Errorf("received error code %s from STS: %s", response.StatusCode, string(responseBody)) } callerIdentityResponse, err := parseGetCallerIdentityResponse(string(responseBody)) if err != nil { - return "", "", fmt.Errorf("error parsing STS response") - } - clientArn := callerIdentityResponse.GetCallerIdentityResult[0].Arn - if clientArn == "" { - return "", "", fmt.Errorf("no ARN validated") + return nil, fmt.Errorf("error parsing STS response") } - return clientArn, callerIdentityResponse.GetCallerIdentityResult[0].Account, nil + return &callerIdentityResponse.GetCallerIdentityResult[0], nil } type GetCallerIdentityResponse struct { @@ -1457,6 +1468,29 @@ type roleTagLoginResponse struct { DisallowReauthentication bool `json:"disallow_reauthentication" structs:"disallow_reauthentication" mapstructure:"disallow_reauthentication"` } +type iamEntity struct { + Partition string + AccountNumber string + Type string + Path string + FriendlyName string + SessionInfo string +} + +func (e *iamEntity) canonicalArn() string { + entityType := e.Type + // canonicalize "assumed-role" into "role" + if entityType == "assumed-role" { + entityType = "role" + } + // Annoyingly, the assumed-role entity type doesn't have the Path of the role which was assumed + // So, we "canonicalize" it by just completely dropping the path. The other option would be to + // make an AWS API call to look up the role by FriendlyName, which introduces more complexity to + // code and test, and it also breaks backwards compatibility in an area where we would really want + // it + return fmt.Sprintf("arn:%s:iam::%s:%s/%s", e.Partition, e.AccountNumber, entityType, e.FriendlyName) +} + const iamServerIdHeader = "X-Vault-AWS-IAM-Server-ID" const pathLoginSyn = ` diff --git a/builtin/credential/aws/path_login_test.go b/builtin/credential/aws/path_login_test.go index e96bed835034..8656ce90c9c7 100644 --- a/builtin/credential/aws/path_login_test.go +++ b/builtin/credential/aws/path_login_test.go @@ -48,37 +48,36 @@ func TestBackend_pathLogin_getCallerIdentityResponse(t *testing.T) { } func TestBackend_pathLogin_parseIamArn(t *testing.T) { - userArn := "arn:aws:iam::123456789012:user/MyUserName" - assumedRoleArn := "arn:aws:sts::123456789012:assumed-role/RoleName/RoleSessionName" - baseRoleArn := "arn:aws:iam::123456789012:role/RoleName" - - xformedUser, principalFriendlyName, sessionName, err := parseIamArn(userArn) - if err != nil { - t.Fatal(err) - } - if xformedUser != userArn { - t.Fatalf("expected to transform ARN %#v into %#v but got %#v instead", userArn, userArn, xformedUser) - } - if principalFriendlyName != "MyUserName" { - t.Fatalf("expected to extract MyUserName from ARN %#v but got %#v instead", userArn, principalFriendlyName) - } - if sessionName != "" { - t.Fatalf("expected to extract no session name from ARN %#v but got %#v instead", userArn, sessionName) - } - - xformedRole, principalFriendlyName, sessionName, err := parseIamArn(assumedRoleArn) - if err != nil { - t.Fatal(err) - } - if xformedRole != baseRoleArn { - t.Fatalf("expected to transform ARN %#v into %#v but got %#v instead", assumedRoleArn, baseRoleArn, xformedRole) - } - if principalFriendlyName != "RoleName" { - t.Fatalf("expected to extract principal name of RoleName from ARN %#v but got %#v instead", assumedRoleArn, sessionName) - } - if sessionName != "RoleSessionName" { - t.Fatalf("expected to extract role session name of RoleSessionName from ARN %#v but got %#v instead", assumedRoleArn, sessionName) - } + testParser := func(inputArn, expectedCanonicalArn string, expectedEntity iamEntity) { + entity, err := parseIamArn(inputArn) + if err != nil { + t.Fatal(err) + } + if expectedCanonicalArn != "" && entity.canonicalArn() != expectedCanonicalArn { + t.Fatalf("expected to canonicalize ARN %q into %q but got %q instead", inputArn, expectedCanonicalArn, entity.canonicalArn()) + } + if *entity != expectedEntity { + t.Fatalf("expected to get iamEntity %#v from input ARN %q but instead got %#v", expectedEntity, inputArn, *entity) + } + } + + testParser("arn:aws:iam::123456789012:user/UserPath/MyUserName", + "arn:aws:iam::123456789012:user/MyUserName", + iamEntity{Partition: "aws", AccountNumber: "123456789012", Type: "user", Path: "UserPath", FriendlyName: "MyUserName"}, + ) + canonicalRoleArn := "arn:aws:iam::123456789012:role/RoleName" + testParser("arn:aws:sts::123456789012:assumed-role/RoleName/RoleSessionName", + canonicalRoleArn, + iamEntity{Partition: "aws", AccountNumber: "123456789012", Type: "assumed-role", FriendlyName: "RoleName", SessionInfo: "RoleSessionName"}, + ) + testParser("arn:aws:iam::123456789012:role/RolePath/RoleName", + canonicalRoleArn, + iamEntity{Partition: "aws", AccountNumber: "123456789012", Type: "role", Path: "RolePath", FriendlyName: "RoleName"}, + ) + testParser("arn:aws:iam::123456789012:instance-profile/profilePath/InstanceProfileName", + "", + iamEntity{Partition: "aws", AccountNumber: "123456789012", Type: "instance-profile", Path: "profilePath", FriendlyName: "InstanceProfileName"}, + ) } func TestBackend_validateVaultHeaderValue(t *testing.T) { diff --git a/builtin/credential/aws/path_role.go b/builtin/credential/aws/path_role.go index 63d0300d872d..79f7a518e931 100644 --- a/builtin/credential/aws/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -63,6 +63,14 @@ with an IAM instance profile ARN which has a prefix that matches the value specified by this parameter. The value is prefix-matched (as though it were a glob ending in '*'). This is only checked when auth_type is ec2.`, + }, + "resolve_aws_unique_ids": { + Type: framework.TypeBool, + Default: true, + Description: `If set, resolve all AWS IAM ARNs into AWS's internal unique IDs. +When an IAM entity (e.g., user, role, or instance profile) is deleted, then all references +to it within the role will be invalidated, which prevents a new IAM entity from being created +with the same name and matching the role's IAM binds. Once set, this cannot be unset.`, }, "inferred_entity_type": { Type: framework.TypeString, @@ -210,7 +218,7 @@ func (b *backend) lockedAWSRole(s logical.Storage, roleName string) (*awsRoleEnt if roleEntry == nil { return nil, nil } - needUpgrade, err := upgradeRoleEntry(roleEntry) + needUpgrade, err := b.upgradeRoleEntry(s, roleEntry) if err != nil { return nil, fmt.Errorf("error upgrading roleEntry: %v", err) } @@ -228,7 +236,7 @@ func (b *backend) lockedAWSRole(s logical.Storage, roleName string) (*awsRoleEnt return nil, nil } // now re-check to see if we need to upgrade - if needUpgrade, err = upgradeRoleEntry(roleEntry); err != nil { + if needUpgrade, err = b.upgradeRoleEntry(s, roleEntry); err != nil { return nil, fmt.Errorf("error upgrading roleEntry: %v", err) } if needUpgrade { @@ -284,7 +292,7 @@ func (b *backend) nonLockedSetAWSRole(s logical.Storage, roleName string, // If needed, updates the role entry and returns a bool indicating if it was updated // (and thus needs to be persisted) -func upgradeRoleEntry(roleEntry *awsRoleEntry) (bool, error) { +func (b *backend) upgradeRoleEntry(s logical.Storage, roleEntry *awsRoleEntry) (bool, error) { if roleEntry == nil { return false, fmt.Errorf("received nil roleEntry") } @@ -307,6 +315,18 @@ func upgradeRoleEntry(roleEntry *awsRoleEntry) (bool, error) { upgraded = true } + if roleEntry.AuthType == iamAuthType && + roleEntry.ResolveAWSUniqueIDs && + roleEntry.BoundIamPrincipalARN != "" && + roleEntry.BoundIamPrincipalID == "" { + principalId, err := b.resolveArnToUniqueIDFunc(s, roleEntry.BoundIamPrincipalARN) + if err != nil { + return false, err + } + roleEntry.BoundIamPrincipalID = principalId + upgraded = true + } + return upgraded, nil } @@ -411,7 +431,7 @@ func (b *backend) pathRoleCreateUpdate( if roleEntry == nil { roleEntry = &awsRoleEntry{} } else { - needUpdate, err := upgradeRoleEntry(roleEntry) + needUpdate, err := b.upgradeRoleEntry(req.Storage, roleEntry) if err != nil { return logical.ErrorResponse(fmt.Sprintf("failed to update roleEntry: %v", err)), nil } @@ -445,6 +465,19 @@ func (b *backend) pathRoleCreateUpdate( roleEntry.BoundSubnetID = boundSubnetIDRaw.(string) } + if resolveAWSUniqueIDsRaw, ok := data.GetOk("resolve_aws_unique_ids"); ok { + switch { + case req.Operation == logical.CreateOperation: + roleEntry.ResolveAWSUniqueIDs = resolveAWSUniqueIDsRaw.(bool) + case roleEntry.ResolveAWSUniqueIDs && !resolveAWSUniqueIDsRaw.(bool): + return logical.ErrorResponse("changing resolve_aws_unique_ids from true to false is not allowed"), nil + default: + roleEntry.ResolveAWSUniqueIDs = resolveAWSUniqueIDsRaw.(bool) + } + } else if req.Operation == logical.CreateOperation { + roleEntry.ResolveAWSUniqueIDs = data.Get("resolve_aws_unique_ids").(bool) + } + if boundIamRoleARNRaw, ok := data.GetOk("bound_iam_role_arn"); ok { roleEntry.BoundIamRoleARN = boundIamRoleARNRaw.(string) } @@ -454,7 +487,26 @@ func (b *backend) pathRoleCreateUpdate( } if boundIamPrincipalARNRaw, ok := data.GetOk("bound_iam_principal_arn"); ok { - roleEntry.BoundIamPrincipalARN = boundIamPrincipalARNRaw.(string) + 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 { + principalID, err := b.resolveArnToUniqueIDFunc(req.Storage, principalARN) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("failed updating the unique ID of ARN %#v: %#v", principalARN, err)), nil + } + roleEntry.BoundIamPrincipalID = principalID + } + } else if roleEntry.ResolveAWSUniqueIDs && roleEntry.BoundIamPrincipalARN != "" { + // we're turning on resolution on this role, so ensure we update it + principalID, err := b.resolveArnToUniqueIDFunc(req.Storage, roleEntry.BoundIamPrincipalARN) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("unable to resolve ARN %#v to internal ID: %#v", roleEntry.BoundIamPrincipalARN, err)), nil + } + roleEntry.BoundIamPrincipalID = principalID } if inferRoleTypeRaw, ok := data.GetOk("inferred_entity_type"); ok { @@ -682,6 +734,7 @@ type awsRoleEntry struct { 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"` @@ -689,6 +742,7 @@ type awsRoleEntry struct { 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"` diff --git a/builtin/credential/aws/path_role_test.go b/builtin/credential/aws/path_role_test.go index 52ff435744af..82d4d92c094b 100644 --- a/builtin/credential/aws/path_role_test.go +++ b/builtin/credential/aws/path_role_test.go @@ -135,7 +135,81 @@ func TestBackend_pathRoleEc2(t *testing.T) { if resp != nil { t.Fatalf("bad: response: expected:nil actual:%#v\n", resp) } +} + +func Test_enableIamIDResolution(t *testing.T) { + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + + b, err := Backend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Setup(config) + if err != nil { + t.Fatal(err) + } + roleName := "upgradable_role" + + b.resolveArnToUniqueIDFunc = resolveArnToFakeUniqueId + + data := map[string]interface{}{ + "auth_type": iamAuthType, + "policies": "p,q", + "bound_iam_principal_arn": "arn:aws:iam::123456789012:role/MyRole", + "resolve_aws_unique_ids": false, + } + + submitRequest := func(roleName string, op logical.Operation) (*logical.Response, error) { + return b.HandleRequest(&logical.Request{ + Operation: op, + Path: "role/" + roleName, + Data: data, + Storage: storage, + }) + } + + resp, err := submitRequest(roleName, logical.CreateOperation) + if err != nil { + t.Fatal(err) + } + if resp != nil && resp.IsError() { + t.Fatalf("failed to create role: %#v", resp) + } + resp, err = submitRequest(roleName, logical.ReadOperation) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.IsError() { + t.Fatalf("failed to read role: resp:%#v,\nerr:%#v", resp, err) + } + if resp.Data["bound_iam_principal_id"] != "" { + t.Fatalf("expected to get no unique ID in role, but got %q", resp.Data["bound_iam_principal_id"]) + } + + data = map[string]interface{}{ + "resolve_aws_unique_ids": true, + } + resp, err = submitRequest(roleName, logical.UpdateOperation) + if err != nil { + t.Fatal(err) + } + if resp != nil && resp.IsError() { + t.Fatalf("unable to upgrade role to resolve internal IDs: resp:%#v", resp) + } + + resp, err = submitRequest(roleName, logical.ReadOperation) + if err != nil { + t.Fatal(err) + } + 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" { + t.Fatalf("bad: expected upgrade of role resolve principal ID to %q, but got %q instead", "FakeUniqueId1", resp.Data["bound_iam_principal_id"]) + } } func TestBackend_pathIam(t *testing.T) { @@ -174,6 +248,7 @@ func TestBackend_pathIam(t *testing.T) { "policies": "p,q,r,s", "max_ttl": "2h", "bound_iam_principal_arn": "n:aws:iam::123456789012:user/MyUserName", + "resolve_aws_unique_ids": false, } resp, err = b.HandleRequest(&logical.Request{ Operation: logical.CreateOperation, @@ -369,6 +444,7 @@ func TestBackend_pathRoleMixedTypes(t *testing.T) { data["inferred_entity_type"] = ec2EntityType data["inferred_aws_region"] = "us-east-1" + data["resolve_aws_unique_ids"] = false resp, err = submitRequest("multipleTypesInferred", logical.CreateOperation) if err != nil { t.Fatal(err) @@ -376,6 +452,29 @@ func TestBackend_pathRoleMixedTypes(t *testing.T) { if resp.IsError() { t.Fatalf("didn't allow creation of roles with only inferred bindings") } + + b.resolveArnToUniqueIDFunc = resolveArnToFakeUniqueId + data["resolve_aws_unique_ids"] = true + resp, err = submitRequest("withInternalIdResolution", logical.CreateOperation) + if err != nil { + t.Fatal(err) + } + if resp.IsError() { + t.Fatalf("didn't allow creation of role resolving unique IDs") + } + resp, err = submitRequest("withInternalIdResolution", logical.ReadOperation) + if resp.Data["bound_iam_principal_id"] != "FakeUniqueId1" { + t.Fatalf("expected fake unique ID of FakeUniqueId1, got %q", resp.Data["bound_iam_principal_id"]) + } + data["resolve_aws_unique_ids"] = false + resp, err = submitRequest("withInternalIdResolution", logical.UpdateOperation) + if err != nil { + t.Fatal(err) + } + if !resp.IsError() { + t.Fatalf("allowed changing resolve_aws_unique_ids from true to false") + } + } func TestAwsEc2_RoleCrud(t *testing.T) { @@ -417,11 +516,12 @@ func TestAwsEc2_RoleCrud(t *testing.T) { "bound_ami_id": "testamiid", "bound_account_id": "testaccountid", "bound_region": "testregion", - "bound_iam_role_arn": "testiamrolearn", - "bound_iam_instance_profile_arn": "testiaminstanceprofilearn", + "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", "role_tag": "testtag", + "resolve_aws_unique_ids": false, "allow_instance_migration": true, "ttl": "10m", "max_ttl": "20m", @@ -451,12 +551,14 @@ func TestAwsEc2_RoleCrud(t *testing.T) { "bound_account_id": "testaccountid", "bound_region": "testregion", "bound_iam_principal_arn": "", - "bound_iam_role_arn": "testiamrolearn", - "bound_iam_instance_profile_arn": "testiaminstanceprofilearn", + "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", "inferred_entity_type": "", "inferred_aws_region": "", + "resolve_aws_unique_ids": false, "role_tag": "testtag", "allow_instance_migration": true, "ttl": time.Duration(600), @@ -519,7 +621,8 @@ func TestAwsEc2_RoleDurationSeconds(t *testing.T) { roleData := map[string]interface{}{ "auth_type": "ec2", - "bound_iam_instance_profile_arn": "testarn", + "bound_iam_instance_profile_arn": "arn:aws:iam::123456789012:instance-profile/test-profile-name", + "resolve_aws_unique_ids": false, "ttl": "10s", "max_ttl": "20s", "period": "30s", @@ -554,3 +657,7 @@ func TestAwsEc2_RoleDurationSeconds(t *testing.T) { t.Fatalf("bad: period; expected: 30, actual: %d", resp.Data["period"]) } } + +func resolveArnToFakeUniqueId(s logical.Storage, arn string) (string, error) { + return "FakeUniqueId1", nil +} diff --git a/website/source/docs/auth/aws.html.md b/website/source/docs/auth/aws.html.md index b1a25e3e91c9..f281cab21ba6 100644 --- a/website/source/docs/auth/aws.html.md +++ b/website/source/docs/auth/aws.html.md @@ -1427,6 +1427,48 @@ The response will be in JSON. For example: activated. This only applies to the iam auth method. +
    +
  • + resolve_aws_unique_ids + optional + When set, resolves the `bound_iam_principal_arn` to the [AWS Unique + ID](http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-unique-ids). + This requires Vault to be able to call `iam:GetUser` or `iam:GetRole` on + the `bound_iam_principal_arn` that is being bound. Resolving to + internal AWS IDs more closely mimics the behavior of AWS services in + that if an IAM user or role is deleted and a new one is recreated with + the same name, those new users or roles won't get access to roles in + Vault that were permissioned to the prior principals of the same name. + The default value for new roles is true, while the default value for + roles that existed prior to this option existing is false (you can + check the value for a given role using the GET method on the role). Any + authentication tokens created prior to this being supported won't + verify the unique ID upon token renewal. When this is changed from + false to true on an existing role, Vault will attempt to resolve the + role's bound IAM ARN to the unique ID and, if unable to do so, will + fail to enable this option. Changing this from `true` to `false` is + not supported; if absolutely necessary, you would need to delete the + role and recreate it explicitly setting it to `false`. However; the + instances in which you would want to do this should be rare. If the + role creation (or upgrading to use this) succeed, then Vault has + already been able to resolve internal IDs, and it doesn't need any + further IAM permissions to authenticate users. If a role has been + deleted and recreated, and Vault has cached the old unique ID, you + should just call this endpoint specifying the same + `bound_iam_principal_arn` and, as long as 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. +
  • +
+
  • ttl