Resolve AWS IAM unique IDs (#2814)

This commit is contained in:
Joel Thompson
2017-06-07 10:27:11 -04:00
committed by Jeff Mitchell
parent 5be733dad5
commit d858511fdf
9 changed files with 555 additions and 127 deletions

View File

@@ -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.

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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::<account_id>:user/<UserName>
// 1. arn:aws:iam::<account_id>:<entity_type>/<UserName>
// 2. arn:aws:sts::<account_id>:assumed-role/<RoleName>/<RoleSessionName>
// if we get something like 2, then we want to transform that back to what
// most people would expect, which is arn:aws:iam::<account_id>:role/<RoleName>
var entity iamEntity
fullParts := strings.Split(iamArn, ":")
principalFullName := fullParts[5]
// principalFullName would now be something like user/<UserName> or assumed-role/<RoleName>/<RoleSessionName>
parts := strings.Split(principalFullName, "/")
principalName := parts[1]
// now, principalName should either be <UserName> or <RoleName>
transformedArn := iamArn
sessionName := ""
if parts[0] == "assumed-role" {
transformedArn = fmt.Sprintf("arn:aws:iam::%s:role/%s", fullParts[4], principalName)
// fullParts[4] is the <account_id>
sessionName = parts[2]
// sessionName is <RoleSessionName>
} else if parts[0] != "user" {
return "", "", "", fmt.Errorf("unrecognized principal type: %q", parts[0])
if fullParts[0] != "arn" {
return nil, fmt.Errorf("unrecognized arn: does not begin with arn:")
}
return transformedArn, principalName, sessionName, nil
// 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/<UserName> or assumed-role/<RoleName>/<RoleSessionName>
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 <UserName> or <RoleName>
switch entity.Type {
case "assumed-role":
// Assumed roles don't have paths and have a slightly different format
// parts[2] is <RoleSessionName>
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")
return nil, fmt.Errorf("error parsing STS response")
}
clientArn := callerIdentityResponse.GetCallerIdentityResult[0].Arn
if clientArn == "" {
return "", "", fmt.Errorf("no ARN validated")
}
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 = `

View File

@@ -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)
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)
}
}
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("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) {

View File

@@ -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"`

View File

@@ -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
}

View File

@@ -1427,6 +1427,48 @@ The response will be in JSON. For example:
activated. This only applies to the iam auth method.
</li>
</ul>
<ul>
<li>
<span class="param">resolve_aws_unique_ids</span>
<span class="param-flags">optional</span>
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.
</li>
</ul>
<ul>
<li>
<span class="param">ttl</span>