Add environment and EC2 instance metadata role providers for AWS creds.

This commit is contained in:
Jeff Mitchell 2016-04-07 18:13:19 +00:00 committed by vishalnayak
parent 012f9273f7
commit a5aadc908d
4 changed files with 151 additions and 43 deletions

View file

@ -1,6 +1,9 @@
package aws
import (
"sync"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/vault/helper/salt"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
@ -57,6 +60,10 @@ func Backend(conf *logical.BackendConfig) (*framework.Backend, error) {
type backend struct {
*framework.Backend
Salt *salt.Salt
configMutex sync.RWMutex
ec2Client *ec2.EC2
}
const backendHelp = `

View file

@ -2,47 +2,98 @@ package aws
import (
"fmt"
"os"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/vault/logical"
)
// getClientConfig creates a aws-sdk-go config, which is used to create
// client that can interact with AWS API. This reads out the secret key
// and access key that was configured via 'config/client' endpoint and
// uses them to create credentials required to make the AWS API calls.
func getClientConfig(s logical.Storage) (*aws.Config, error) {
// getClientConfig creates a aws-sdk-go config, which is used to create client
// that can interact with AWS API. This builds credentials in the following
// order of preference:
//
// * Static credentials from 'config/client'
// * Environment variables
// * Instance metadata role
func (b *backend) getClientConfig(s logical.Storage) (*aws.Config, error) {
// Read the configured secret key and access key
config, err := clientConfigEntry(s)
if err != nil {
return nil, err
}
if config == nil {
return nil, fmt.Errorf(
"client credentials haven't been configured. Please configure\n" +
"them at the 'config/client' endpoint")
var providers []credentials.Provider
region := os.Getenv("AWS_REGION")
if config != nil {
if config.Region != "" {
region = config.Region
}
switch {
case config.AccessKey != "" && config.SecretKey != "":
providers = append(providers, &credentials.StaticProvider{
Value: credentials.Value{
AccessKeyID: config.AccessKey,
SecretAccessKey: config.SecretKey,
}})
case config.AccessKey == "" && config.AccessKey == "":
// Attempt to get credentials from the IAM instance role below
default: // Have one or the other but not both and not neither
return nil, fmt.Errorf(
"static AWS client credentials haven't been properly configured (the access key or secret key were provided but not both); configure or remove them at the 'config/client' endpoint")
}
}
providers = append(providers, &credentials.EnvProvider{})
// Create the credentials required to access the API.
creds := credentials.NewStaticCredentials(config.AccessKey, config.SecretKey, "")
providers = append(providers, &ec2rolecreds.EC2RoleProvider{
Client: ec2metadata.New(session.New(&aws.Config{
Region: aws.String(region),
HTTPClient: cleanhttp.DefaultClient(),
})),
ExpiryWindow: 15,
})
creds := credentials.NewChainCredentials(providers)
if creds == nil {
return nil, fmt.Errorf("could not compile valid credential providers from static config, environemnt, or instance metadata")
}
// Create a config that can be used to make the API calls.
return &aws.Config{
Credentials: creds,
Region: aws.String(config.Region),
Region: aws.String(region),
HTTPClient: cleanhttp.DefaultClient(),
}, nil
}
// clientEC2 creates a client to interact with AWS EC2 API.
func clientEC2(s logical.Storage) (*ec2.EC2, error) {
awsConfig, err := getClientConfig(s)
func (b *backend) clientEC2(s logical.Storage, recreate bool) (*ec2.EC2, error) {
if !recreate {
b.configMutex.RLock()
if b.ec2Client != nil {
defer b.configMutex.RUnlock()
return b.ec2Client, nil
}
b.configMutex.RUnlock()
}
b.configMutex.Lock()
defer b.configMutex.Unlock()
awsConfig, err := b.getClientConfig(s)
if err != nil {
return nil, err
}
return ec2.New(session.New(awsConfig)), nil
b.ec2Client = ec2.New(session.New(awsConfig))
return b.ec2Client, nil
}

View file

@ -3,6 +3,7 @@ package aws
import (
"fmt"
"github.com/fatih/structs"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
@ -23,8 +24,7 @@ func pathConfigClient(b *backend) *framework.Path {
"region": &framework.FieldSchema{
Type: framework.TypeString,
Default: "us-east-1",
Description: "Region for API calls.",
Description: "Region for API calls. Defaults to the value of the AWS_REGION env var. Required.",
},
},
@ -32,6 +32,8 @@ func pathConfigClient(b *backend) *framework.Path {
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.CreateOperation: b.pathConfigClientCreateUpdate,
logical.DeleteOperation: b.pathConfigClientDelete,
logical.ReadOperation: b.pathConfigClientRead,
logical.UpdateOperation: b.pathConfigClientCreateUpdate,
},
@ -44,6 +46,9 @@ func pathConfigClient(b *backend) *framework.Path {
// Returning 'true' forces an UpdateOperation, CreateOperation otherwise.
func (b *backend) pathConfigClientExistenceCheck(
req *logical.Request, data *framework.FieldData) (bool, error) {
b.configMutex.RLock()
defer b.configMutex.RUnlock()
entry, err := clientConfigEntry(req.Storage)
if err != nil {
return false, err
@ -68,10 +73,51 @@ func clientConfigEntry(s logical.Storage) (*clientConfig, error) {
return &result, nil
}
func (b *backend) pathConfigClientRead(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
b.configMutex.RLock()
defer b.configMutex.RUnlock()
clientConfig, err := clientConfigEntry(req.Storage)
if err != nil {
return nil, err
}
if clientConfig == nil {
return nil, nil
}
return &logical.Response{
Data: structs.New(clientConfig).Map(),
}, nil
}
func (b *backend) pathConfigClientDelete(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
b.configMutex.Lock()
err := req.Storage.Delete("config/client")
if err != nil {
b.configMutex.Unlock()
return nil, err
}
b.configMutex.Unlock()
_, err = b.clientEC2(req.Storage, true)
if err != nil {
return nil, fmt.Errorf("error creating client with updated credentials: %s", err)
}
return nil, nil
}
// pathConfigClientCreateUpdate is used to register the 'aws_secret_key' and 'aws_access_key'
// that can be used to interact with AWS EC2 API.
func (b *backend) pathConfigClientCreateUpdate(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
b.configMutex.Lock()
defer b.configMutex.Unlock()
configEntry, err := clientConfigEntry(req.Storage)
if err != nil {
@ -88,35 +134,27 @@ func (b *backend) pathConfigClientCreateUpdate(
configEntry.Region = data.Get("region").(string)
}
// Either a valid region needs to be provided or it should be left empty
// so a default value could take over.
if configEntry.Region == "" {
return nil, fmt.Errorf("invalid region")
}
changedCreds := false
accessKeyStr, ok := data.GetOk("access_key")
if ok {
if configEntry.AccessKey != accessKeyStr.(string) {
changedCreds = true
}
configEntry.AccessKey = accessKeyStr.(string)
} else if req.Operation == logical.CreateOperation {
if configEntry.AccessKey = data.Get("access_key").(string); configEntry.AccessKey == "" {
return nil, fmt.Errorf("missing access_key")
}
}
if configEntry.AccessKey == "" {
return nil, fmt.Errorf("invalid access_key")
// Use the default
configEntry.AccessKey = data.Get("access_key").(string)
}
secretKeyStr, ok := data.GetOk("secret_key")
if ok {
if configEntry.SecretKey != secretKeyStr.(string) {
changedCreds = true
}
configEntry.SecretKey = secretKeyStr.(string)
} else if req.Operation == logical.CreateOperation {
if configEntry.SecretKey = data.Get("secret_key").(string); configEntry.SecretKey == "" {
return nil, fmt.Errorf("missing secret_key")
}
}
if configEntry.SecretKey == "" {
return nil, fmt.Errorf("invalid secret_key")
configEntry.SecretKey = data.Get("secret_key").(string)
}
entry, err := logical.StorageEntryJSON("config/client", configEntry)
@ -128,6 +166,19 @@ func (b *backend) pathConfigClientCreateUpdate(
return nil, err
}
if changedCreds {
// We have to be careful here to re-lock as we have a deferred unlock
// queued up and unlocking an unlocked mutex leads to a panic
b.configMutex.Unlock()
_, err = b.clientEC2(req.Storage, true)
b.configMutex.Lock()
if err != nil {
return logical.ErrorResponse(fmt.Sprintf(
"error creating client with updated credentials: %s", err),
), nil
}
}
return nil, nil
}

View file

@ -41,9 +41,9 @@ func pathLogin(b *backend) *framework.Path {
// validateInstanceID queries the status of the EC2 instance using AWS EC2 API and
// checks if the instance is running and is healthy.
func validateInstanceID(s logical.Storage, instanceID string) error {
func (b *backend) validateInstanceID(s logical.Storage, instanceID string) error {
// Create an EC2 client to pull the instance information
ec2Client, err := clientEC2(s)
ec2Client, err := b.clientEC2(s, false)
if err != nil {
return err
}
@ -63,7 +63,6 @@ func validateInstanceID(s logical.Storage, instanceID string) error {
// validateMetadata matches the given client nonce and pending time with the one cached
// in the identity whitelist during the previous login.
func validateMetadata(clientNonce, pendingTime string, storedIdentity *whitelistIdentity, imageEntry *awsImageEntry) error {
givenPendingTime, err := time.Parse(time.RFC3339, pendingTime)
if err != nil {
return err
@ -190,7 +189,7 @@ func (b *backend) pathLoginUpdate(
}
// Validate the instance ID.
if err := validateInstanceID(req.Storage, identityDoc.InstanceID); err != nil {
if err := b.validateInstanceID(req.Storage, identityDoc.InstanceID); err != nil {
return logical.ErrorResponse(fmt.Sprintf("failed to verify instance ID: %s", err)), nil
}
@ -233,7 +232,7 @@ func (b *backend) pathLoginUpdate(
// Role tag is enabled for the AMI.
if imageEntry.RoleTag != "" {
// Overwrite the policies with the ones returned from processing the role tag.
resp, err := handleRoleTagLogin(req.Storage, identityDoc, imageEntry)
resp, err := b.handleRoleTagLogin(req.Storage, identityDoc, imageEntry)
if err != nil {
return nil, err
}
@ -291,8 +290,8 @@ func (b *backend) pathLoginUpdate(
// fetchRoleTagValue creates an AWS EC2 client and queries the tags
// attached to the instance identified by the given instanceID.
func fetchRoleTagValue(s logical.Storage, tagKey string) (string, error) {
ec2Client, err := clientEC2(s)
func (b *backend) fetchRoleTagValue(s logical.Storage, tagKey string) (string, error) {
ec2Client, err := b.clientEC2(s, false)
if err != nil {
return "", err
}
@ -324,13 +323,13 @@ func fetchRoleTagValue(s logical.Storage, tagKey string) (string, error) {
// handleRoleTagLogin is used to fetch the role tag if the instance and verifies it to be correct.
// Then the policies for the login request will be set off of the role tag, if certain creteria satisfies.
func handleRoleTagLogin(s logical.Storage, identityDoc *identityDocument, imageEntry *awsImageEntry) (*roleTagLoginResponse, error) {
func (b *backend) handleRoleTagLogin(s logical.Storage, identityDoc *identityDocument, imageEntry *awsImageEntry) (*roleTagLoginResponse, error) {
// Make a secondary call to the AWS instance to see if the desired tag is set.
// NOTE: If AWS adds the instance tags as meta-data in the instance identity
// document, then it is better to look this information there instead of making
// another API call. Currently, we don't have an option but make this call.
rTagValue, err := fetchRoleTagValue(s, imageEntry.RoleTag)
rTagValue, err := b.fetchRoleTagValue(s, imageEntry.RoleTag)
if err != nil {
return nil, err
}