mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 17:52:32 +00:00
Add a /config/rotate-root path to the ldap auth backend (#24099)
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/cap/ldap"
|
||||
"github.com/hashicorp/go-secure-stdlib/strutil"
|
||||
@@ -17,8 +18,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
operationPrefixLDAP = "ldap"
|
||||
errUserBindFailed = "ldap operation failed: failed to bind as user"
|
||||
operationPrefixLDAP = "ldap"
|
||||
errUserBindFailed = "ldap operation failed: failed to bind as user"
|
||||
defaultPasswordLength = 64 // length to use for configured root password on rotations by default
|
||||
)
|
||||
|
||||
func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
|
||||
@@ -51,6 +53,7 @@ func Backend() *backend {
|
||||
pathUsers(&b),
|
||||
pathUsersList(&b),
|
||||
pathLogin(&b),
|
||||
pathConfigRotateRoot(&b),
|
||||
},
|
||||
|
||||
AuthRenew: b.pathLoginRenew,
|
||||
@@ -62,6 +65,8 @@ func Backend() *backend {
|
||||
|
||||
type backend struct {
|
||||
*framework.Backend
|
||||
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (b *backend) Login(ctx context.Context, req *logical.Request, username string, password string, usernameAsAlias bool) (string, []string, *logical.Response, []string, error) {
|
||||
|
||||
@@ -48,6 +48,12 @@ func pathConfig(b *backend) *framework.Path {
|
||||
|
||||
tokenutil.AddTokenFields(p.Fields)
|
||||
p.Fields["token_policies"].Description += ". This will apply to all tokens generated by this auth method, in addition to any configured for specific users/groups."
|
||||
|
||||
p.Fields["password_policy"] = &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: "Password policy to use to rotate the root password",
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
@@ -118,6 +124,9 @@ func (b *backend) Config(ctx context.Context, req *logical.Request) (*ldapConfig
|
||||
}
|
||||
|
||||
func (b *backend) pathConfigRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
|
||||
cfg, err := b.Config(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -128,6 +137,7 @@ func (b *backend) pathConfigRead(ctx context.Context, req *logical.Request, d *f
|
||||
|
||||
data := cfg.PasswordlessMap()
|
||||
cfg.PopulateTokenData(data)
|
||||
data["password_policy"] = cfg.PasswordPolicy
|
||||
|
||||
resp := &logical.Response{
|
||||
Data: data,
|
||||
@@ -164,6 +174,9 @@ func (b *backend) checkConfigUserFilter(cfg *ldapConfigEntry) []string {
|
||||
}
|
||||
|
||||
func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
cfg, err := b.Config(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -194,6 +207,10 @@ func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, d *
|
||||
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
|
||||
}
|
||||
|
||||
if passwordPolicy, ok := d.GetOk("password_policy"); ok {
|
||||
cfg.PasswordPolicy = passwordPolicy.(string)
|
||||
}
|
||||
|
||||
entry, err := logical.StorageEntryJSON("config", cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -234,6 +251,8 @@ func (b *backend) getConfigFieldData() (*framework.FieldData, error) {
|
||||
type ldapConfigEntry struct {
|
||||
tokenutil.TokenParams
|
||||
*ldaputil.ConfigEntry
|
||||
|
||||
PasswordPolicy string `json:"password_policy"`
|
||||
}
|
||||
|
||||
const pathConfigHelpSyn = `
|
||||
|
||||
115
builtin/credential/ldap/path_config_rotate_root.go
Normal file
115
builtin/credential/ldap/path_config_rotate_root.go
Normal file
@@ -0,0 +1,115 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/helper/base62"
|
||||
"github.com/hashicorp/vault/sdk/helper/ldaputil"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
func pathConfigRotateRoot(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "config/rotate-root",
|
||||
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationPrefix: operationPrefixLDAP,
|
||||
OperationVerb: "rotate",
|
||||
OperationSuffix: "root-credentials",
|
||||
},
|
||||
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.UpdateOperation: &framework.PathOperation{
|
||||
Callback: b.pathConfigRotateRootUpdate,
|
||||
ForwardPerformanceSecondary: true,
|
||||
ForwardPerformanceStandby: true,
|
||||
},
|
||||
},
|
||||
|
||||
HelpSynopsis: pathConfigRotateRootHelpSyn,
|
||||
HelpDescription: pathConfigRotateRootHelpDesc,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) pathConfigRotateRootUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, 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
|
||||
}
|
||||
if cfg == nil {
|
||||
return logical.ErrorResponse("attempted to rotate root on an undefined config"), nil
|
||||
}
|
||||
|
||||
u, p := cfg.BindDN, cfg.BindPassword
|
||||
if u == "" || p == "" {
|
||||
return logical.ErrorResponse("auth is not using authenticated search, no root to rotate"), nil
|
||||
}
|
||||
|
||||
// grab our ldap client
|
||||
client := ldaputil.Client{
|
||||
Logger: b.Logger(),
|
||||
LDAP: ldaputil.NewLDAP(),
|
||||
}
|
||||
|
||||
conn, err := client.DialLDAP(cfg.ConfigEntry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = conn.Bind(u, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lreq := &ldap.ModifyRequest{
|
||||
DN: cfg.BindDN,
|
||||
}
|
||||
|
||||
var newPassword string
|
||||
if cfg.PasswordPolicy != "" {
|
||||
newPassword, err = b.System().GeneratePasswordFromPolicy(ctx, cfg.PasswordPolicy)
|
||||
} else {
|
||||
newPassword, err = base62.Random(defaultPasswordLength)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lreq.Replace("userPassword", []string{newPassword})
|
||||
|
||||
err = conn.Modify(lreq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// update config with new password
|
||||
cfg.BindPassword = newPassword
|
||||
entry, err := logical.StorageEntryJSON("config", cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := req.Storage.Put(ctx, entry); err != nil {
|
||||
// we might have to roll-back the password here?
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
const pathConfigRotateRootHelpSyn = `
|
||||
Request to rotate the LDAP credentials used by Vault
|
||||
`
|
||||
|
||||
const pathConfigRotateRootHelpDesc = `
|
||||
This path attempts to rotate the LDAP bindpass used by Vault for this mount.
|
||||
`
|
||||
66
builtin/credential/ldap/path_config_rotate_root_test.go
Normal file
66
builtin/credential/ldap/path_config_rotate_root_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/vault/helper/testhelpers/ldap"
|
||||
logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
// This test relies on a docker ldap server with a suitable person object (cn=admin,dc=planetexpress,dc=com)
|
||||
// with bindpassword "admin". `PrepareTestContainer` does this for us. - see the backend_test for more details
|
||||
func TestRotateRoot(t *testing.T) {
|
||||
if os.Getenv(logicaltest.TestEnvVar) == "" {
|
||||
t.Skip("skipping rotate root tests because VAULT_ACC is unset")
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
b, store := createBackendWithStorage(t)
|
||||
cleanup, cfg := ldap.PrepareTestContainer(t, "latest")
|
||||
defer cleanup()
|
||||
// set up auth config
|
||||
req := &logical.Request{
|
||||
Operation: logical.UpdateOperation,
|
||||
Path: "config",
|
||||
Storage: store,
|
||||
Data: map[string]interface{}{
|
||||
"url": cfg.Url,
|
||||
"binddn": cfg.BindDN,
|
||||
"bindpass": cfg.BindPassword,
|
||||
"userdn": cfg.UserDN,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := b.HandleRequest(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to initialize ldap auth config: %s", err)
|
||||
}
|
||||
if resp != nil && resp.IsError() {
|
||||
t.Fatalf("failed to initialize ldap auth config: %s", resp.Data["error"])
|
||||
}
|
||||
|
||||
req = &logical.Request{
|
||||
Operation: logical.UpdateOperation,
|
||||
Path: "config/rotate-root",
|
||||
Storage: store,
|
||||
}
|
||||
|
||||
_, err = b.HandleRequest(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to rotate password: %s", err)
|
||||
}
|
||||
|
||||
newCFG, err := b.Config(ctx, req)
|
||||
if newCFG.BindDN != cfg.BindDN {
|
||||
t.Fatalf("a value in config that should have stayed the same changed: %s", cfg.BindDN)
|
||||
}
|
||||
if newCFG.BindPassword == cfg.BindPassword {
|
||||
t.Fatalf("the password should have changed, but it didn't")
|
||||
}
|
||||
}
|
||||
3
changelog/24099.txt
Normal file
3
changelog/24099.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
```release-note:feature
|
||||
**Rotate Root for LDAP auth**: Rotate root operations are now supported for the LDAP auth engine.
|
||||
```
|
||||
Reference in New Issue
Block a user