mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 17:52:32 +00:00
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:
@@ -55,8 +55,9 @@ func Backend() *backend {
|
|||||||
pathConfigRotateRoot(&b),
|
pathConfigRotateRoot(&b),
|
||||||
},
|
},
|
||||||
|
|
||||||
AuthRenew: b.pathLoginRenew,
|
AuthRenew: b.pathLoginRenew,
|
||||||
BackendType: logical.TypeCredential,
|
BackendType: logical.TypeCredential,
|
||||||
|
RotateCredential: b.rotateRootCredential,
|
||||||
}
|
}
|
||||||
|
|
||||||
return &b
|
return &b
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/sdk/helper/automatedrotationutil"
|
||||||
|
"github.com/hashicorp/vault/sdk/rotation"
|
||||||
|
|
||||||
goldap "github.com/go-ldap/ldap/v3"
|
goldap "github.com/go-ldap/ldap/v3"
|
||||||
"github.com/go-test/deep"
|
"github.com/go-test/deep"
|
||||||
hclog "github.com/hashicorp/go-hclog"
|
hclog "github.com/hashicorp/go-hclog"
|
||||||
@@ -25,9 +28,26 @@ import (
|
|||||||
"github.com/mitchellh/mapstructure"
|
"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) {
|
func createBackendWithStorage(t *testing.T) (*backend, logical.Storage) {
|
||||||
|
sv := testSystemView{}
|
||||||
|
sv.MaxLeaseTTLVal = time.Hour * 2 * 24
|
||||||
|
sv.DefaultLeaseTTLVal = time.Minute
|
||||||
|
|
||||||
config := logical.TestBackendConfig()
|
config := logical.TestBackendConfig()
|
||||||
config.StorageView = &logical.InmemStorage{}
|
config.StorageView = &logical.InmemStorage{}
|
||||||
|
config.System = sv
|
||||||
|
|
||||||
b := Backend()
|
b := Backend()
|
||||||
if b == nil {
|
if b == nil {
|
||||||
@@ -402,15 +422,17 @@ func TestLdapAuthBackend_UserPolicies(t *testing.T) {
|
|||||||
func factory(t *testing.T) logical.Backend {
|
func factory(t *testing.T) logical.Backend {
|
||||||
defaultLeaseTTLVal := time.Hour * 24
|
defaultLeaseTTLVal := time.Hour * 24
|
||||||
maxLeaseTTLVal := time.Hour * 24 * 32
|
maxLeaseTTLVal := time.Hour * 24 * 32
|
||||||
|
|
||||||
|
sv := testSystemView{}
|
||||||
|
sv.DefaultLeaseTTLVal = defaultLeaseTTLVal
|
||||||
|
sv.MaxLeaseTTLVal = maxLeaseTTLVal
|
||||||
|
|
||||||
b, err := Factory(context.Background(), &logical.BackendConfig{
|
b, err := Factory(context.Background(), &logical.BackendConfig{
|
||||||
Logger: hclog.New(&hclog.LoggerOptions{
|
Logger: hclog.New(&hclog.LoggerOptions{
|
||||||
Name: "FactoryLogger",
|
Name: "FactoryLogger",
|
||||||
Level: hclog.Debug,
|
Level: hclog.Debug,
|
||||||
}),
|
}),
|
||||||
System: &logical.StaticSystemView{
|
System: sv,
|
||||||
DefaultLeaseTTLVal: defaultLeaseTTLVal,
|
|
||||||
MaxLeaseTTLVal: maxLeaseTTLVal,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unable to create backend: %s", err)
|
t.Fatalf("Unable to create backend: %s", err)
|
||||||
|
|||||||
@@ -5,17 +5,22 @@ package ldap
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"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/ldaputil"
|
"github.com/hashicorp/vault/sdk/helper/ldaputil"
|
||||||
"github.com/hashicorp/vault/sdk/helper/tokenutil"
|
"github.com/hashicorp/vault/sdk/helper/tokenutil"
|
||||||
"github.com/hashicorp/vault/sdk/logical"
|
"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 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 {
|
func pathConfig(b *backend) *framework.Path {
|
||||||
p := &framework.Path{
|
p := &framework.Path{
|
||||||
Pattern: `config`,
|
Pattern: `config`,
|
||||||
@@ -54,6 +59,8 @@ func pathConfig(b *backend) *framework.Path {
|
|||||||
Description: "Password policy to use to rotate the root password",
|
Description: "Password policy to use to rotate the root password",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
automatedrotationutil.AddAutomatedRotationFields(p.Fields)
|
||||||
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +144,8 @@ func (b *backend) pathConfigRead(ctx context.Context, req *logical.Request, d *f
|
|||||||
|
|
||||||
data := cfg.PasswordlessMap()
|
data := cfg.PasswordlessMap()
|
||||||
cfg.PopulateTokenData(data)
|
cfg.PopulateTokenData(data)
|
||||||
|
cfg.PopulateAutomatedRotationData(data)
|
||||||
|
|
||||||
data["password_policy"] = cfg.PasswordPolicy
|
data["password_policy"] = cfg.PasswordPolicy
|
||||||
|
|
||||||
resp := &logical.Response{
|
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
|
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 {
|
if passwordPolicy, ok := d.GetOk("password_policy"); ok {
|
||||||
cfg.PasswordPolicy = passwordPolicy.(string)
|
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)
|
entry, err := logical.StorageEntryJSON("config", cfg)
|
||||||
if err != nil {
|
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 {
|
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 {
|
if warnings := b.checkConfigUserFilter(cfg); len(warnings) > 0 {
|
||||||
@@ -251,6 +311,7 @@ func (b *backend) getConfigFieldData() (*framework.FieldData, error) {
|
|||||||
type ldapConfigEntry struct {
|
type ldapConfigEntry struct {
|
||||||
tokenutil.TokenParams
|
tokenutil.TokenParams
|
||||||
*ldaputil.ConfigEntry
|
*ldaputil.ConfigEntry
|
||||||
|
automatedrotationutil.AutomatedRotationParams
|
||||||
|
|
||||||
PasswordPolicy string `json:"password_policy"`
|
PasswordPolicy string `json:"password_policy"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package ldap
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"github.com/go-ldap/ldap/v3"
|
"github.com/go-ldap/ldap/v3"
|
||||||
"github.com/hashicorp/vault/sdk/framework"
|
"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
|
// lock the backend's state - really just the config state - for mutating
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
cfg, err := b.Config(ctx, req)
|
cfg, err := b.Config(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
if cfg == nil {
|
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
|
u, p := cfg.BindDN, cfg.BindPassword
|
||||||
@@ -55,7 +72,7 @@ func (b *backend) pathConfigRotateRootUpdate(ctx context.Context, req *logical.R
|
|||||||
if b.Logger().IsDebug() {
|
if b.Logger().IsDebug() {
|
||||||
b.Logger().Debug("auth is not using authenticated search, no root to rotate")
|
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
|
// grab our ldap client
|
||||||
@@ -66,12 +83,12 @@ func (b *backend) pathConfigRotateRootUpdate(ctx context.Context, req *logical.R
|
|||||||
|
|
||||||
conn, err := client.DialLDAP(cfg.ConfigEntry)
|
conn, err := client.DialLDAP(cfg.ConfigEntry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = conn.Bind(u, p)
|
err = conn.Bind(u, p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
lreq := &ldap.ModifyRequest{
|
lreq := &ldap.ModifyRequest{
|
||||||
@@ -85,27 +102,27 @@ func (b *backend) pathConfigRotateRootUpdate(ctx context.Context, req *logical.R
|
|||||||
newPassword, err = base62.Random(defaultPasswordLength)
|
newPassword, err = base62.Random(defaultPasswordLength)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
lreq.Replace("userPassword", []string{newPassword})
|
lreq.Replace("userPassword", []string{newPassword})
|
||||||
|
|
||||||
err = conn.Modify(lreq)
|
err = conn.Modify(lreq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
// update config with new password
|
// update config with new password
|
||||||
cfg.BindPassword = newPassword
|
cfg.BindPassword = newPassword
|
||||||
entry, err := logical.StorageEntryJSON("config", cfg)
|
entry, err := logical.StorageEntryJSON("config", cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
if err := req.Storage.Put(ctx, entry); err != nil {
|
if err := req.Storage.Put(ctx, entry); err != nil {
|
||||||
// we might have to roll-back the password here?
|
// we might have to roll-back the password here?
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathConfigRotateRootHelpSyn = `
|
const pathConfigRotateRootHelpSyn = `
|
||||||
|
|||||||
3
changelog/29535.txt
Normal file
3
changelog/29535.txt
Normal 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.
|
||||||
|
```
|
||||||
@@ -108,6 +108,27 @@ This endpoint configures the LDAP auth method.
|
|||||||
- `enable_samaccountname_login` `(bool: false)` - (Optional) Lets Active Directory
|
- `enable_samaccountname_login` `(bool: false)` - (Optional) Lets Active Directory
|
||||||
LDAP users log in using `sAMAccountName` or `userPrincipalName` when the
|
LDAP users log in using `sAMAccountName` or `userPrincipalName` when the
|
||||||
`upndomain` parameter is set.
|
`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'
|
@include 'tokenfields.mdx'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user