Add automated root rotation support to DB Secrets (#29557)

This commit is contained in:
vinay-gopalan
2025-02-11 12:09:26 -08:00
committed by GitHub
parent b9ee65e302
commit 9e38a88883
11 changed files with 258 additions and 124 deletions

View File

@@ -6,6 +6,7 @@ package awsauth
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"net/http" "net/http"
"net/textproto" "net/textproto"
"net/url" "net/url"
@@ -394,7 +395,7 @@ func (b *backend) pathConfigClientCreateUpdate(ctx context.Context, req *logical
var performedRotationManagerOpern string var performedRotationManagerOpern string
if configEntry.ShouldDeregisterRotationJob() { if configEntry.ShouldDeregisterRotationJob() {
performedRotationManagerOpern = "deregistration" performedRotationManagerOpern = rotation.PerformedDeregistration
// Disable Automated Rotation and Deregister credentials if required // Disable Automated Rotation and Deregister credentials if required
deregisterReq := &rotation.RotationJobDeregisterRequest{ deregisterReq := &rotation.RotationJobDeregisterRequest{
MountPoint: req.MountPoint, MountPoint: req.MountPoint,
@@ -406,7 +407,7 @@ func (b *backend) pathConfigClientCreateUpdate(ctx context.Context, req *logical
return logical.ErrorResponse("error deregistering rotation job: %s", err), nil return logical.ErrorResponse("error deregistering rotation job: %s", err), nil
} }
} else if configEntry.ShouldRegisterRotationJob() { } else if configEntry.ShouldRegisterRotationJob() {
performedRotationManagerOpern = "registration" performedRotationManagerOpern = rotation.PerformedRegistration
// Register the rotation job if it's required. // Register the rotation job if it's required.
cfgReq := &rotation.RotationJobConfigureRequest{ cfgReq := &rotation.RotationJobConfigureRequest{
MountPoint: req.MountPoint, MountPoint: req.MountPoint,
@@ -434,11 +435,15 @@ func (b *backend) pathConfigClientCreateUpdate(ctx context.Context, req *logical
if changedCreds || changedOtherConfig || req.Operation == logical.CreateOperation { if changedCreds || changedOtherConfig || req.Operation == logical.CreateOperation {
if err := req.Storage.Put(ctx, entry); err != nil { if err := req.Storage.Put(ctx, entry); err != nil {
b.Logger().Error("write to storage failed but the rotation manager still succeeded.",
"operation", performedRotationManagerOpern, "mount", req.MountPoint, "path", req.Path)
wrappedError := err
if performedRotationManagerOpern != "" { if performedRotationManagerOpern != "" {
b.Logger().Error("write to storage failed but the rotation manager still succeeded.", wrappedError = fmt.Errorf("write to storage failed but the rotation manager still succeeded; "+
"operation", performedRotationManagerOpern, "mount", req.MountPoint, "path", req.Path) "operation=%s, mount=%s, path=%s, storageError=%s", performedRotationManagerOpern, req.MountPoint, req.Path, err)
} }
return nil, err return nil, wrappedError
} }
} }

View File

@@ -6,6 +6,7 @@ package aws
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/framework"
@@ -252,7 +253,7 @@ func (b *backend) pathConfigRootWrite(ctx context.Context, req *logical.Request,
var performedRotationManagerOpern string var performedRotationManagerOpern string
if rc.ShouldDeregisterRotationJob() { if rc.ShouldDeregisterRotationJob() {
performedRotationManagerOpern = "deregistration" performedRotationManagerOpern = rotation.PerformedDeregistration
// Disable Automated Rotation and Deregister credentials if required // Disable Automated Rotation and Deregister credentials if required
deregisterReq := &rotation.RotationJobDeregisterRequest{ deregisterReq := &rotation.RotationJobDeregisterRequest{
MountPoint: req.MountPoint, MountPoint: req.MountPoint,
@@ -264,7 +265,7 @@ func (b *backend) pathConfigRootWrite(ctx context.Context, req *logical.Request,
return logical.ErrorResponse("error deregistering rotation job: %s", err), nil return logical.ErrorResponse("error deregistering rotation job: %s", err), nil
} }
} else if rc.ShouldRegisterRotationJob() { } else if rc.ShouldRegisterRotationJob() {
performedRotationManagerOpern = "registration" performedRotationManagerOpern = rotation.PerformedRegistration
// Register the rotation job if it's required. // Register the rotation job if it's required.
cfgReq := &rotation.RotationJobConfigureRequest{ cfgReq := &rotation.RotationJobConfigureRequest{
MountPoint: req.MountPoint, MountPoint: req.MountPoint,
@@ -282,11 +283,15 @@ func (b *backend) pathConfigRootWrite(ctx context.Context, req *logical.Request,
// Save the config // Save the config
if err := putConfigToStorage(ctx, req, rc); err != nil { if err := putConfigToStorage(ctx, req, rc); err != nil {
b.Logger().Error("write to storage failed but the rotation manager still succeeded.",
"operation", performedRotationManagerOpern, "mount", req.MountPoint, "path", req.Path)
wrappedError := err
if performedRotationManagerOpern != "" { if performedRotationManagerOpern != "" {
b.Logger().Error("write to storage failed but the rotation manager still succeeded.", wrappedError = fmt.Errorf("write to storage failed but the rotation manager still succeeded; "+
"operation", performedRotationManagerOpern, "mount", req.MountPoint, "path", req.Path) "operation=%s, mount=%s, path=%s, storageError=%s", performedRotationManagerOpern, req.MountPoint, req.Path, err)
} }
return nil, err return nil, wrappedError
} }
// clear possible cached IAM / STS clients after successfully updating // clear possible cached IAM / STS clients after successfully updating

View File

@@ -8,6 +8,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/rpc" "net/rpc"
"regexp"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@@ -39,6 +40,8 @@ const (
minRootCredRollbackAge = 1 * time.Minute minRootCredRollbackAge = 1 * time.Minute
) )
var databaseConfigNameFromRotationIDRegex = regexp.MustCompile("^.+/config/(.+$)")
type dbPluginInstance struct { type dbPluginInstance struct {
sync.RWMutex sync.RWMutex
database databaseVersionWrapper database databaseVersionWrapper
@@ -127,6 +130,14 @@ func Backend(conf *logical.BackendConfig) *databaseBackend {
WALRollback: b.walRollback, WALRollback: b.walRollback,
WALRollbackMinAge: minRootCredRollbackAge, WALRollbackMinAge: minRootCredRollbackAge,
BackendType: logical.TypeLogical, BackendType: logical.TypeLogical,
RotateCredential: func(ctx context.Context, request *logical.Request) error {
name, err := b.getDatabaseConfigNameFromRotationID(request.RotationID)
if err != nil {
return err
}
_, err = b.rotateRootCredentials(ctx, request, name)
return err
},
} }
b.logger = conf.Logger b.logger = conf.Logger
@@ -483,6 +494,17 @@ func (b *databaseBackend) dbEvent(ctx context.Context,
} }
} }
func (b *databaseBackend) getDatabaseConfigNameFromRotationID(path string) (string, error) {
if !databaseConfigNameFromRotationIDRegex.MatchString(path) {
return "", fmt.Errorf("no name found from rotation ID")
}
res := databaseConfigNameFromRotationIDRegex.FindStringSubmatch(path)
if len(res) != 2 {
return "", fmt.Errorf("unexpected number of matches (%d) for name in rotation ID", len(res))
}
return res[1], nil
}
const backendHelp = ` const backendHelp = `
The database backend supports using many different databases The database backend supports using many different databases
as secret backends, including but not limited to: as secret backends, including but not limited to:

View File

@@ -213,6 +213,10 @@ func TestBackend_config_connection(t *testing.T) {
"plugin_version": "", "plugin_version": "",
"verify_connection": false, "verify_connection": false,
"skip_static_role_import_rotation": false, "skip_static_role_import_rotation": false,
"rotation_schedule": "",
"rotation_period": 0,
"rotation_window": 0,
"disable_automated_rotation": false,
} }
configReq.Operation = logical.ReadOperation configReq.Operation = logical.ReadOperation
resp, err = b.HandleRequest(namespace.RootContext(nil), configReq) resp, err = b.HandleRequest(namespace.RootContext(nil), configReq)
@@ -221,6 +225,7 @@ func TestBackend_config_connection(t *testing.T) {
} }
delete(resp.Data["connection_details"].(map[string]interface{}), "name") delete(resp.Data["connection_details"].(map[string]interface{}), "name")
delete(resp.Data, "AutomatedRotationParams")
if !reflect.DeepEqual(expected, resp.Data) { if !reflect.DeepEqual(expected, resp.Data) {
t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data) t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data)
} }
@@ -269,6 +274,10 @@ func TestBackend_config_connection(t *testing.T) {
"plugin_version": "", "plugin_version": "",
"verify_connection": false, "verify_connection": false,
"skip_static_role_import_rotation": false, "skip_static_role_import_rotation": false,
"rotation_schedule": "",
"rotation_period": 0,
"rotation_window": 0,
"disable_automated_rotation": false,
} }
configReq.Operation = logical.ReadOperation configReq.Operation = logical.ReadOperation
resp, err = b.HandleRequest(namespace.RootContext(nil), configReq) resp, err = b.HandleRequest(namespace.RootContext(nil), configReq)
@@ -277,6 +286,7 @@ func TestBackend_config_connection(t *testing.T) {
} }
delete(resp.Data["connection_details"].(map[string]interface{}), "name") delete(resp.Data["connection_details"].(map[string]interface{}), "name")
delete(resp.Data, "AutomatedRotationParams")
if !reflect.DeepEqual(expected, resp.Data) { if !reflect.DeepEqual(expected, resp.Data) {
t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data) t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data)
} }
@@ -314,6 +324,10 @@ func TestBackend_config_connection(t *testing.T) {
"plugin_version": "", "plugin_version": "",
"verify_connection": false, "verify_connection": false,
"skip_static_role_import_rotation": false, "skip_static_role_import_rotation": false,
"rotation_schedule": "",
"rotation_period": 0,
"rotation_window": 0,
"disable_automated_rotation": false,
} }
configReq.Operation = logical.ReadOperation configReq.Operation = logical.ReadOperation
resp, err = b.HandleRequest(namespace.RootContext(nil), configReq) resp, err = b.HandleRequest(namespace.RootContext(nil), configReq)
@@ -322,6 +336,7 @@ func TestBackend_config_connection(t *testing.T) {
} }
delete(resp.Data["connection_details"].(map[string]interface{}), "name") delete(resp.Data["connection_details"].(map[string]interface{}), "name")
delete(resp.Data, "AutomatedRotationParams")
if !reflect.DeepEqual(expected, resp.Data) { if !reflect.DeepEqual(expected, resp.Data) {
t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data) t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data)
} }
@@ -773,6 +788,10 @@ func TestBackend_connectionCrud(t *testing.T) {
"plugin_version": "", "plugin_version": "",
"verify_connection": false, "verify_connection": false,
"skip_static_role_import_rotation": false, "skip_static_role_import_rotation": false,
"rotation_schedule": "",
"rotation_period": json.Number("0"),
"rotation_window": json.Number("0"),
"disable_automated_rotation": false,
} }
resp, err = client.Read("database/config/plugin-test") resp, err = client.Read("database/config/plugin-test")
if err != nil { if err != nil {
@@ -780,6 +799,7 @@ func TestBackend_connectionCrud(t *testing.T) {
} }
delete(resp.Data["connection_details"].(map[string]interface{}), "name") delete(resp.Data["connection_details"].(map[string]interface{}), "name")
delete(resp.Data, "AutomatedRotationParams")
if diff := deep.Equal(resp.Data, expected); diff != nil { if diff := deep.Equal(resp.Data, expected); diff != nil {
t.Fatal(strings.Join(diff, "\n")) t.Fatal(strings.Join(diff, "\n"))
} }

View File

@@ -17,9 +17,11 @@ import (
"github.com/hashicorp/vault/helper/versions" "github.com/hashicorp/vault/helper/versions"
v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5" v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
"github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/automatedrotationutil"
"github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/pluginutil" "github.com/hashicorp/vault/sdk/helper/pluginutil"
"github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/sdk/rotation"
) )
var ( var (
@@ -49,6 +51,8 @@ type DatabaseConfig struct {
// role-level by the role's skip_import_rotation field. The default is // role-level by the role's skip_import_rotation field. The default is
// false. Enterprise only. // false. Enterprise only.
SkipStaticRoleImportRotation bool `json:"skip_static_role_import_rotation" structs:"skip_static_role_import_rotation" mapstructure:"skip_static_role_import_rotation"` SkipStaticRoleImportRotation bool `json:"skip_static_role_import_rotation" structs:"skip_static_role_import_rotation" mapstructure:"skip_static_role_import_rotation"`
automatedrotationutil.AutomatedRotationParams
} }
// ConnectionDetails represents the DatabaseConfig.ConnectionDetails map as a // ConnectionDetails represents the DatabaseConfig.ConnectionDetails map as a
@@ -263,6 +267,7 @@ func pathConfigurePluginConnection(b *databaseBackend) *framework.Path {
}, },
} }
AddConnectionFieldsEnt(fields) AddConnectionFieldsEnt(fields)
automatedrotationutil.AddAutomatedRotationFields(fields)
return &framework.Path{ return &framework.Path{
Pattern: fmt.Sprintf("config/%s", framework.GenericNameRegex("name")), Pattern: fmt.Sprintf("config/%s", framework.GenericNameRegex("name")),
@@ -409,6 +414,7 @@ func (b *databaseBackend) connectionReadHandler() framework.OperationFunc {
} }
resp.Data = structs.New(config).Map() resp.Data = structs.New(config).Map()
config.PopulateAutomatedRotationData(resp.Data)
return resp, nil return resp, nil
} }
} }
@@ -500,6 +506,10 @@ func (b *databaseBackend) connectionWriteHandler() framework.OperationFunc {
config.SkipStaticRoleImportRotation = skipImportRotationRaw.(bool) config.SkipStaticRoleImportRotation = skipImportRotationRaw.(bool)
} }
if err := config.ParseAutomatedRotationFields(data); err != nil {
return logical.ErrorResponse(err.Error()), nil
}
// Remove these entries from the data before we store it keyed under // Remove these entries from the data before we store it keyed under
// ConnectionDetails. // ConnectionDetails.
delete(data.Raw, "name") delete(data.Raw, "name")
@@ -510,6 +520,10 @@ func (b *databaseBackend) connectionWriteHandler() framework.OperationFunc {
delete(data.Raw, "root_rotation_statements") delete(data.Raw, "root_rotation_statements")
delete(data.Raw, "password_policy") delete(data.Raw, "password_policy")
delete(data.Raw, "skip_static_role_import_rotation") delete(data.Raw, "skip_static_role_import_rotation")
delete(data.Raw, "rotation_schedule")
delete(data.Raw, "rotation_window")
delete(data.Raw, "rotation_ttl")
delete(data.Raw, "disable_automated_rotation")
id, err := uuid.GenerateUUID() id, err := uuid.GenerateUUID()
if err != nil { if err != nil {
@@ -560,6 +574,36 @@ func (b *databaseBackend) connectionWriteHandler() framework.OperationFunc {
oldConn.Close() oldConn.Close()
} }
var performedRotationManagerOpern string
if config.ShouldDeregisterRotationJob() {
performedRotationManagerOpern = rotation.PerformedDeregistration
// Disable Automated Rotation and Deregister credentials if required
deregisterReq := &rotation.RotationJobDeregisterRequest{
MountPoint: req.MountPoint,
ReqPath: req.Path,
}
b.Logger().Debug("Deregistering rotation job", "mount", req.MountPoint+req.Path)
if err := b.System().DeregisterRotationJob(ctx, deregisterReq); err != nil {
return logical.ErrorResponse("error deregistering rotation job: %s", err), nil
}
} else if config.ShouldRegisterRotationJob() {
performedRotationManagerOpern = rotation.PerformedRegistration
// Register the rotation job if it's required.
cfgReq := &rotation.RotationJobConfigureRequest{
MountPoint: req.MountPoint,
ReqPath: req.Path,
RotationSchedule: config.RotationSchedule,
RotationWindow: config.RotationWindow,
RotationPeriod: config.RotationPeriod,
}
b.Logger().Debug("Registering rotation job", "mount", req.MountPoint+req.Path)
if _, err = b.System().RegisterRotationJob(ctx, cfgReq); err != nil {
return logical.ErrorResponse("error registering rotation job: %s", err), nil
}
}
// 1.12.0 and 1.12.1 stored builtin plugins in storage, but 1.12.2 reverted // 1.12.0 and 1.12.1 stored builtin plugins in storage, but 1.12.2 reverted
// that, so clean up any pre-existing stored builtin versions on write. // that, so clean up any pre-existing stored builtin versions on write.
if versions.IsBuiltinVersion(config.PluginVersion) { if versions.IsBuiltinVersion(config.PluginVersion) {
@@ -567,7 +611,15 @@ func (b *databaseBackend) connectionWriteHandler() framework.OperationFunc {
} }
err = storeConfig(ctx, req.Storage, name, config) err = storeConfig(ctx, req.Storage, name, config)
if err != nil { if err != nil {
return nil, err b.Logger().Error("write to storage failed but the rotation manager still succeeded.",
"operation", performedRotationManagerOpern, "mount", req.MountPoint, "path", req.Path)
wrappedError := err
if performedRotationManagerOpern != "" {
wrappedError = fmt.Errorf("write to storage failed but the rotation manager still succeeded; "+
"operation=%s, mount=%s, path=%s, storageError=%s", performedRotationManagerOpern, req.MountPoint, req.Path, err)
}
return nil, wrappedError
} }
resp := &logical.Response{} resp := &logical.Response{}

View File

@@ -77,117 +77,121 @@ func pathRotateRootCredentials(b *databaseBackend) []*framework.Path {
func (b *databaseBackend) pathRotateRootCredentialsUpdate() framework.OperationFunc { func (b *databaseBackend) pathRotateRootCredentialsUpdate() framework.OperationFunc {
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (resp *logical.Response, err error) { return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (resp *logical.Response, err error) {
name := data.Get("name").(string) name := data.Get("name").(string)
modified := false return b.rotateRootCredentials(ctx, req, name)
defer func() {
if err == nil {
b.dbEvent(ctx, "rotate-root", req.Path, name, modified)
} else {
b.dbEvent(ctx, "rotate-root-fail", req.Path, name, modified)
}
}()
if name == "" {
return logical.ErrorResponse(respErrEmptyName), nil
}
config, err := b.DatabaseConfig(ctx, req.Storage, name)
if err != nil {
return nil, err
}
rootUsername, ok := config.ConnectionDetails["username"].(string)
if !ok || rootUsername == "" {
return nil, fmt.Errorf("unable to rotate root credentials: no username in configuration")
}
rootPassword, ok := config.ConnectionDetails["password"].(string)
if !ok || rootPassword == "" {
return nil, fmt.Errorf("unable to rotate root credentials: no password in configuration")
}
dbi, err := b.GetConnection(ctx, req.Storage, name)
if err != nil {
return nil, err
}
// Take the write lock on the instance
dbi.Lock()
defer func() {
dbi.Unlock()
// Even on error, still remove the connection
b.ClearConnectionId(name, dbi.id)
}()
defer func() {
// Close the plugin
dbi.closed = true
if err := dbi.database.Close(); err != nil {
b.Logger().Error("error closing the database plugin connection", "err", err)
}
}()
generator, err := newPasswordGenerator(nil)
if err != nil {
return nil, fmt.Errorf("failed to construct credential generator: %s", err)
}
generator.PasswordPolicy = config.PasswordPolicy
// Generate new credentials
oldPassword := config.ConnectionDetails["password"].(string)
newPassword, err := generator.generate(ctx, b, dbi.database)
if err != nil {
b.CloseIfShutdown(dbi, err)
return nil, fmt.Errorf("failed to generate password: %s", err)
}
config.ConnectionDetails["password"] = newPassword
// Write a WAL entry
walID, err := framework.PutWAL(ctx, req.Storage, rotateRootWALKey, &rotateRootCredentialsWAL{
ConnectionName: name,
UserName: rootUsername,
OldPassword: oldPassword,
NewPassword: newPassword,
})
if err != nil {
return nil, err
}
updateReq := v5.UpdateUserRequest{
Username: rootUsername,
CredentialType: v5.CredentialTypePassword,
Password: &v5.ChangePassword{
NewPassword: newPassword,
Statements: v5.Statements{
Commands: config.RootCredentialsRotateStatements,
},
},
}
newConfigDetails, err := dbi.database.UpdateUser(ctx, updateReq, true)
if err != nil {
return nil, fmt.Errorf("failed to update user: %w", err)
}
if newConfigDetails != nil {
config.ConnectionDetails = newConfigDetails
}
modified = true
// 1.12.0 and 1.12.1 stored builtin plugins in storage, but 1.12.2 reverted
// that, so clean up any pre-existing stored builtin versions on write.
if versions.IsBuiltinVersion(config.PluginVersion) {
config.PluginVersion = ""
}
err = storeConfig(ctx, req.Storage, name, config)
if err != nil {
return nil, err
}
err = framework.DeleteWAL(ctx, req.Storage, walID)
if err != nil {
b.Logger().Warn("unable to delete WAL", "error", err, "WAL ID", walID)
}
return nil, nil
} }
} }
func (b *databaseBackend) rotateRootCredentials(ctx context.Context, req *logical.Request, name string) (resp *logical.Response, err error) {
if name == "" {
return logical.ErrorResponse(respErrEmptyName), nil
}
modified := false
defer func() {
if err == nil {
b.dbEvent(ctx, "rotate-root", req.Path, name, modified)
} else {
b.dbEvent(ctx, "rotate-root-fail", req.Path, name, modified)
}
}()
config, err := b.DatabaseConfig(ctx, req.Storage, name)
if err != nil {
return nil, err
}
rootUsername, ok := config.ConnectionDetails["username"].(string)
if !ok || rootUsername == "" {
return nil, fmt.Errorf("unable to rotate root credentials: no username in configuration")
}
rootPassword, ok := config.ConnectionDetails["password"].(string)
if !ok || rootPassword == "" {
return nil, fmt.Errorf("unable to rotate root credentials: no password in configuration")
}
dbi, err := b.GetConnection(ctx, req.Storage, name)
if err != nil {
return nil, err
}
// Take the write lock on the instance
dbi.Lock()
defer func() {
dbi.Unlock()
// Even on error, still remove the connection
b.ClearConnectionId(name, dbi.id)
}()
defer func() {
// Close the plugin
dbi.closed = true
if err := dbi.database.Close(); err != nil {
b.Logger().Error("error closing the database plugin connection", "err", err)
}
}()
generator, err := newPasswordGenerator(nil)
if err != nil {
return nil, fmt.Errorf("failed to construct credential generator: %s", err)
}
generator.PasswordPolicy = config.PasswordPolicy
// Generate new credentials
oldPassword := config.ConnectionDetails["password"].(string)
newPassword, err := generator.generate(ctx, b, dbi.database)
if err != nil {
b.CloseIfShutdown(dbi, err)
return nil, fmt.Errorf("failed to generate password: %s", err)
}
config.ConnectionDetails["password"] = newPassword
// Write a WAL entry
walID, err := framework.PutWAL(ctx, req.Storage, rotateRootWALKey, &rotateRootCredentialsWAL{
ConnectionName: name,
UserName: rootUsername,
OldPassword: oldPassword,
NewPassword: newPassword,
})
if err != nil {
return nil, err
}
updateReq := v5.UpdateUserRequest{
Username: rootUsername,
CredentialType: v5.CredentialTypePassword,
Password: &v5.ChangePassword{
NewPassword: newPassword,
Statements: v5.Statements{
Commands: config.RootCredentialsRotateStatements,
},
},
}
newConfigDetails, err := dbi.database.UpdateUser(ctx, updateReq, true)
if err != nil {
return nil, fmt.Errorf("failed to update user: %w", err)
}
if newConfigDetails != nil {
config.ConnectionDetails = newConfigDetails
}
modified = true
// 1.12.0 and 1.12.1 stored builtin plugins in storage, but 1.12.2 reverted
// that, so clean up any pre-existing stored builtin versions on write.
if versions.IsBuiltinVersion(config.PluginVersion) {
config.PluginVersion = ""
}
err = storeConfig(ctx, req.Storage, name, config)
if err != nil {
return nil, err
}
err = framework.DeleteWAL(ctx, req.Storage, walID)
if err != nil {
b.Logger().Warn("unable to delete WAL", "error", err, "WAL ID", walID)
}
return nil, nil
}
func (b *databaseBackend) pathRotateRoleCredentialsUpdate() framework.OperationFunc { func (b *databaseBackend) pathRotateRoleCredentialsUpdate() framework.OperationFunc {
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (_ *logical.Response, err error) { return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (_ *logical.Response, err error) {
name := data.Get("name").(string) name := data.Get("name").(string)

View File

@@ -1,5 +1,11 @@
```release-note:feature ```release-note:feature
**Automated Root Rotation**: Adds Automated Root Rotation capabilities to the AWS Auth, AWS Secrets **Automated Root Rotation**: Adds Automated Root Rotation capabilities to the AWS Auth and AWS Secrets
and DB Secrets plugins. This allows plugin users to automate their root credential rotations based plugins. This allows plugin users to automate their root credential rotations based on configurable
on configurable schedules/periods via the Rotation Manager. Note: Enterprise only. schedules/periods via the Rotation Manager. Note: Enterprise only.
```
```release-note:change
secrets/aws: The AWS Secrets engine now persists entries to storage between writes. This enables users
to not have to pass every required field on each write and to make individual updates as necessary.
Note: in order to zero out a value that is previously configured, users must now explicitly set the
field to its zero value on an update.
``` ```

5
changelog/29557.txt Normal file
View File

@@ -0,0 +1,5 @@
```release-note:feature
**Automated Root Rotation**: Adds Automated Root Rotation capabilities to the DB Secrets plugin.
This allows plugin users to automate their root credential rotations based on configurable
schedules/periods via the Rotation Manager. Note: Enterprise only.
```

View File

@@ -63,8 +63,8 @@ func (p *AutomatedRotationParams) ParseAutomatedRotationFields(d *framework.Fiel
p.RotationPeriod = rotationPeriodRaw.(int) p.RotationPeriod = rotationPeriodRaw.(int)
} }
if (scheduleOk && !windowOk) || (windowOk && !scheduleOk) { if windowOk && !scheduleOk {
return fmt.Errorf("must include both schedule and window") return fmt.Errorf("cannot use rotation_window without rotation_schedule")
} }
p.DisableAutomatedRotation = d.Get("disable_automated_rotation").(bool) p.DisableAutomatedRotation = d.Get("disable_automated_rotation").(bool)

View File

@@ -86,6 +86,16 @@ func TestParseAutomatedRotationFields(t *testing.T) {
}, },
expectedError: "rotation_window does not apply to period", expectedError: "rotation_window does not apply to period",
}, },
{
name: "window-without-schedule",
data: &framework.FieldData{
Raw: map[string]interface{}{
"rotation_window": 60,
},
Schema: schemaMap,
},
expectedError: "cannot use rotation_window without rotation_schedule",
},
} }
for _, tt := range tests { for _, tt := range tests {

View File

@@ -10,6 +10,11 @@ import (
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
) )
const (
PerformedRegistration = "registration"
PerformedDeregistration = "deregistration"
)
// RotationOptions is an embeddable struct to capture common rotation // RotationOptions is an embeddable struct to capture common rotation
// settings between a Secret and Auth // settings between a Secret and Auth
type RotationOptions struct { type RotationOptions struct {