open-vault/builtin/credential/aws/path_config_client.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)
`