Skip to content

Add support code for auth/ldap root autorotation #29535

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions builtin/credential/ldap/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 26 additions & 4 deletions builtin/credential/ldap/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
65 changes: 63 additions & 2 deletions builtin/credential/ldap/path_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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"`
}
Expand Down
39 changes: 28 additions & 11 deletions builtin/credential/ldap/path_config_rotate_root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package ldap

import (
"context"
"errors"

"github.com/go-ldap/ldap/v3"
"github.com/hashicorp/vault/sdk/framework"
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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{
Expand All @@ -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 = `
Expand Down
3 changes: 3 additions & 0 deletions changelog/29535.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
**Automated Root Rotation**: A schedule or ttl can be defined for automated rotation of the root credential.
```
21 changes: 21 additions & 0 deletions website/content/api-docs/auth/ldap.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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)` – <EnterpriseAlert product="vault" inline />
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: "")` – <EnterpriseAlert product="vault" inline />
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)` – <EnterpriseAlert product="vault" inline />
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)` - <EnterpriseAlert product="vault" inline />
Cancels all upcoming rotations of the root credential until unset.

@include 'tokenfields.mdx'

Expand Down
Loading