diff --git a/builtin/credential/ldap/backend.go b/builtin/credential/ldap/backend.go
index 9bdb6f5673..f4c2c51eb1 100644
--- a/builtin/credential/ldap/backend.go
+++ b/builtin/credential/ldap/backend.go
@@ -55,8 +55,9 @@ func Backend() *backend {
pathConfigRotateRoot(&b),
},
- AuthRenew: b.pathLoginRenew,
- BackendType: logical.TypeCredential,
+ AuthRenew: b.pathLoginRenew,
+ BackendType: logical.TypeCredential,
+ RotateCredential: b.rotateRootCredential,
}
return &b
diff --git a/builtin/credential/ldap/backend_test.go b/builtin/credential/ldap/backend_test.go
index c1b84c82a9..a8cd549c43 100644
--- a/builtin/credential/ldap/backend_test.go
+++ b/builtin/credential/ldap/backend_test.go
@@ -11,6 +11,9 @@ import (
"testing"
"time"
+ "github.com/hashicorp/vault/sdk/helper/automatedrotationutil"
+ "github.com/hashicorp/vault/sdk/rotation"
+
goldap "github.com/go-ldap/ldap/v3"
"github.com/go-test/deep"
hclog "github.com/hashicorp/go-hclog"
@@ -25,9 +28,26 @@ import (
"github.com/mitchellh/mapstructure"
)
+type testSystemView struct {
+ logical.StaticSystemView
+}
+
+func (d testSystemView) RegisterRotationJob(_ context.Context, _ *rotation.RotationJobConfigureRequest) (string, error) {
+ return "", automatedrotationutil.ErrRotationManagerUnsupported
+}
+
+func (d testSystemView) DeregisterRotationJob(_ context.Context, _ *rotation.RotationJobDeregisterRequest) error {
+ return nil
+}
+
func createBackendWithStorage(t *testing.T) (*backend, logical.Storage) {
+ sv := testSystemView{}
+ sv.MaxLeaseTTLVal = time.Hour * 2 * 24
+ sv.DefaultLeaseTTLVal = time.Minute
+
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
+ config.System = sv
b := Backend()
if b == nil {
@@ -402,15 +422,17 @@ func TestLdapAuthBackend_UserPolicies(t *testing.T) {
func factory(t *testing.T) logical.Backend {
defaultLeaseTTLVal := time.Hour * 24
maxLeaseTTLVal := time.Hour * 24 * 32
+
+ sv := testSystemView{}
+ sv.DefaultLeaseTTLVal = defaultLeaseTTLVal
+ sv.MaxLeaseTTLVal = maxLeaseTTLVal
+
b, err := Factory(context.Background(), &logical.BackendConfig{
Logger: hclog.New(&hclog.LoggerOptions{
Name: "FactoryLogger",
Level: hclog.Debug,
}),
- System: &logical.StaticSystemView{
- DefaultLeaseTTLVal: defaultLeaseTTLVal,
- MaxLeaseTTLVal: maxLeaseTTLVal,
- },
+ System: sv,
})
if err != nil {
t.Fatalf("Unable to create backend: %s", err)
diff --git a/builtin/credential/ldap/path_config.go b/builtin/credential/ldap/path_config.go
index e24d04b295..d2f2553f55 100644
--- a/builtin/credential/ldap/path_config.go
+++ b/builtin/credential/ldap/path_config.go
@@ -5,17 +5,22 @@ package ldap
import (
"context"
+ "fmt"
"strings"
"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/ldaputil"
"github.com/hashicorp/vault/sdk/helper/tokenutil"
"github.com/hashicorp/vault/sdk/logical"
+ "github.com/hashicorp/vault/sdk/rotation"
)
const userFilterWarning = "userfilter configured does not consider userattr and may result in colliding entity aliases on logins"
+const rootRotationJobName = "ldap-auth-root-creds"
+
func pathConfig(b *backend) *framework.Path {
p := &framework.Path{
Pattern: `config`,
@@ -54,6 +59,8 @@ func pathConfig(b *backend) *framework.Path {
Description: "Password policy to use to rotate the root password",
}
+ automatedrotationutil.AddAutomatedRotationFields(p.Fields)
+
return p
}
@@ -137,6 +144,8 @@ func (b *backend) pathConfigRead(ctx context.Context, req *logical.Request, d *f
data := cfg.PasswordlessMap()
cfg.PopulateTokenData(data)
+ cfg.PopulateAutomatedRotationData(data)
+
data["password_policy"] = cfg.PasswordPolicy
resp := &logical.Response{
@@ -207,16 +216,67 @@ func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, d *
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}
+ if err := cfg.ParseAutomatedRotationFields(d); err != nil {
+ return nil, err
+ }
+
if passwordPolicy, ok := d.GetOk("password_policy"); ok {
cfg.PasswordPolicy = passwordPolicy.(string)
}
+ var rotOp string
+ if cfg.ShouldDeregisterRotationJob() {
+ rotOp = rotation.PerformedDeregistration
+ dr := &rotation.RotationJobDeregisterRequest{
+ MountPoint: req.MountPoint,
+ ReqPath: req.Path,
+ }
+
+ err := b.System().DeregisterRotationJob(ctx, dr)
+ if err != nil {
+ return logical.ErrorResponse("error de-registering rotation job: %s", err), nil
+ }
+ } else if cfg.ShouldRegisterRotationJob() {
+ rotOp = rotation.PerformedRegistration
+ // Now that the root config is set up, register the rotation job if it's required.
+ r := &rotation.RotationJobConfigureRequest{
+ Name: rootRotationJobName,
+ MountPoint: req.MountPoint,
+ ReqPath: req.Path,
+ RotationSchedule: cfg.RotationSchedule,
+ RotationWindow: cfg.RotationWindow,
+ RotationPeriod: cfg.RotationPeriod,
+ }
+
+ b.Logger().Debug("registering rotation job", "mount", r.MountPoint, "path", r.ReqPath)
+ _, err = b.System().RegisterRotationJob(ctx, r)
+ if err != nil {
+ return logical.ErrorResponse("error registering rotation job: %s", err), nil
+ }
+ }
+
+ wrapRotationError := func(innerError error) error {
+ b.Logger().Error("write to storage failed but the rotation manager still succeeded.",
+ "operation", rotOp, "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", rotOp, req.MountPoint, req.Path, err)
+ return wrappedError
+ }
+
entry, err := logical.StorageEntryJSON("config", cfg)
if err != nil {
- return nil, err
+ var wrappedError error
+ if rotOp != "" {
+ wrappedError = wrapRotationError(err)
+ }
+ return nil, wrappedError
}
if err := req.Storage.Put(ctx, entry); err != nil {
- return nil, err
+ var wrappedError error
+ if rotOp != "" {
+ wrappedError = wrapRotationError(err)
+ }
+ return nil, wrappedError
}
if warnings := b.checkConfigUserFilter(cfg); len(warnings) > 0 {
@@ -251,6 +311,7 @@ func (b *backend) getConfigFieldData() (*framework.FieldData, error) {
type ldapConfigEntry struct {
tokenutil.TokenParams
*ldaputil.ConfigEntry
+ automatedrotationutil.AutomatedRotationParams
PasswordPolicy string `json:"password_policy"`
}
diff --git a/builtin/credential/ldap/path_config_rotate_root.go b/builtin/credential/ldap/path_config_rotate_root.go
index 6d30e1d3d3..132abf0172 100644
--- a/builtin/credential/ldap/path_config_rotate_root.go
+++ b/builtin/credential/ldap/path_config_rotate_root.go
@@ -5,6 +5,7 @@ package ldap
import (
"context"
+ "errors"
"github.com/go-ldap/ldap/v3"
"github.com/hashicorp/vault/sdk/framework"
@@ -36,17 +37,33 @@ func pathConfigRotateRoot(b *backend) *framework.Path {
}
}
-func (b *backend) pathConfigRotateRootUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
+func (b *backend) pathConfigRotateRootUpdate(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
+ err := b.rotateRootCredential(ctx, req)
+ var responseError responseError
+ if errors.As(err, &responseError) {
+ return logical.ErrorResponse(responseError.Error()), nil
+ }
+
+ // naturally this is `nil, nil` if the err is nil
+ return nil, err
+}
+
+// responseError exists to capture the cases in the old rotate call that returned specific error responses
+type responseError struct {
+ error
+}
+
+func (b *backend) rotateRootCredential(ctx context.Context, req *logical.Request) error {
// lock the backend's state - really just the config state - for mutating
b.mu.Lock()
defer b.mu.Unlock()
cfg, err := b.Config(ctx, req)
if err != nil {
- return nil, err
+ return err
}
if cfg == nil {
- return logical.ErrorResponse("attempted to rotate root on an undefined config"), nil
+ return responseError{errors.New("attempted to rotate root on an undefined config")}
}
u, p := cfg.BindDN, cfg.BindPassword
@@ -55,7 +72,7 @@ func (b *backend) pathConfigRotateRootUpdate(ctx context.Context, req *logical.R
if b.Logger().IsDebug() {
b.Logger().Debug("auth is not using authenticated search, no root to rotate")
}
- return logical.ErrorResponse("auth is not using authenticated search, no root to rotate"), nil
+ return responseError{errors.New("auth is not using authenticated search, no root to rotate")}
}
// grab our ldap client
@@ -66,12 +83,12 @@ func (b *backend) pathConfigRotateRootUpdate(ctx context.Context, req *logical.R
conn, err := client.DialLDAP(cfg.ConfigEntry)
if err != nil {
- return nil, err
+ return err
}
err = conn.Bind(u, p)
if err != nil {
- return nil, err
+ return err
}
lreq := &ldap.ModifyRequest{
@@ -85,27 +102,27 @@ func (b *backend) pathConfigRotateRootUpdate(ctx context.Context, req *logical.R
newPassword, err = base62.Random(defaultPasswordLength)
}
if err != nil {
- return nil, err
+ return err
}
lreq.Replace("userPassword", []string{newPassword})
err = conn.Modify(lreq)
if err != nil {
- return nil, err
+ return err
}
// update config with new password
cfg.BindPassword = newPassword
entry, err := logical.StorageEntryJSON("config", cfg)
if err != nil {
- return nil, err
+ return err
}
if err := req.Storage.Put(ctx, entry); err != nil {
// we might have to roll-back the password here?
- return nil, err
+ return err
}
- return nil, nil
+ return nil
}
const pathConfigRotateRootHelpSyn = `
diff --git a/changelog/29535.txt b/changelog/29535.txt
new file mode 100644
index 0000000000..3adc28d599
--- /dev/null
+++ b/changelog/29535.txt
@@ -0,0 +1,3 @@
+```release-note:feature
+**Automated Root Rotation**: A schedule or ttl can be defined for automated rotation of the root credential.
+```
diff --git a/website/content/api-docs/auth/ldap.mdx b/website/content/api-docs/auth/ldap.mdx
index de204ae805..14ac7a0c79 100644
--- a/website/content/api-docs/auth/ldap.mdx
+++ b/website/content/api-docs/auth/ldap.mdx
@@ -108,6 +108,27 @@ This endpoint configures the LDAP auth method.
- `enable_samaccountname_login` `(bool: false)` - (Optional) Lets Active Directory
LDAP users log in using `sAMAccountName` or `userPrincipalName` when the
`upndomain` parameter is set.
+- `rotation_period` `(integer: 0)` –
+ The amount of time, in seconds,
+ Vault should wait before rotating the root credential. A zero value tells Vault
+ not to rotate the token. The minimum rotation period is 5 seconds. **You must
+ set one of `rotation_period` or `rotation_schedule`, but cannot set both**.
+- `rotation_schedule` `(string: "")` –
+ The schedule, in [cron-style time format](https://en.wikipedia.org/wiki/Cron),
+ defining the schedule on which Vault should rotate the root token. Standard
+ cron-style time format uses five fields to define the minute, hour, day of
+ month, month, and day of week respectively. For example, `0 0 * * SAT` tells
+ Vault to rotate the root token every Saturday at 00:00. **You must set one of
+ `rotation_schedule` or `rotation_period`, but cannot set both**.
+- `rotation_window` `(integer: 0)` –
+ The maximum amount of time, in seconds, allowed to complete
+ a rotation when a scheduled token rotation occurs. If Vault cannot rotate the
+ token within the window (for example, due to a failure), Vault must wait to
+ try again until the next scheduled rotation. The default rotation window is
+ unbound and the minimum allowable window is 1 hour. **You cannot set a rotation
+ window when using `rotation_period`**.
+- `disable_automated_rotation` `(bool: false)` -
+ Cancels all upcoming rotations of the root credential until unset.
@include 'tokenfields.mdx'