mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 09:42:25 +00:00
Add automated root rotation support to DB Secrets (#29557)
This commit is contained in:
@@ -6,6 +6,7 @@ package awsauth
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
@@ -394,7 +395,7 @@ func (b *backend) pathConfigClientCreateUpdate(ctx context.Context, req *logical
|
||||
|
||||
var performedRotationManagerOpern string
|
||||
if configEntry.ShouldDeregisterRotationJob() {
|
||||
performedRotationManagerOpern = "deregistration"
|
||||
performedRotationManagerOpern = rotation.PerformedDeregistration
|
||||
// Disable Automated Rotation and Deregister credentials if required
|
||||
deregisterReq := &rotation.RotationJobDeregisterRequest{
|
||||
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
|
||||
}
|
||||
} else if configEntry.ShouldRegisterRotationJob() {
|
||||
performedRotationManagerOpern = "registration"
|
||||
performedRotationManagerOpern = rotation.PerformedRegistration
|
||||
// Register the rotation job if it's required.
|
||||
cfgReq := &rotation.RotationJobConfigureRequest{
|
||||
MountPoint: req.MountPoint,
|
||||
@@ -434,11 +435,15 @@ func (b *backend) pathConfigClientCreateUpdate(ctx context.Context, req *logical
|
||||
|
||||
if changedCreds || changedOtherConfig || req.Operation == logical.CreateOperation {
|
||||
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 != "" {
|
||||
b.Logger().Error("write to storage failed but the rotation manager still succeeded.",
|
||||
"operation", performedRotationManagerOpern, "mount", req.MountPoint, "path", req.Path)
|
||||
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, err
|
||||
return nil, wrappedError
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ package aws
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
@@ -252,7 +253,7 @@ func (b *backend) pathConfigRootWrite(ctx context.Context, req *logical.Request,
|
||||
|
||||
var performedRotationManagerOpern string
|
||||
if rc.ShouldDeregisterRotationJob() {
|
||||
performedRotationManagerOpern = "deregistration"
|
||||
performedRotationManagerOpern = rotation.PerformedDeregistration
|
||||
// Disable Automated Rotation and Deregister credentials if required
|
||||
deregisterReq := &rotation.RotationJobDeregisterRequest{
|
||||
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
|
||||
}
|
||||
} else if rc.ShouldRegisterRotationJob() {
|
||||
performedRotationManagerOpern = "registration"
|
||||
performedRotationManagerOpern = rotation.PerformedRegistration
|
||||
// Register the rotation job if it's required.
|
||||
cfgReq := &rotation.RotationJobConfigureRequest{
|
||||
MountPoint: req.MountPoint,
|
||||
@@ -282,11 +283,15 @@ func (b *backend) pathConfigRootWrite(ctx context.Context, req *logical.Request,
|
||||
|
||||
// Save the config
|
||||
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 != "" {
|
||||
b.Logger().Error("write to storage failed but the rotation manager still succeeded.",
|
||||
"operation", performedRotationManagerOpern, "mount", req.MountPoint, "path", req.Path)
|
||||
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, err
|
||||
return nil, wrappedError
|
||||
}
|
||||
|
||||
// clear possible cached IAM / STS clients after successfully updating
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/rpc"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -39,6 +40,8 @@ const (
|
||||
minRootCredRollbackAge = 1 * time.Minute
|
||||
)
|
||||
|
||||
var databaseConfigNameFromRotationIDRegex = regexp.MustCompile("^.+/config/(.+$)")
|
||||
|
||||
type dbPluginInstance struct {
|
||||
sync.RWMutex
|
||||
database databaseVersionWrapper
|
||||
@@ -127,6 +130,14 @@ func Backend(conf *logical.BackendConfig) *databaseBackend {
|
||||
WALRollback: b.walRollback,
|
||||
WALRollbackMinAge: minRootCredRollbackAge,
|
||||
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
|
||||
@@ -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 = `
|
||||
The database backend supports using many different databases
|
||||
as secret backends, including but not limited to:
|
||||
|
||||
@@ -213,6 +213,10 @@ func TestBackend_config_connection(t *testing.T) {
|
||||
"plugin_version": "",
|
||||
"verify_connection": false,
|
||||
"skip_static_role_import_rotation": false,
|
||||
"rotation_schedule": "",
|
||||
"rotation_period": 0,
|
||||
"rotation_window": 0,
|
||||
"disable_automated_rotation": false,
|
||||
}
|
||||
configReq.Operation = logical.ReadOperation
|
||||
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, "AutomatedRotationParams")
|
||||
if !reflect.DeepEqual(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": "",
|
||||
"verify_connection": false,
|
||||
"skip_static_role_import_rotation": false,
|
||||
"rotation_schedule": "",
|
||||
"rotation_period": 0,
|
||||
"rotation_window": 0,
|
||||
"disable_automated_rotation": false,
|
||||
}
|
||||
configReq.Operation = logical.ReadOperation
|
||||
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, "AutomatedRotationParams")
|
||||
if !reflect.DeepEqual(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": "",
|
||||
"verify_connection": false,
|
||||
"skip_static_role_import_rotation": false,
|
||||
"rotation_schedule": "",
|
||||
"rotation_period": 0,
|
||||
"rotation_window": 0,
|
||||
"disable_automated_rotation": false,
|
||||
}
|
||||
configReq.Operation = logical.ReadOperation
|
||||
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, "AutomatedRotationParams")
|
||||
if !reflect.DeepEqual(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": "",
|
||||
"verify_connection": 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")
|
||||
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, "AutomatedRotationParams")
|
||||
if diff := deep.Equal(resp.Data, expected); diff != nil {
|
||||
t.Fatal(strings.Join(diff, "\n"))
|
||||
}
|
||||
|
||||
@@ -17,9 +17,11 @@ import (
|
||||
"github.com/hashicorp/vault/helper/versions"
|
||||
v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
|
||||
"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/pluginutil"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
"github.com/hashicorp/vault/sdk/rotation"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -49,6 +51,8 @@ type DatabaseConfig struct {
|
||||
// role-level by the role's skip_import_rotation field. The default is
|
||||
// false. Enterprise only.
|
||||
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
|
||||
@@ -263,6 +267,7 @@ func pathConfigurePluginConnection(b *databaseBackend) *framework.Path {
|
||||
},
|
||||
}
|
||||
AddConnectionFieldsEnt(fields)
|
||||
automatedrotationutil.AddAutomatedRotationFields(fields)
|
||||
|
||||
return &framework.Path{
|
||||
Pattern: fmt.Sprintf("config/%s", framework.GenericNameRegex("name")),
|
||||
@@ -409,6 +414,7 @@ func (b *databaseBackend) connectionReadHandler() framework.OperationFunc {
|
||||
}
|
||||
|
||||
resp.Data = structs.New(config).Map()
|
||||
config.PopulateAutomatedRotationData(resp.Data)
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
@@ -500,6 +506,10 @@ func (b *databaseBackend) connectionWriteHandler() framework.OperationFunc {
|
||||
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
|
||||
// ConnectionDetails.
|
||||
delete(data.Raw, "name")
|
||||
@@ -510,6 +520,10 @@ func (b *databaseBackend) connectionWriteHandler() framework.OperationFunc {
|
||||
delete(data.Raw, "root_rotation_statements")
|
||||
delete(data.Raw, "password_policy")
|
||||
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()
|
||||
if err != nil {
|
||||
@@ -560,6 +574,36 @@ func (b *databaseBackend) connectionWriteHandler() framework.OperationFunc {
|
||||
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
|
||||
// that, so clean up any pre-existing stored builtin versions on write.
|
||||
if versions.IsBuiltinVersion(config.PluginVersion) {
|
||||
@@ -567,7 +611,15 @@ func (b *databaseBackend) connectionWriteHandler() framework.OperationFunc {
|
||||
}
|
||||
err = storeConfig(ctx, req.Storage, name, config)
|
||||
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{}
|
||||
|
||||
@@ -77,117 +77,121 @@ func pathRotateRootCredentials(b *databaseBackend) []*framework.Path {
|
||||
func (b *databaseBackend) pathRotateRootCredentialsUpdate() framework.OperationFunc {
|
||||
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (resp *logical.Response, err error) {
|
||||
name := data.Get("name").(string)
|
||||
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)
|
||||
}
|
||||
}()
|
||||
|
||||
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
|
||||
return b.rotateRootCredentials(ctx, req, name)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (_ *logical.Response, err error) {
|
||||
name := data.Get("name").(string)
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
```release-note:feature
|
||||
**Automated Root Rotation**: Adds Automated Root Rotation capabilities to the AWS Auth, AWS Secrets
|
||||
and DB Secrets plugins. This allows plugin users to automate their root credential rotations based
|
||||
on configurable schedules/periods via the Rotation Manager. Note: Enterprise only.
|
||||
**Automated Root Rotation**: Adds Automated Root Rotation capabilities to the AWS Auth and AWS Secrets
|
||||
plugins. This allows plugin users to automate their root credential rotations based on configurable
|
||||
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
5
changelog/29557.txt
Normal 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.
|
||||
```
|
||||
@@ -63,8 +63,8 @@ func (p *AutomatedRotationParams) ParseAutomatedRotationFields(d *framework.Fiel
|
||||
p.RotationPeriod = rotationPeriodRaw.(int)
|
||||
}
|
||||
|
||||
if (scheduleOk && !windowOk) || (windowOk && !scheduleOk) {
|
||||
return fmt.Errorf("must include both schedule and window")
|
||||
if windowOk && !scheduleOk {
|
||||
return fmt.Errorf("cannot use rotation_window without rotation_schedule")
|
||||
}
|
||||
|
||||
p.DisableAutomatedRotation = d.Get("disable_automated_rotation").(bool)
|
||||
|
||||
@@ -86,6 +86,16 @@ func TestParseAutomatedRotationFields(t *testing.T) {
|
||||
},
|
||||
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 {
|
||||
|
||||
@@ -10,6 +10,11 @@ import (
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
PerformedRegistration = "registration"
|
||||
PerformedDeregistration = "deregistration"
|
||||
)
|
||||
|
||||
// RotationOptions is an embeddable struct to capture common rotation
|
||||
// settings between a Secret and Auth
|
||||
type RotationOptions struct {
|
||||
|
||||
Reference in New Issue
Block a user