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'