| // 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) |
| ` |