mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-02 19:47:54 +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
|
package awsauth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go/aws/endpoints"
|
||||||
"github.com/aws/aws-sdk-go/service/ec2"
|
"github.com/aws/aws-sdk-go/service/ec2"
|
||||||
"github.com/aws/aws-sdk-go/service/iam"
|
"github.com/aws/aws-sdk-go/service/iam"
|
||||||
"github.com/hashicorp/vault/logical"
|
"github.com/hashicorp/vault/logical"
|
||||||
@@ -54,6 +56,15 @@ type backend struct {
|
|||||||
// When the credentials are modified or deleted, all the cached client objects
|
// When the credentials are modified or deleted, all the cached client objects
|
||||||
// will be flushed. The empty STS role signifies the master account
|
// will be flushed. The empty STS role signifies the master account
|
||||||
IAMClientsMap map[string]map[string]*iam.IAM
|
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) {
|
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),
|
IAMClientsMap: make(map[string]map[string]*iam.IAM),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b.resolveArnToUniqueIDFunc = b.resolveArnToRealUniqueId
|
||||||
|
|
||||||
b.Backend = &framework.Backend{
|
b.Backend = &framework.Backend{
|
||||||
PeriodicFunc: b.periodicFunc,
|
PeriodicFunc: b.periodicFunc,
|
||||||
AuthRenew: b.pathLoginRenew,
|
AuthRenew: b.pathLoginRenew,
|
||||||
@@ -171,9 +184,86 @@ func (b *backend) invalidate(key string) {
|
|||||||
defer b.configMutex.Unlock()
|
defer b.configMutex.Unlock()
|
||||||
b.flushCachedEC2Clients()
|
b.flushCachedEC2Clients()
|
||||||
b.flushCachedIAMClients()
|
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 = `
|
const backendHelp = `
|
||||||
aws-ec2 auth backend takes in PKCS#7 signature of an AWS EC2 instance and a client
|
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.
|
created nonce to authenticates the EC2 instance with Vault.
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go/aws/session"
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
"github.com/aws/aws-sdk-go/service/sts"
|
"github.com/aws/aws-sdk-go/service/sts"
|
||||||
"github.com/hashicorp/vault/helper/policyutil"
|
"github.com/hashicorp/vault/helper/policyutil"
|
||||||
"github.com/hashicorp/vault/logical"
|
"github.com/hashicorp/vault/logical"
|
||||||
|
"github.com/hashicorp/vault/logical/framework"
|
||||||
logicaltest "github.com/hashicorp/vault/logical/testing"
|
logicaltest "github.com/hashicorp/vault/logical/testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1346,7 +1348,7 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Received error retrieving identity: %s", err)
|
t.Fatalf("Received error retrieving identity: %s", err)
|
||||||
}
|
}
|
||||||
testIdentityArn, _, _, err := parseIamArn(*testIdentity.Arn)
|
entity, err := parseIamArn(*testIdentity.Arn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -1385,7 +1387,7 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) {
|
|||||||
|
|
||||||
// configuring the valid role we'll be able to login to
|
// configuring the valid role we'll be able to login to
|
||||||
roleData := map[string]interface{}{
|
roleData := map[string]interface{}{
|
||||||
"bound_iam_principal_arn": testIdentityArn,
|
"bound_iam_principal_arn": entity.canonicalArn(),
|
||||||
"policies": "root",
|
"policies": "root",
|
||||||
"auth_type": iamAuthType,
|
"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)
|
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
|
// 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
|
roleRequest.Path = "role/" + testInvalidRoleName
|
||||||
resp, err = b.HandleRequest(roleRequest)
|
resp, err = b.HandleRequest(roleRequest)
|
||||||
if err != nil || (resp != nil && resp.IsError()) {
|
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)
|
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
|
loginData["role"] = testValidRoleName
|
||||||
resp, err = b.HandleRequest(loginRequest)
|
resp, err = b.HandleRequest(loginRequest)
|
||||||
@@ -1501,4 +1512,52 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) {
|
|||||||
if resp == nil || resp.Auth == nil || resp.IsError() {
|
if resp == nil || resp.Auth == nil || resp.IsError() {
|
||||||
t.Errorf("bad: expected valid login: resp:%#v", resp)
|
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/aws/session"
|
||||||
"github.com/aws/aws-sdk-go/service/ec2"
|
"github.com/aws/aws-sdk-go/service/ec2"
|
||||||
"github.com/aws/aws-sdk-go/service/iam"
|
"github.com/aws/aws-sdk-go/service/iam"
|
||||||
|
"github.com/aws/aws-sdk-go/service/sts"
|
||||||
"github.com/hashicorp/go-cleanhttp"
|
"github.com/hashicorp/go-cleanhttp"
|
||||||
"github.com/hashicorp/vault/helper/awsutil"
|
"github.com/hashicorp/vault/helper/awsutil"
|
||||||
"github.com/hashicorp/vault/logical"
|
"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
|
// 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
|
// 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.
|
// 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)
|
config, err := b.getRawClientConfig(s, region, clientType)
|
||||||
if err != nil {
|
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")
|
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 != "" {
|
if stsRole != "" {
|
||||||
assumeRoleConfig, err := b.getRawClientConfig(s, region, "sts")
|
assumedCredentials := stscreds.NewCredentials(session.New(stsConfig), stsRole)
|
||||||
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)
|
|
||||||
// Test that we actually have permissions to assume the role
|
// Test that we actually have permissions to assume the role
|
||||||
if _, err = assumedCredentials.Get(); err != nil {
|
if _, err = assumedCredentials.Get(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
config.Credentials = assumedCredentials
|
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
|
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
|
// 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()
|
b.configMutex.RLock()
|
||||||
if b.EC2ClientsMap[region] != nil && b.EC2ClientsMap[region][stsRole] != nil {
|
if b.EC2ClientsMap[region] != nil && b.EC2ClientsMap[region][stsRole] != nil {
|
||||||
defer b.configMutex.RUnlock()
|
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
|
// Create an AWS config object using a chain of providers
|
||||||
var awsConfig *aws.Config
|
var awsConfig *aws.Config
|
||||||
var err error
|
awsConfig, err = b.getClientConfig(s, region, stsRole, accountID, "ec2")
|
||||||
awsConfig, err = b.getClientConfig(s, region, stsRole, "ec2")
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
// 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()
|
b.configMutex.RLock()
|
||||||
if b.IAMClientsMap[region] != nil && b.IAMClientsMap[region][stsRole] != nil {
|
if b.IAMClientsMap[region] != nil && b.IAMClientsMap[region][stsRole] != nil {
|
||||||
defer b.configMutex.RUnlock()
|
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
|
// Create an AWS config object using a chain of providers
|
||||||
var awsConfig *aws.Config
|
var awsConfig *aws.Config
|
||||||
var err error
|
awsConfig, err = b.getClientConfig(s, region, stsRole, accountID, "iam")
|
||||||
awsConfig, err = b.getClientConfig(s, region, stsRole, "iam")
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -129,6 +129,9 @@ func (b *backend) pathConfigClientDelete(
|
|||||||
// Remove all the cached EC2 client objects in the backend.
|
// Remove all the cached EC2 client objects in the backend.
|
||||||
b.flushCachedIAMClients()
|
b.flushCachedIAMClients()
|
||||||
|
|
||||||
|
// unset the cached default AWS account ID
|
||||||
|
b.defaultAWSAccountID = ""
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,6 +237,7 @@ func (b *backend) pathConfigClientCreateUpdate(
|
|||||||
if changedCreds {
|
if changedCreds {
|
||||||
b.flushCachedEC2Clients()
|
b.flushCachedEC2Clients()
|
||||||
b.flushCachedIAMClients()
|
b.flushCachedIAMClients()
|
||||||
|
b.defaultAWSAccountID = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
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
|
// validateInstance queries the status of the EC2 instance using AWS EC2 API
|
||||||
// and checks if the instance is running and is healthy
|
// and checks if the instance is running and is healthy
|
||||||
func (b *backend) validateInstance(s logical.Storage, instanceID, region, accountID string) (*ec2.Instance, error) {
|
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
|
// 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -472,32 +460,20 @@ func (b *backend) verifyInstanceMeetsRoleRequirements(
|
|||||||
|
|
||||||
// Extract out the instance profile name from the instance
|
// Extract out the instance profile name from the instance
|
||||||
// profile ARN
|
// profile ARN
|
||||||
iamInstanceProfileARNSlice := strings.SplitAfter(iamInstanceProfileARN, "/")
|
iamInstanceProfileEntity, err := parseIamArn(iamInstanceProfileARN)
|
||||||
iamInstanceProfileName := iamInstanceProfileARNSlice[len(iamInstanceProfileARNSlice)-1]
|
|
||||||
|
|
||||||
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("error fetching STS config for account ID %q: %q\n", identityDoc.AccountID, err), nil
|
return nil, fmt.Errorf("failed to parse IAM instance profile ARN %q; error: %v", iamInstanceProfileARN, err)
|
||||||
}
|
|
||||||
// An empty STS role signifies the master account
|
|
||||||
stsRole := ""
|
|
||||||
if sts != nil {
|
|
||||||
stsRole = sts.StsRole
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use instance profile ARN to fetch the associated role ARN
|
// 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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not fetch IAM client: %v", err)
|
return nil, fmt.Errorf("could not fetch IAM client: %v", err)
|
||||||
} else if iamClient == nil {
|
} else if iamClient == nil {
|
||||||
return nil, fmt.Errorf("received a nil iamClient")
|
return nil, fmt.Errorf("received a nil iamClient")
|
||||||
}
|
}
|
||||||
iamRoleARN, err := b.instanceIamRoleARN(iamClient, iamInstanceProfileName)
|
iamRoleARN, err := b.instanceIamRoleARN(iamClient, iamInstanceProfileEntity.FriendlyName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("IAM role ARN could not be fetched: %v", err)
|
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 {
|
if roleEntry.BoundIamPrincipalARN != "" && roleEntry.BoundIamPrincipalARN != canonicalArn {
|
||||||
return nil, fmt.Errorf("role no longer bound to arn %q", 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)
|
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 {
|
if err != nil {
|
||||||
return logical.ErrorResponse(fmt.Sprintf("error making upstream request: %v", 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 {
|
if err != nil {
|
||||||
return logical.ErrorResponse(fmt.Sprintf("Error parsing arn: %v", err)), nil
|
return logical.ErrorResponse(fmt.Sprintf("Error parsing arn: %v", err)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
roleName := data.Get("role").(string)
|
roleName := data.Get("role").(string)
|
||||||
if roleName == "" {
|
if roleName == "" {
|
||||||
roleName = principalName
|
roleName = entity.FriendlyName
|
||||||
}
|
}
|
||||||
|
|
||||||
roleEntry, err := b.lockedAWSRole(req.Storage, roleName)
|
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
|
// The role creation should ensure that either we're inferring this is an EC2 instance
|
||||||
// or that we're binding an ARN
|
// or that we're binding an ARN
|
||||||
if roleEntry.BoundIamPrincipalARN != "" && roleEntry.BoundIamPrincipalARN != canonicalArn {
|
// The only way BoundIamPrincipalID could get set is if BoundIamPrincipalARN was also set and
|
||||||
return logical.ErrorResponse(fmt.Sprintf("IAM Principal %q does not belong to the role %q", clientArn, roleName)), nil
|
// 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
|
policies := roleEntry.Policies
|
||||||
@@ -1171,9 +1170,9 @@ func (b *backend) pathLoginUpdateIam(
|
|||||||
inferredEntityType := ""
|
inferredEntityType := ""
|
||||||
inferredEntityId := ""
|
inferredEntityId := ""
|
||||||
if roleEntry.InferredEntityType == ec2EntityType {
|
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 {
|
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
|
// 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
|
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,
|
InstanceID: *instance.InstanceId,
|
||||||
AmiID: *instance.ImageId,
|
AmiID: *instance.ImageId,
|
||||||
AccountID: accountID,
|
AccountID: callerID.Account,
|
||||||
Region: roleEntry.InferredAWSRegion,
|
Region: roleEntry.InferredAWSRegion,
|
||||||
PendingTime: instance.LaunchTime.Format(time.RFC3339),
|
PendingTime: instance.LaunchTime.Format(time.RFC3339),
|
||||||
}
|
}
|
||||||
@@ -1191,11 +1190,11 @@ func (b *backend) pathLoginUpdateIam(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if validationError != nil {
|
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
|
inferredEntityType = ec2EntityType
|
||||||
inferredEntityId = sessionName
|
inferredEntityId = entity.SessionInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := &logical.Response{
|
resp := &logical.Response{
|
||||||
@@ -1203,18 +1202,19 @@ func (b *backend) pathLoginUpdateIam(
|
|||||||
Period: roleEntry.Period,
|
Period: roleEntry.Period,
|
||||||
Policies: policies,
|
Policies: policies,
|
||||||
Metadata: map[string]string{
|
Metadata: map[string]string{
|
||||||
"client_arn": clientArn,
|
"client_arn": callerID.Arn,
|
||||||
"canonical_arn": canonicalArn,
|
"canonical_arn": entity.canonicalArn(),
|
||||||
|
"client_user_id": callerUniqueId,
|
||||||
"auth_type": iamAuthType,
|
"auth_type": iamAuthType,
|
||||||
"inferred_entity_type": inferredEntityType,
|
"inferred_entity_type": inferredEntityType,
|
||||||
"inferred_entity_id": inferredEntityId,
|
"inferred_entity_id": inferredEntityId,
|
||||||
"inferred_aws_region": roleEntry.InferredAWSRegion,
|
"inferred_aws_region": roleEntry.InferredAWSRegion,
|
||||||
"account_id": accountID,
|
"account_id": entity.AccountNumber,
|
||||||
},
|
},
|
||||||
InternalData: map[string]interface{}{
|
InternalData: map[string]interface{}{
|
||||||
"role_name": roleName,
|
"role_name": roleName,
|
||||||
},
|
},
|
||||||
DisplayName: principalName,
|
DisplayName: entity.FriendlyName,
|
||||||
LeaseOptions: logical.LeaseOptions{
|
LeaseOptions: logical.LeaseOptions{
|
||||||
Renewable: true,
|
Renewable: true,
|
||||||
TTL: roleEntry.TTL,
|
TTL: roleEntry.TTL,
|
||||||
@@ -1267,29 +1267,44 @@ func hasValuesForIamAuth(data *framework.FieldData) (bool, bool) {
|
|||||||
(hasRequestMethod || hasRequestUrl || hasRequestBody || hasRequestHeaders)
|
(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:
|
// 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>
|
// 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
|
// 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>
|
// most people would expect, which is arn:aws:iam::<account_id>:role/<RoleName>
|
||||||
|
var entity iamEntity
|
||||||
fullParts := strings.Split(iamArn, ":")
|
fullParts := strings.Split(iamArn, ":")
|
||||||
principalFullName := fullParts[5]
|
if fullParts[0] != "arn" {
|
||||||
// principalFullName would now be something like user/<UserName> or assumed-role/<RoleName>/<RoleSessionName>
|
return nil, fmt.Errorf("unrecognized arn: does not begin with arn:")
|
||||||
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])
|
|
||||||
}
|
}
|
||||||
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 {
|
func validateVaultHeaderValue(headers http.Header, requestUrl *url.URL, requiredHeaderValue string) error {
|
||||||
@@ -1392,7 +1407,7 @@ func parseGetCallerIdentityResponse(response string) (GetCallerIdentityResponse,
|
|||||||
return result, err
|
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
|
// 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
|
// 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
|
// 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()
|
client := cleanhttp.DefaultClient()
|
||||||
response, err := client.Do(request)
|
response, err := client.Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", fmt.Errorf("error making request: %v", err)
|
return nil, fmt.Errorf("error making request: %v", err)
|
||||||
}
|
}
|
||||||
if response != nil {
|
if response != nil {
|
||||||
defer response.Body.Close()
|
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
|
// we check for status code afterwards to also print out response body
|
||||||
responseBody, err := ioutil.ReadAll(response.Body)
|
responseBody, err := ioutil.ReadAll(response.Body)
|
||||||
if response.StatusCode != 200 {
|
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))
|
callerIdentityResponse, err := parseGetCallerIdentityResponse(string(responseBody))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", fmt.Errorf("error parsing STS response")
|
return nil, fmt.Errorf("error parsing STS response")
|
||||||
}
|
}
|
||||||
clientArn := callerIdentityResponse.GetCallerIdentityResult[0].Arn
|
return &callerIdentityResponse.GetCallerIdentityResult[0], nil
|
||||||
if clientArn == "" {
|
|
||||||
return "", "", fmt.Errorf("no ARN validated")
|
|
||||||
}
|
|
||||||
return clientArn, callerIdentityResponse.GetCallerIdentityResult[0].Account, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetCallerIdentityResponse struct {
|
type GetCallerIdentityResponse struct {
|
||||||
@@ -1457,6 +1468,29 @@ type roleTagLoginResponse struct {
|
|||||||
DisallowReauthentication bool `json:"disallow_reauthentication" structs:"disallow_reauthentication" mapstructure:"disallow_reauthentication"`
|
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 iamServerIdHeader = "X-Vault-AWS-IAM-Server-ID"
|
||||||
|
|
||||||
const pathLoginSyn = `
|
const pathLoginSyn = `
|
||||||
|
|||||||
@@ -48,37 +48,36 @@ func TestBackend_pathLogin_getCallerIdentityResponse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBackend_pathLogin_parseIamArn(t *testing.T) {
|
func TestBackend_pathLogin_parseIamArn(t *testing.T) {
|
||||||
userArn := "arn:aws:iam::123456789012:user/MyUserName"
|
testParser := func(inputArn, expectedCanonicalArn string, expectedEntity iamEntity) {
|
||||||
assumedRoleArn := "arn:aws:sts::123456789012:assumed-role/RoleName/RoleSessionName"
|
entity, err := parseIamArn(inputArn)
|
||||||
baseRoleArn := "arn:aws:iam::123456789012:role/RoleName"
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
xformedUser, principalFriendlyName, sessionName, err := parseIamArn(userArn)
|
}
|
||||||
if err != nil {
|
if expectedCanonicalArn != "" && entity.canonicalArn() != expectedCanonicalArn {
|
||||||
t.Fatal(err)
|
t.Fatalf("expected to canonicalize ARN %q into %q but got %q instead", inputArn, expectedCanonicalArn, entity.canonicalArn())
|
||||||
}
|
}
|
||||||
if xformedUser != userArn {
|
if *entity != expectedEntity {
|
||||||
t.Fatalf("expected to transform ARN %#v into %#v but got %#v instead", userArn, userArn, xformedUser)
|
t.Fatalf("expected to get iamEntity %#v from input ARN %q but instead got %#v", expectedEntity, inputArn, *entity)
|
||||||
}
|
}
|
||||||
if principalFriendlyName != "MyUserName" {
|
|
||||||
t.Fatalf("expected to extract MyUserName from ARN %#v but got %#v instead", userArn, principalFriendlyName)
|
|
||||||
}
|
|
||||||
if sessionName != "" {
|
|
||||||
t.Fatalf("expected to extract no session name from ARN %#v but got %#v instead", userArn, sessionName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
xformedRole, principalFriendlyName, sessionName, err := parseIamArn(assumedRoleArn)
|
testParser("arn:aws:iam::123456789012:user/UserPath/MyUserName",
|
||||||
if err != nil {
|
"arn:aws:iam::123456789012:user/MyUserName",
|
||||||
t.Fatal(err)
|
iamEntity{Partition: "aws", AccountNumber: "123456789012", Type: "user", Path: "UserPath", FriendlyName: "MyUserName"},
|
||||||
}
|
)
|
||||||
if xformedRole != baseRoleArn {
|
canonicalRoleArn := "arn:aws:iam::123456789012:role/RoleName"
|
||||||
t.Fatalf("expected to transform ARN %#v into %#v but got %#v instead", assumedRoleArn, baseRoleArn, xformedRole)
|
testParser("arn:aws:sts::123456789012:assumed-role/RoleName/RoleSessionName",
|
||||||
}
|
canonicalRoleArn,
|
||||||
if principalFriendlyName != "RoleName" {
|
iamEntity{Partition: "aws", AccountNumber: "123456789012", Type: "assumed-role", FriendlyName: "RoleName", SessionInfo: "RoleSessionName"},
|
||||||
t.Fatalf("expected to extract principal name of RoleName from ARN %#v but got %#v instead", assumedRoleArn, sessionName)
|
)
|
||||||
}
|
testParser("arn:aws:iam::123456789012:role/RolePath/RoleName",
|
||||||
if sessionName != "RoleSessionName" {
|
canonicalRoleArn,
|
||||||
t.Fatalf("expected to extract role session name of RoleSessionName from ARN %#v but got %#v instead", assumedRoleArn, sessionName)
|
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) {
|
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
|
the value specified by this parameter. The value is prefix-matched
|
||||||
(as though it were a glob ending in '*'). This is only checked when
|
(as though it were a glob ending in '*'). This is only checked when
|
||||||
auth_type is ec2.`,
|
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": {
|
"inferred_entity_type": {
|
||||||
Type: framework.TypeString,
|
Type: framework.TypeString,
|
||||||
@@ -210,7 +218,7 @@ func (b *backend) lockedAWSRole(s logical.Storage, roleName string) (*awsRoleEnt
|
|||||||
if roleEntry == nil {
|
if roleEntry == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
needUpgrade, err := upgradeRoleEntry(roleEntry)
|
needUpgrade, err := b.upgradeRoleEntry(s, roleEntry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error upgrading roleEntry: %v", err)
|
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
|
return nil, nil
|
||||||
}
|
}
|
||||||
// now re-check to see if we need to upgrade
|
// 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)
|
return nil, fmt.Errorf("error upgrading roleEntry: %v", err)
|
||||||
}
|
}
|
||||||
if needUpgrade {
|
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
|
// If needed, updates the role entry and returns a bool indicating if it was updated
|
||||||
// (and thus needs to be persisted)
|
// (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 {
|
if roleEntry == nil {
|
||||||
return false, fmt.Errorf("received nil roleEntry")
|
return false, fmt.Errorf("received nil roleEntry")
|
||||||
}
|
}
|
||||||
@@ -307,6 +315,18 @@ func upgradeRoleEntry(roleEntry *awsRoleEntry) (bool, error) {
|
|||||||
upgraded = true
|
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
|
return upgraded, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -411,7 +431,7 @@ func (b *backend) pathRoleCreateUpdate(
|
|||||||
if roleEntry == nil {
|
if roleEntry == nil {
|
||||||
roleEntry = &awsRoleEntry{}
|
roleEntry = &awsRoleEntry{}
|
||||||
} else {
|
} else {
|
||||||
needUpdate, err := upgradeRoleEntry(roleEntry)
|
needUpdate, err := b.upgradeRoleEntry(req.Storage, roleEntry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return logical.ErrorResponse(fmt.Sprintf("failed to update roleEntry: %v", 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)
|
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 {
|
if boundIamRoleARNRaw, ok := data.GetOk("bound_iam_role_arn"); ok {
|
||||||
roleEntry.BoundIamRoleARN = boundIamRoleARNRaw.(string)
|
roleEntry.BoundIamRoleARN = boundIamRoleARNRaw.(string)
|
||||||
}
|
}
|
||||||
@@ -454,7 +487,26 @@ func (b *backend) pathRoleCreateUpdate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if boundIamPrincipalARNRaw, ok := data.GetOk("bound_iam_principal_arn"); ok {
|
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 {
|
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"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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"`
|
AllowInstanceMigration bool `json:"allow_instance_migration" structs:"allow_instance_migration" mapstructure:"allow_instance_migration"`
|
||||||
TTL time.Duration `json:"ttl" structs:"ttl" mapstructure:"ttl"`
|
TTL time.Duration `json:"ttl" structs:"ttl" mapstructure:"ttl"`
|
||||||
|
|||||||
@@ -135,7 +135,81 @@ func TestBackend_pathRoleEc2(t *testing.T) {
|
|||||||
if resp != nil {
|
if resp != nil {
|
||||||
t.Fatalf("bad: response: expected:nil actual:%#v\n", resp)
|
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) {
|
func TestBackend_pathIam(t *testing.T) {
|
||||||
@@ -174,6 +248,7 @@ func TestBackend_pathIam(t *testing.T) {
|
|||||||
"policies": "p,q,r,s",
|
"policies": "p,q,r,s",
|
||||||
"max_ttl": "2h",
|
"max_ttl": "2h",
|
||||||
"bound_iam_principal_arn": "n:aws:iam::123456789012:user/MyUserName",
|
"bound_iam_principal_arn": "n:aws:iam::123456789012:user/MyUserName",
|
||||||
|
"resolve_aws_unique_ids": false,
|
||||||
}
|
}
|
||||||
resp, err = b.HandleRequest(&logical.Request{
|
resp, err = b.HandleRequest(&logical.Request{
|
||||||
Operation: logical.CreateOperation,
|
Operation: logical.CreateOperation,
|
||||||
@@ -369,6 +444,7 @@ func TestBackend_pathRoleMixedTypes(t *testing.T) {
|
|||||||
|
|
||||||
data["inferred_entity_type"] = ec2EntityType
|
data["inferred_entity_type"] = ec2EntityType
|
||||||
data["inferred_aws_region"] = "us-east-1"
|
data["inferred_aws_region"] = "us-east-1"
|
||||||
|
data["resolve_aws_unique_ids"] = false
|
||||||
resp, err = submitRequest("multipleTypesInferred", logical.CreateOperation)
|
resp, err = submitRequest("multipleTypesInferred", logical.CreateOperation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -376,6 +452,29 @@ func TestBackend_pathRoleMixedTypes(t *testing.T) {
|
|||||||
if resp.IsError() {
|
if resp.IsError() {
|
||||||
t.Fatalf("didn't allow creation of roles with only inferred bindings")
|
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) {
|
func TestAwsEc2_RoleCrud(t *testing.T) {
|
||||||
@@ -417,11 +516,12 @@ func TestAwsEc2_RoleCrud(t *testing.T) {
|
|||||||
"bound_ami_id": "testamiid",
|
"bound_ami_id": "testamiid",
|
||||||
"bound_account_id": "testaccountid",
|
"bound_account_id": "testaccountid",
|
||||||
"bound_region": "testregion",
|
"bound_region": "testregion",
|
||||||
"bound_iam_role_arn": "testiamrolearn",
|
"bound_iam_role_arn": "arn:aws:iam::123456789012:role/MyRole",
|
||||||
"bound_iam_instance_profile_arn": "testiaminstanceprofilearn",
|
"bound_iam_instance_profile_arn": "arn:aws:iam::123456789012:instance-profile/MyInstanceProfile",
|
||||||
"bound_subnet_id": "testsubnetid",
|
"bound_subnet_id": "testsubnetid",
|
||||||
"bound_vpc_id": "testvpcid",
|
"bound_vpc_id": "testvpcid",
|
||||||
"role_tag": "testtag",
|
"role_tag": "testtag",
|
||||||
|
"resolve_aws_unique_ids": false,
|
||||||
"allow_instance_migration": true,
|
"allow_instance_migration": true,
|
||||||
"ttl": "10m",
|
"ttl": "10m",
|
||||||
"max_ttl": "20m",
|
"max_ttl": "20m",
|
||||||
@@ -451,12 +551,14 @@ func TestAwsEc2_RoleCrud(t *testing.T) {
|
|||||||
"bound_account_id": "testaccountid",
|
"bound_account_id": "testaccountid",
|
||||||
"bound_region": "testregion",
|
"bound_region": "testregion",
|
||||||
"bound_iam_principal_arn": "",
|
"bound_iam_principal_arn": "",
|
||||||
"bound_iam_role_arn": "testiamrolearn",
|
"bound_iam_principal_id": "",
|
||||||
"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_subnet_id": "testsubnetid",
|
||||||
"bound_vpc_id": "testvpcid",
|
"bound_vpc_id": "testvpcid",
|
||||||
"inferred_entity_type": "",
|
"inferred_entity_type": "",
|
||||||
"inferred_aws_region": "",
|
"inferred_aws_region": "",
|
||||||
|
"resolve_aws_unique_ids": false,
|
||||||
"role_tag": "testtag",
|
"role_tag": "testtag",
|
||||||
"allow_instance_migration": true,
|
"allow_instance_migration": true,
|
||||||
"ttl": time.Duration(600),
|
"ttl": time.Duration(600),
|
||||||
@@ -519,7 +621,8 @@ func TestAwsEc2_RoleDurationSeconds(t *testing.T) {
|
|||||||
|
|
||||||
roleData := map[string]interface{}{
|
roleData := map[string]interface{}{
|
||||||
"auth_type": "ec2",
|
"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",
|
"ttl": "10s",
|
||||||
"max_ttl": "20s",
|
"max_ttl": "20s",
|
||||||
"period": "30s",
|
"period": "30s",
|
||||||
@@ -554,3 +657,7 @@ func TestAwsEc2_RoleDurationSeconds(t *testing.T) {
|
|||||||
t.Fatalf("bad: period; expected: 30, actual: %d", resp.Data["period"])
|
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.
|
activated. This only applies to the iam auth method.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</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>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<span class="param">ttl</span>
|
<span class="param">ttl</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user