Skip to content

Commit

Permalink
Add SDK helpers and Core stubs for plugins to communicate with Enterp…
Browse files Browse the repository at this point in the history
…rise Rotation Manager (#29273)

Co-authored-by: Robert <[email protected]>
Co-authored-by: John-Michael Faircloth <[email protected]>
  • Loading branch information
3 people authored Jan 7, 2025
1 parent f188016 commit 27bd3e9
Show file tree
Hide file tree
Showing 21 changed files with 1,992 additions and 1,175 deletions.
87 changes: 87 additions & 0 deletions builtin/logical/aws/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"sync"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/aws/aws-sdk-go/service/iam/iamiface"
"github.com/aws/aws-sdk-go/service/sts/stsiface"
"github.com/hashicorp/vault/sdk/framework"
Expand Down Expand Up @@ -74,6 +76,91 @@ func Backend(_ *logical.BackendConfig) *backend {
}
return nil
},
RotateCredential: func(ctx context.Context, req *logical.Request) error {
// the following code is a modified version of the rotate-root method
client, err := b.clientIAM(ctx, req.Storage)
if err != nil {
return err
}
if client == nil {
return fmt.Errorf("nil IAM client")
}

b.clientMutex.Lock()
defer b.clientMutex.Unlock()

rawRootConfig, err := req.Storage.Get(ctx, "config/root")
if err != nil {
return err
}
if rawRootConfig == nil {
return fmt.Errorf("no configuration found for config/root")
}
var config rootConfig
if err := rawRootConfig.DecodeJSON(&config); err != nil {
return fmt.Errorf("error reading root configuration: %w", err)
}

if config.AccessKey == "" || config.SecretKey == "" {
return fmt.Errorf("cannot call config/rotate-root when either access_key or secret_key is empty")
}

var getUserInput iam.GetUserInput // empty input means get current user
getUserRes, err := client.GetUserWithContext(ctx, &getUserInput)
if err != nil {
return fmt.Errorf("error calling GetUser: %w", err)
}
if getUserRes == nil {
return fmt.Errorf("nil response from GetUser")
}
if getUserRes.User == nil {
return fmt.Errorf("nil user returned from GetUser")
}
if getUserRes.User.UserName == nil {
return fmt.Errorf("nil UserName returned from GetUser")
}

createAccessKeyInput := iam.CreateAccessKeyInput{
UserName: getUserRes.User.UserName,
}
createAccessKeyRes, err := client.CreateAccessKeyWithContext(ctx, &createAccessKeyInput)
if err != nil {
return fmt.Errorf("error calling CreateAccessKey: %w", err)
}
if createAccessKeyRes.AccessKey == nil {
return fmt.Errorf("nil response from CreateAccessKey")
}
if createAccessKeyRes.AccessKey.AccessKeyId == nil || createAccessKeyRes.AccessKey.SecretAccessKey == nil {
return fmt.Errorf("nil AccessKeyId or SecretAccessKey returned from CreateAccessKey")
}

oldAccessKey := config.AccessKey

config.AccessKey = *createAccessKeyRes.AccessKey.AccessKeyId
config.SecretKey = *createAccessKeyRes.AccessKey.SecretAccessKey

newEntry, err := logical.StorageEntryJSON("config/root", config)
if err != nil {
return fmt.Errorf("error generating new config/root JSON: %w", err)
}
if err := req.Storage.Put(ctx, newEntry); err != nil {
return fmt.Errorf("error saving new config/root: %w", err)
}

b.iamClient = nil
b.stsClient = nil

deleteAccessKeyInput := iam.DeleteAccessKeyInput{
AccessKeyId: aws.String(oldAccessKey),
UserName: getUserRes.User.UserName,
}
_, err = client.DeleteAccessKeyWithContext(ctx, &deleteAccessKeyInput)
if err != nil {
return fmt.Errorf("error deleting old access key: %w", err)
}

return nil
},
BackendType: logical.TypeLogical,
}

Expand Down
111 changes: 98 additions & 13 deletions builtin/logical/aws/path_config_root.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ import (

"github.com/aws/aws-sdk-go/aws"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/automatedrotationutil"
"github.com/hashicorp/vault/sdk/helper/pluginidentityutil"
"github.com/hashicorp/vault/sdk/helper/pluginutil"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/sdk/rotation"
)

// A single default template that supports both the different credential types (IAM/STS) that are capped at differing length limits (64 chars/32 chars respectively)
const defaultUserNameTemplate = `{{ if (eq .Type "STS") }}{{ printf "vault-%s-%s" (unix_time) (random 20) | truncate 32 }}{{ else }}{{ printf "vault-%s-%s-%s" (printf "%s-%s" (.DisplayName) (.PolicyName) | truncate 42) (unix_time) (random 20) | truncate 64 }}{{ end }}`
const (
defaultUserNameTemplate = `{{ if (eq .Type "STS") }}{{ printf "vault-%s-%s" (unix_time) (random 20) | truncate 32 }}{{ else }}{{ printf "vault-%s-%s-%s" (printf "%s-%s" (.DisplayName) (.PolicyName) | truncate 42) (unix_time) (random 20) | truncate 64 }}{{ end }}`
rootRotationJobName = "aws-root-creds"
)

func pathConfigRoot(b *backend) *framework.Path {
p := &framework.Path{
Expand Down Expand Up @@ -95,6 +100,7 @@ func pathConfigRoot(b *backend) *framework.Path {
HelpDescription: pathConfigRootHelpDesc,
}
pluginidentityutil.AddPluginIdentityTokenFields(p.Fields)
automatedrotationutil.AddAutomatedRotationFields(p.Fields)

return p
}
Expand All @@ -103,20 +109,14 @@ func (b *backend) pathConfigRootRead(ctx context.Context, req *logical.Request,
b.clientMutex.RLock()
defer b.clientMutex.RUnlock()

entry, err := req.Storage.Get(ctx, "config/root")
config, exists, err := getConfigFromStorage(ctx, req)
if err != nil {
return nil, err
}
if entry == nil {
if !exists {
return nil, nil
}

var config rootConfig

if err := entry.DecodeJSON(&config); err != nil {
return nil, err
}

configData := map[string]interface{}{
"access_key": config.AccessKey,
"region": config.Region,
Expand All @@ -131,6 +131,8 @@ func (b *backend) pathConfigRootRead(ctx context.Context, req *logical.Request,
}

config.PopulatePluginIdentityTokenData(configData)
config.PopulateAutomatedRotationData(configData)

return &logical.Response{
Data: configData,
}, nil
Expand Down Expand Up @@ -158,6 +160,12 @@ func (b *backend) pathConfigRootWrite(ctx context.Context, req *logical.Request,
b.clientMutex.Lock()
defer b.clientMutex.Unlock()

// check for existing config
previousCfg, previousCfgExists, err := getConfigFromStorage(ctx, req)
if err != nil {
return nil, err
}

rc := rootConfig{
AccessKey: data.Get("access_key").(string),
SecretKey: data.Get("secret_key").(string),
Expand All @@ -174,6 +182,9 @@ func (b *backend) pathConfigRootWrite(ctx context.Context, req *logical.Request,
if err := rc.ParsePluginIdentityTokenFields(data); err != nil {
return logical.ErrorResponse(err.Error()), nil
}
if err := rc.ParseAutomatedRotationFields(data); err != nil {
return logical.ErrorResponse(err.Error()), nil
}

if rc.IdentityTokenAudience != "" && rc.AccessKey != "" {
return logical.ErrorResponse("only one of 'access_key' or 'identity_token_audience' can be set"), nil
Expand All @@ -195,12 +206,54 @@ func (b *backend) pathConfigRootWrite(ctx context.Context, req *logical.Request,
}
}

entry, err := logical.StorageEntryJSON("config/root", rc)
if err != nil {
return nil, err
// Save the initial config only if it does not already exist
if !previousCfgExists {
if err := putConfigToStorage(ctx, req, &rc); err != nil {
return nil, err
}
}

if err := req.Storage.Put(ctx, entry); err != nil {
// Now that the root config is set up, register the rotation job if it required
if rc.ShouldRegisterRotationJob() {
cfgReq := &rotation.RotationJobConfigureRequest{
Name: rootRotationJobName,
MountPoint: req.MountPoint,
ReqPath: req.Path,
RotationSchedule: rc.RotationSchedule,
RotationWindow: rc.RotationWindow,
RotationPeriod: rc.RotationPeriod,
}

rotationJob, err := rotation.ConfigureRotationJob(cfgReq)
if err != nil {
return logical.ErrorResponse("error configuring rotation job: %s", err), nil
}

b.Logger().Debug("Registering rotation job", "mount", req.MountPoint+req.Path)
rotationID, err := b.System().RegisterRotationJob(ctx, rotationJob)
if err != nil {
return logical.ErrorResponse("error registering rotation job: %s", err), nil
}

rc.RotationID = rotationID
}

// Disable Automated Rotation and Deregister credentials if required
if rc.DisableAutomatedRotation {
// Ensure de-registering only occurs on updates and if
// a credential has actually been registered
if previousCfgExists && previousCfg.RotationID != "" {
err := b.System().DeregisterRotationJob(ctx, previousCfg.RotationID)
if err != nil {
return logical.ErrorResponse("error de-registering rotation job: %s", err), nil
}

rc.RotationID = ""
}
}

// update config entry with rotation ID
if err := putConfigToStorage(ctx, req, &rc); err != nil {
return nil, err
}

Expand All @@ -212,8 +265,40 @@ func (b *backend) pathConfigRootWrite(ctx context.Context, req *logical.Request,
return nil, nil
}

func getConfigFromStorage(ctx context.Context, req *logical.Request) (*rootConfig, bool, error) {
entry, err := req.Storage.Get(ctx, "config/root")
if err != nil {
return nil, false, err
}
if entry == nil {
return nil, false, nil
}

var config rootConfig

if err := entry.DecodeJSON(&config); err != nil {
return nil, false, err
}

return &config, true, nil
}

func putConfigToStorage(ctx context.Context, req *logical.Request, rc *rootConfig) error {
entry, err := logical.StorageEntryJSON("config/root", rc)
if err != nil {
return err
}

if err := req.Storage.Put(ctx, entry); err != nil {
return err
}

return nil
}

type rootConfig struct {
pluginidentityutil.PluginIdentityTokenParams
automatedrotationutil.AutomatedRotationParams

AccessKey string `json:"access_key"`
SecretKey string `json:"secret_key"`
Expand Down
Loading

0 comments on commit 27bd3e9

Please sign in to comment.