mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 17:52:32 +00:00
Add automated root rotation support to DB Secrets (#29557)
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
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)
|
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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user