Add environment and EC2 instance metadata role providers for AWS creds.
This commit is contained in:
parent
012f9273f7
commit
a5aadc908d
|
@ -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 = `
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue