Add support code for auth/ldap root autorotation (#29535)

---------

Co-authored-by: Sarah Chavis <62406755+schavis@users.noreply.github.com>
This commit is contained in:
kpcraig
2025-02-13 16:06:47 -05:00
committed by GitHub
parent 6efe9c7142
commit db0d911683
6 changed files with 144 additions and 19 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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"`
}

View File

@@ -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 = `

3
changelog/29535.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:feature
**Automated Root Rotation**: A schedule or ttl can be defined for automated rotation of the root credential.
```

View File

@@ -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'