-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Resolve AWS IAM unique IDs #2814
Changes from 4 commits
0f8e818
221d0f3
3d4dd56
d7d815a
3ea6c5a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
|
||
resolveArnToUniqueId 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.resolveArnToUniqueId = b.resolveArnToRealUniqueId | ||
|
||
b.Backend = &framework.Backend{ | ||
PeriodicFunc: b.periodicFunc, | ||
AuthRenew: b.pathLoginRenew, | ||
|
@@ -171,7 +184,75 @@ 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 | ||
} | ||
return *userInfo.User.UserId, nil | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Based on the current source, it's not possible. But, I don't see that as a guarantee documented anywhere, so better safe than sorry. I've added the additional checks to be safe. |
||
case "role": | ||
roleInfo, err := iamClient.GetRole(&iam.GetRoleInput{RoleName: &entity.FriendlyName}) | ||
if err != nil { | ||
return "", err | ||
} | ||
return *roleInfo.Role.RoleId, nil | ||
case "instance-profile": | ||
profileInfo, err := iamClient.GetInstanceProfile(&iam.GetInstanceProfileInput{InstanceProfileName: &entity.FriendlyName}) | ||
if err != nil { | ||
return "", err | ||
} | ||
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 = ` | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,36 @@ 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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you explain this change? Will it always be a valid assumption to assume that you're getting STS credentials? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question (and I probably should have commented it). Previously, Vault just assumed that if there wasn't an
In this PR, I need to resolve unique IDs, which requires looking up a particular IAM entity (user or role) in a given account with a given name. If I have an Thinking through this a little more, there's a small issue here in that an operator could call
I'd put it a little differently. I believe it is a valid assumption that, if you can make any AWS API call, you are able to call Does this all make sense? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My question was less about sts:GetCallerIdentity and more about whether it's safe to assume that you always want to use STS creds to make the client calls but from your explanation it sounds correct -- I assume that in any given account we should always be able to get STS creds? Or will this not be the case if they're not using IAM roles, in which case there needs to be a fallback? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure how to answer your question because the phrase "STS creds" isn't well defined, so I'm not sure what you're asking and thus not sure how to answer. Let me try to explain a little more, and if I'm still not answering your question, it'd probably be best to discuss offline. Apologies if this response is a bit basic, but I'm just trying to understand where we're not connecting. STS is both a service that requires credentials to authenticate to it and a service that can act as a source of credentials. The code in question does both. I'm pretty sure that any type of IAM entity can authenticate to STS.
Which client calls? If an If no
Which client calls? The only times we ask STS for credentials is when clients have explicitly told Vault to go to STS for credentials for those calls, and I'm not changing that.
In any given account, we should always be able to call
The Does this answer what you're asking? If not, what am I missing? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm going to go with "I'm out of my AWS depth here" and just trust you on this. Don't worry, it's not that you haven't answered my question, it's more that I'm not sure what my question is anymore, but your answer has convinced me that you know exactly what you're doing and I should leave it be :-D |
||
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) | ||
} | ||
b.defaultAWSAccountID = *identity.Account | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
} | ||
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 +138,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 +176,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 +201,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 +225,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 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we rename this to
resolveArnToUniqueIDFunc
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done