Add a /config/rotate-root path to the ldap auth backend (#24099)

This commit is contained in:
kpcraig
2023-11-27 15:48:16 -05:00
committed by GitHub
parent e69b0b2bcf
commit 9b7d06839f
5 changed files with 210 additions and 2 deletions

View File

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

View File

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

View 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.
`

View 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
View File

@@ -0,0 +1,3 @@
```release-note:feature
**Rotate Root for LDAP auth**: Rotate root operations are now supported for the LDAP auth engine.
```