mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-30 18:17:55 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			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)
 | |
| `
 | 
