Skip to content
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

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

Merged
merged 20 commits into from
Nov 27, 2023
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
9 changes: 7 additions & 2 deletions builtin/credential/ldap/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"fmt"
"strings"
"sync"

"github.com/hashicorp/cap/ldap"
"github.com/hashicorp/go-secure-stdlib/strutil"
Expand All @@ -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) {
Expand Down Expand Up @@ -51,6 +53,7 @@ func Backend() *backend {
pathUsers(&b),
pathUsersList(&b),
pathLogin(&b),
pathConfigRotateRoot(&b),
},

AuthRenew: b.pathLoginRenew,
Expand All @@ -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) {
Expand Down
19 changes: 19 additions & 0 deletions builtin/credential/ldap/path_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = `
Expand Down
115 changes: 115 additions & 0 deletions builtin/credential/ldap/path_config_rotate_root.go
Original file line number Diff line number Diff line change
@@ -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(),
}

Comment on lines +59 to +64
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this live on the *backend struct so we can reuse it or does the client need to be created every time?
Also, there's a newer cap/ldap client that we're trying to migrate to. Any reason not to use this one instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on potentially adding a getClient helper on the backend struct. That would also match up with other plugins

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i thought about this - this is the only place this particular is currently used in this (credential/)ldap, and i want to avoid confusion with the other similarly named one (cap/ldap/Client).

Possibly whoever adds the second use of this client can add the helper?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not overly concerned with having a helper method, but more so wondering why we can't reuse the same instance of the ldap client. Probably not too concerning though, assuming that root rotation isn't a high-volume endpoint.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't notice the earlier mention of the cap client the first time around - the issue i encountered was that it wasn't interacting with LDAP at the level I needed to make Modify calls to the server, although maybe there was a way to do it that I missed.

I'm not too worried about multiple instantiations of the ldap client - whatever we're allocating is pretty thin each time, and rotate-root is going to be pretty low volume.

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 changes: 66 additions & 0 deletions builtin/credential/ldap/path_config_rotate_root_test.go
Original file line number Diff line number Diff line change
@@ -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 changes: 3 additions & 0 deletions changelog/24099.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
**Rotate Root for LDAP auth**: Rotate root operations are now supported for the LDAP auth engine.
```