mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 02:28:09 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			219 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			219 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) HashiCorp, Inc.
 | |
| // SPDX-License-Identifier: MPL-2.0
 | |
| 
 | |
| package awsauth
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 
 | |
| 	"github.com/aws/aws-sdk-go/aws"
 | |
| 	"github.com/aws/aws-sdk-go/aws/credentials"
 | |
| 	"github.com/aws/aws-sdk-go/aws/session"
 | |
| 	"github.com/aws/aws-sdk-go/service/ec2"
 | |
| 	"github.com/aws/aws-sdk-go/service/iam"
 | |
| 	"github.com/aws/aws-sdk-go/service/iam/iamiface"
 | |
| 	"github.com/hashicorp/go-cleanhttp"
 | |
| 	"github.com/hashicorp/go-multierror"
 | |
| 	"github.com/hashicorp/go-secure-stdlib/awsutil"
 | |
| 	"github.com/hashicorp/vault/sdk/framework"
 | |
| 	"github.com/hashicorp/vault/sdk/logical"
 | |
| )
 | |
| 
 | |
| func (b *backend) pathConfigRotateRoot() *framework.Path {
 | |
| 	return &framework.Path{
 | |
| 		Pattern: "config/rotate-root",
 | |
| 
 | |
| 		DisplayAttrs: &framework.DisplayAttributes{
 | |
| 			OperationPrefix: operationPrefixAWS,
 | |
| 			OperationVerb:   "rotate",
 | |
| 			OperationSuffix: "auth-root-credentials",
 | |
| 		},
 | |
| 
 | |
| 		Operations: map[logical.Operation]framework.OperationHandler{
 | |
| 			logical.UpdateOperation: &framework.PathOperation{
 | |
| 				Callback: b.pathConfigRotateRootUpdate,
 | |
| 			},
 | |
| 		},
 | |
| 
 | |
| 		HelpSynopsis:    pathConfigRotateRootHelpSyn,
 | |
| 		HelpDescription: pathConfigRotateRootHelpDesc,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (b *backend) pathConfigRotateRootUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
 | |
| 	// First get the AWS key and secret and validate that we _can_ rotate them.
 | |
| 	// We need the read lock here to prevent anything else from mutating it while we're using it.
 | |
| 	b.configMutex.Lock()
 | |
| 	defer b.configMutex.Unlock()
 | |
| 
 | |
| 	clientConf, err := b.nonLockedClientConfigEntry(ctx, req.Storage)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	if clientConf == nil {
 | |
| 		return logical.ErrorResponse(`can't update client config because it's unset`), nil
 | |
| 	}
 | |
| 	if clientConf.AccessKey == "" {
 | |
| 		return logical.ErrorResponse("can't update access_key because it's unset"), nil
 | |
| 	}
 | |
| 	if clientConf.SecretKey == "" {
 | |
| 		return logical.ErrorResponse("can't update secret_key because it's unset"), nil
 | |
| 	}
 | |
| 
 | |
| 	// Getting our client through the b.clientIAM method requires values retrieved through
 | |
| 	// the user providing an ARN, which we don't have here, so let's just directly
 | |
| 	// make what we need.
 | |
| 	staticCreds := &credentials.StaticProvider{
 | |
| 		Value: credentials.Value{
 | |
| 			AccessKeyID:     clientConf.AccessKey,
 | |
| 			SecretAccessKey: clientConf.SecretKey,
 | |
| 		},
 | |
| 	}
 | |
| 	// By default, leave the iamEndpoint nil to tell AWS it's unset. However, if it is
 | |
| 	// configured, populate the pointer.
 | |
| 	var iamEndpoint *string
 | |
| 	if clientConf.IAMEndpoint != "" {
 | |
| 		iamEndpoint = aws.String(clientConf.IAMEndpoint)
 | |
| 	}
 | |
| 
 | |
| 	// Attempt to retrieve the region, error out if no region is provided.
 | |
| 	region, err := awsutil.GetRegion("")
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("error retrieving region: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	awsConfig := &aws.Config{
 | |
| 		Credentials: credentials.NewCredentials(staticCreds),
 | |
| 		Endpoint:    iamEndpoint,
 | |
| 
 | |
| 		// Generally speaking, GetRegion will use the Vault server's region. However, if this
 | |
| 		// needs to be overridden, an easy way would be to set the AWS_DEFAULT_REGION on the Vault server
 | |
| 		// to the desired region. If that's still insufficient for someone's use case, in the future we
 | |
| 		// could add the ability to specify the region either on the client config or as part of the
 | |
| 		// inbound rotation call.
 | |
| 		Region: aws.String(region),
 | |
| 
 | |
| 		// Prevents races.
 | |
| 		HTTPClient: cleanhttp.DefaultClient(),
 | |
| 	}
 | |
| 	sess, err := session.NewSession(awsConfig)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	iamClient := getIAMClient(sess)
 | |
| 
 | |
| 	// Get the current user's name since it's required to create an access key.
 | |
| 	// Empty input means get the current user.
 | |
| 	var getUserInput iam.GetUserInput
 | |
| 	getUserRes, err := iamClient.GetUserWithContext(ctx, &getUserInput)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("error calling GetUser: %w", err)
 | |
| 	}
 | |
| 	if getUserRes == nil {
 | |
| 		return nil, fmt.Errorf("nil response from GetUser")
 | |
| 	}
 | |
| 	if getUserRes.User == nil {
 | |
| 		return nil, fmt.Errorf("nil user returned from GetUser")
 | |
| 	}
 | |
| 	if getUserRes.User.UserName == nil {
 | |
| 		return nil, fmt.Errorf("nil UserName returned from GetUser")
 | |
| 	}
 | |
| 
 | |
| 	// Create the new access key and secret.
 | |
| 	createAccessKeyInput := iam.CreateAccessKeyInput{
 | |
| 		UserName: getUserRes.User.UserName,
 | |
| 	}
 | |
| 	createAccessKeyRes, err := iamClient.CreateAccessKeyWithContext(ctx, &createAccessKeyInput)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("error calling CreateAccessKey: %w", err)
 | |
| 	}
 | |
| 	if createAccessKeyRes.AccessKey == nil {
 | |
| 		return nil, fmt.Errorf("nil response from CreateAccessKey")
 | |
| 	}
 | |
| 	if createAccessKeyRes.AccessKey.AccessKeyId == nil || createAccessKeyRes.AccessKey.SecretAccessKey == nil {
 | |
| 		return nil, fmt.Errorf("nil AccessKeyId or SecretAccessKey returned from CreateAccessKey")
 | |
| 	}
 | |
| 
 | |
| 	// We're about to attempt to store the newly created key and secret, but just in case we can't,
 | |
| 	// let's clean up after ourselves.
 | |
| 	storedNewConf := false
 | |
| 	var errs error
 | |
| 	defer func() {
 | |
| 		if storedNewConf {
 | |
| 			return
 | |
| 		}
 | |
| 		// Attempt to delete the access key and secret we created but couldn't store and use.
 | |
| 		deleteAccessKeyInput := iam.DeleteAccessKeyInput{
 | |
| 			AccessKeyId: createAccessKeyRes.AccessKey.AccessKeyId,
 | |
| 			UserName:    getUserRes.User.UserName,
 | |
| 		}
 | |
| 		if _, err := iamClient.DeleteAccessKeyWithContext(ctx, &deleteAccessKeyInput); err != nil {
 | |
| 			// Include this error in the errs returned by this method.
 | |
| 			errs = multierror.Append(errs, fmt.Errorf("error deleting newly created but unstored access key ID %s: %s", *createAccessKeyRes.AccessKey.AccessKeyId, err))
 | |
| 		}
 | |
| 	}()
 | |
| 
 | |
| 	oldAccessKey := clientConf.AccessKey
 | |
| 	clientConf.AccessKey = *createAccessKeyRes.AccessKey.AccessKeyId
 | |
| 	clientConf.SecretKey = *createAccessKeyRes.AccessKey.SecretAccessKey
 | |
| 
 | |
| 	// Now get ready to update storage, doing everything beforehand so we can minimize how long
 | |
| 	// we need to hold onto the lock.
 | |
| 	newEntry, err := b.configClientToEntry(clientConf)
 | |
| 	if err != nil {
 | |
| 		errs = multierror.Append(errs, fmt.Errorf("error generating new client config JSON: %w", err))
 | |
| 		return nil, errs
 | |
| 	}
 | |
| 
 | |
| 	// Someday we may want to allow the user to send a number of seconds to wait here
 | |
| 	// before deleting the previous access key to allow work to complete. That would allow
 | |
| 	// AWS, which is eventually consistent, to finish populating the new key in all places.
 | |
| 	if err := req.Storage.Put(ctx, newEntry); err != nil {
 | |
| 		errs = multierror.Append(errs, fmt.Errorf("error saving new client config: %w", err))
 | |
| 		return nil, errs
 | |
| 	}
 | |
| 	storedNewConf = true
 | |
| 
 | |
| 	// Previous cached clients need to be cleared because they may have been made using
 | |
| 	// the soon-to-be-obsolete credentials.
 | |
| 	b.IAMClientsMap = make(map[string]map[string]*iam.IAM)
 | |
| 	b.EC2ClientsMap = make(map[string]map[string]*ec2.EC2)
 | |
| 
 | |
| 	// Now to clean up the old key.
 | |
| 	deleteAccessKeyInput := iam.DeleteAccessKeyInput{
 | |
| 		AccessKeyId: aws.String(oldAccessKey),
 | |
| 		UserName:    getUserRes.User.UserName,
 | |
| 	}
 | |
| 	if _, err = iamClient.DeleteAccessKeyWithContext(ctx, &deleteAccessKeyInput); err != nil {
 | |
| 		errs = multierror.Append(errs, fmt.Errorf("error deleting old access key ID %s: %w", oldAccessKey, err))
 | |
| 		return nil, errs
 | |
| 	}
 | |
| 	return &logical.Response{
 | |
| 		Data: map[string]interface{}{
 | |
| 			"access_key": clientConf.AccessKey,
 | |
| 		},
 | |
| 	}, nil
 | |
| }
 | |
| 
 | |
| // getIAMClient allows us to change how an IAM client is created
 | |
| // during testing. The AWS SDK doesn't easily lend itself to testing
 | |
| // using a Go httptest server because if you inject a test URL into
 | |
| // the config, the client strips important information about which
 | |
| // endpoint it's hitting. Per
 | |
| // https://aws.amazon.com/blogs/developer/mocking-out-then-aws-sdk-for-go-for-unit-testing/,
 | |
| // this is the recommended approach.
 | |
| var getIAMClient = func(sess *session.Session) iamiface.IAMAPI {
 | |
| 	return iam.New(sess)
 | |
| }
 | |
| 
 | |
| const pathConfigRotateRootHelpSyn = `
 | |
| Request to rotate the AWS credentials used by Vault
 | |
| `
 | |
| 
 | |
| const pathConfigRotateRootHelpDesc = `
 | |
| This path attempts to rotate the AWS credentials used by Vault for this mount.
 | |
| It is only valid if Vault has been configured to use AWS IAM credentials via the
 | |
| config/client endpoint.
 | |
| `
 | 
