396 lines
13 KiB
Go
396 lines
13 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package awsauth
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/textproto"
|
|
"strings"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/hashicorp/go-secure-stdlib/strutil"
|
|
"github.com/hashicorp/vault/sdk/framework"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
func (b *backend) pathConfigClient() *framework.Path {
|
|
return &framework.Path{
|
|
Pattern: "config/client$",
|
|
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationPrefix: operationPrefixAWS,
|
|
},
|
|
|
|
Fields: map[string]*framework.FieldSchema{
|
|
"access_key": {
|
|
Type: framework.TypeString,
|
|
Default: "",
|
|
Description: "AWS Access Key ID for the account used to make AWS API requests.",
|
|
},
|
|
|
|
"secret_key": {
|
|
Type: framework.TypeString,
|
|
Default: "",
|
|
Description: "AWS Secret Access Key for the account used to make AWS API requests.",
|
|
},
|
|
|
|
"endpoint": {
|
|
Type: framework.TypeString,
|
|
Default: "",
|
|
Description: "URL to override the default generated endpoint for making AWS EC2 API calls.",
|
|
},
|
|
|
|
"iam_endpoint": {
|
|
Type: framework.TypeString,
|
|
Default: "",
|
|
Description: "URL to override the default generated endpoint for making AWS IAM API calls.",
|
|
},
|
|
|
|
"sts_endpoint": {
|
|
Type: framework.TypeString,
|
|
Default: "",
|
|
Description: "URL to override the default generated endpoint for making AWS STS API calls.",
|
|
},
|
|
|
|
"sts_region": {
|
|
Type: framework.TypeString,
|
|
Default: "",
|
|
Description: "The region ID for the sts_endpoint, if set.",
|
|
},
|
|
|
|
"iam_server_id_header_value": {
|
|
Type: framework.TypeString,
|
|
Default: "",
|
|
Description: "Value to require in the X-Vault-AWS-IAM-Server-ID request header",
|
|
},
|
|
|
|
"allowed_sts_header_values": {
|
|
Type: framework.TypeCommaStringSlice,
|
|
Default: nil,
|
|
Description: "List of additional headers that are allowed to be in AWS STS request headers",
|
|
},
|
|
|
|
"max_retries": {
|
|
Type: framework.TypeInt,
|
|
Default: aws.UseServiceDefaultRetries,
|
|
Description: "Maximum number of retries for recoverable exceptions of AWS APIs",
|
|
},
|
|
},
|
|
|
|
ExistenceCheck: b.pathConfigClientExistenceCheck,
|
|
|
|
Operations: map[logical.Operation]framework.OperationHandler{
|
|
logical.CreateOperation: &framework.PathOperation{
|
|
Callback: b.pathConfigClientCreateUpdate,
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationVerb: "configure",
|
|
OperationSuffix: "client",
|
|
},
|
|
},
|
|
logical.UpdateOperation: &framework.PathOperation{
|
|
Callback: b.pathConfigClientCreateUpdate,
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationVerb: "configure",
|
|
OperationSuffix: "client",
|
|
},
|
|
},
|
|
logical.DeleteOperation: &framework.PathOperation{
|
|
Callback: b.pathConfigClientDelete,
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationSuffix: "client-configuration",
|
|
},
|
|
},
|
|
logical.ReadOperation: &framework.PathOperation{
|
|
Callback: b.pathConfigClientRead,
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationSuffix: "client-configuration",
|
|
},
|
|
},
|
|
},
|
|
|
|
HelpSynopsis: pathConfigClientHelpSyn,
|
|
HelpDescription: pathConfigClientHelpDesc,
|
|
}
|
|
}
|
|
|
|
// Establishes dichotomy of request operation between CreateOperation and UpdateOperation.
|
|
// Returning 'true' forces an UpdateOperation, CreateOperation otherwise.
|
|
func (b *backend) pathConfigClientExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) {
|
|
entry, err := b.lockedClientConfigEntry(ctx, req.Storage)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return entry != nil, nil
|
|
}
|
|
|
|
// Fetch the client configuration required to access the AWS API, after acquiring an exclusive lock.
|
|
func (b *backend) lockedClientConfigEntry(ctx context.Context, s logical.Storage) (*clientConfig, error) {
|
|
b.configMutex.RLock()
|
|
defer b.configMutex.RUnlock()
|
|
|
|
return b.nonLockedClientConfigEntry(ctx, s)
|
|
}
|
|
|
|
// Fetch the client configuration required to access the AWS API.
|
|
func (b *backend) nonLockedClientConfigEntry(ctx context.Context, s logical.Storage) (*clientConfig, error) {
|
|
entry, err := s.Get(ctx, "config/client")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if entry == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
var result clientConfig
|
|
if err := entry.DecodeJSON(&result); err != nil {
|
|
return nil, err
|
|
}
|
|
return &result, nil
|
|
}
|
|
|
|
func (b *backend) pathConfigClientRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
|
clientConfig, err := b.lockedClientConfigEntry(ctx, req.Storage)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if clientConfig == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
return &logical.Response{
|
|
Data: map[string]interface{}{
|
|
"access_key": clientConfig.AccessKey,
|
|
"endpoint": clientConfig.Endpoint,
|
|
"iam_endpoint": clientConfig.IAMEndpoint,
|
|
"sts_endpoint": clientConfig.STSEndpoint,
|
|
"sts_region": clientConfig.STSRegion,
|
|
"iam_server_id_header_value": clientConfig.IAMServerIdHeaderValue,
|
|
"max_retries": clientConfig.MaxRetries,
|
|
"allowed_sts_header_values": clientConfig.AllowedSTSHeaderValues,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (b *backend) pathConfigClientDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
|
b.configMutex.Lock()
|
|
defer b.configMutex.Unlock()
|
|
|
|
if err := req.Storage.Delete(ctx, "config/client"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Remove all the cached EC2 client objects in the backend.
|
|
b.flushCachedEC2Clients()
|
|
|
|
// Remove all the cached EC2 client objects in the backend.
|
|
b.flushCachedIAMClients()
|
|
|
|
// unset the cached default AWS account ID
|
|
b.defaultAWSAccountID = ""
|
|
|
|
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(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
|
b.configMutex.Lock()
|
|
defer b.configMutex.Unlock()
|
|
|
|
configEntry, err := b.nonLockedClientConfigEntry(ctx, req.Storage)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if configEntry == nil {
|
|
configEntry = &clientConfig{}
|
|
}
|
|
|
|
// changedCreds is whether we need to flush the cached AWS clients and store in the backend
|
|
changedCreds := false
|
|
// changedOtherConfig is whether other config has changed that requires storing in the backend
|
|
// but does not require flushing the cached clients
|
|
changedOtherConfig := 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 {
|
|
// 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 {
|
|
configEntry.SecretKey = data.Get("secret_key").(string)
|
|
}
|
|
|
|
endpointStr, ok := data.GetOk("endpoint")
|
|
if ok {
|
|
if configEntry.Endpoint != endpointStr.(string) {
|
|
changedCreds = true
|
|
configEntry.Endpoint = endpointStr.(string)
|
|
}
|
|
} else if req.Operation == logical.CreateOperation {
|
|
configEntry.Endpoint = data.Get("endpoint").(string)
|
|
}
|
|
|
|
iamEndpointStr, ok := data.GetOk("iam_endpoint")
|
|
if ok {
|
|
if configEntry.IAMEndpoint != iamEndpointStr.(string) {
|
|
changedCreds = true
|
|
configEntry.IAMEndpoint = iamEndpointStr.(string)
|
|
}
|
|
} else if req.Operation == logical.CreateOperation {
|
|
configEntry.IAMEndpoint = data.Get("iam_endpoint").(string)
|
|
}
|
|
|
|
stsEndpointStr, ok := data.GetOk("sts_endpoint")
|
|
if ok {
|
|
if configEntry.STSEndpoint != stsEndpointStr.(string) {
|
|
// We don't directly cache STS clients as they are never directly used.
|
|
// However, they are potentially indirectly used as credential providers
|
|
// for the EC2 and IAM clients, and thus we would be indirectly caching
|
|
// them there. So, if we change the STS endpoint, we should flush those
|
|
// cached clients.
|
|
changedCreds = true
|
|
configEntry.STSEndpoint = stsEndpointStr.(string)
|
|
}
|
|
} else if req.Operation == logical.CreateOperation {
|
|
configEntry.STSEndpoint = data.Get("sts_endpoint").(string)
|
|
}
|
|
|
|
stsRegionStr, ok := data.GetOk("sts_region")
|
|
if ok {
|
|
if configEntry.STSRegion != stsRegionStr.(string) {
|
|
// Region is used when building STS clients. As such, all the comments
|
|
// regarding the sts_endpoint changing apply here as well.
|
|
changedCreds = true
|
|
configEntry.STSRegion = stsRegionStr.(string)
|
|
}
|
|
}
|
|
|
|
headerValStr, ok := data.GetOk("iam_server_id_header_value")
|
|
if ok {
|
|
if configEntry.IAMServerIdHeaderValue != headerValStr.(string) {
|
|
// NOT setting changedCreds here, since this isn't really cached
|
|
configEntry.IAMServerIdHeaderValue = headerValStr.(string)
|
|
changedOtherConfig = true
|
|
}
|
|
} else if req.Operation == logical.CreateOperation {
|
|
configEntry.IAMServerIdHeaderValue = data.Get("iam_server_id_header_value").(string)
|
|
}
|
|
|
|
aHeadersValStr, ok := data.GetOk("allowed_sts_header_values")
|
|
if ok {
|
|
aHeadersValSl := aHeadersValStr.([]string)
|
|
for i, v := range aHeadersValSl {
|
|
aHeadersValSl[i] = textproto.CanonicalMIMEHeaderKey(v)
|
|
}
|
|
if !strutil.EquivalentSlices(configEntry.AllowedSTSHeaderValues, aHeadersValSl) {
|
|
// NOT setting changedCreds here, since this isn't really cached
|
|
configEntry.AllowedSTSHeaderValues = aHeadersValSl
|
|
changedOtherConfig = true
|
|
}
|
|
} else if req.Operation == logical.CreateOperation {
|
|
ah, ok := data.GetOk("allowed_sts_header_values")
|
|
if ok {
|
|
configEntry.AllowedSTSHeaderValues = ah.([]string)
|
|
}
|
|
}
|
|
|
|
maxRetriesInt, ok := data.GetOk("max_retries")
|
|
if ok {
|
|
configEntry.MaxRetries = maxRetriesInt.(int)
|
|
changedOtherConfig = true
|
|
} else if req.Operation == logical.CreateOperation {
|
|
configEntry.MaxRetries = data.Get("max_retries").(int)
|
|
}
|
|
|
|
// Since this endpoint supports both create operation and update operation,
|
|
// the error checks for access_key and secret_key not being set are not present.
|
|
// This allows calling this endpoint multiple times to provide the values.
|
|
// Hence, the readers of this endpoint should do the validation on
|
|
// the validation of keys before using them.
|
|
entry, err := b.configClientToEntry(configEntry)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if changedCreds || changedOtherConfig || req.Operation == logical.CreateOperation {
|
|
if err := req.Storage.Put(ctx, entry); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if changedCreds {
|
|
b.flushCachedEC2Clients()
|
|
b.flushCachedIAMClients()
|
|
b.defaultAWSAccountID = ""
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// configClientToEntry allows the client config code to encapsulate its
|
|
// knowledge about where its config is stored. It also provides a way
|
|
// for other endpoints to update the config properly.
|
|
func (b *backend) configClientToEntry(conf *clientConfig) (*logical.StorageEntry, error) {
|
|
entry, err := logical.StorageEntryJSON("config/client", conf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return entry, nil
|
|
}
|
|
|
|
// Struct to hold 'aws_access_key' and 'aws_secret_key' that are required to
|
|
// interact with the AWS EC2 API.
|
|
type clientConfig struct {
|
|
AccessKey string `json:"access_key"`
|
|
SecretKey string `json:"secret_key"`
|
|
Endpoint string `json:"endpoint"`
|
|
IAMEndpoint string `json:"iam_endpoint"`
|
|
STSEndpoint string `json:"sts_endpoint"`
|
|
STSRegion string `json:"sts_region"`
|
|
IAMServerIdHeaderValue string `json:"iam_server_id_header_value"`
|
|
AllowedSTSHeaderValues []string `json:"allowed_sts_header_values"`
|
|
MaxRetries int `json:"max_retries"`
|
|
}
|
|
|
|
func (c *clientConfig) validateAllowedSTSHeaderValues(headers http.Header) error {
|
|
for k := range headers {
|
|
h := textproto.CanonicalMIMEHeaderKey(k)
|
|
if strings.HasPrefix(h, amzHeaderPrefix) &&
|
|
!strutil.StrListContains(defaultAllowedSTSRequestHeaders, h) &&
|
|
!strutil.StrListContains(c.AllowedSTSHeaderValues, h) {
|
|
return errors.New("invalid request header: " + k)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
const pathConfigClientHelpSyn = `
|
|
Configure AWS IAM credentials that are used to query instance and role details from the AWS API.
|
|
`
|
|
|
|
const pathConfigClientHelpDesc = `
|
|
The aws-ec2 auth method makes AWS API queries to retrieve information
|
|
regarding EC2 instances that perform login operations. The 'aws_secret_key' and
|
|
'aws_access_key' parameters configured here should map to an AWS IAM user that
|
|
has permission to make the following API queries:
|
|
|
|
* ec2:DescribeInstances
|
|
* iam:GetInstanceProfile (if IAM Role binding is used)
|
|
`
|