mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 17:52:32 +00:00
Resolve AWS IAM unique IDs (#2814)
This commit is contained in:
committed by
Jeff Mitchell
parent
5be733dad5
commit
d858511fdf
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = `
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user