Skip to content

Commit 9b7d068

Browse files
authored
Add a /config/rotate-root path to the ldap auth backend (#24099)
1 parent e69b0b2 commit 9b7d068

File tree

5 files changed

+210
-2
lines changed

5 files changed

+210
-2
lines changed

builtin/credential/ldap/backend.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"fmt"
99
"strings"
10+
"sync"
1011

1112
"github.com/hashicorp/cap/ldap"
1213
"github.com/hashicorp/go-secure-stdlib/strutil"
@@ -17,8 +18,9 @@ import (
1718
)
1819

1920
const (
20-
operationPrefixLDAP = "ldap"
21-
errUserBindFailed = "ldap operation failed: failed to bind as user"
21+
operationPrefixLDAP = "ldap"
22+
errUserBindFailed = "ldap operation failed: failed to bind as user"
23+
defaultPasswordLength = 64 // length to use for configured root password on rotations by default
2224
)
2325

2426
func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
@@ -51,6 +53,7 @@ func Backend() *backend {
5153
pathUsers(&b),
5254
pathUsersList(&b),
5355
pathLogin(&b),
56+
pathConfigRotateRoot(&b),
5457
},
5558

5659
AuthRenew: b.pathLoginRenew,
@@ -62,6 +65,8 @@ func Backend() *backend {
6265

6366
type backend struct {
6467
*framework.Backend
68+
69+
mu sync.RWMutex
6570
}
6671

6772
func (b *backend) Login(ctx context.Context, req *logical.Request, username string, password string, usernameAsAlias bool) (string, []string, *logical.Response, []string, error) {

builtin/credential/ldap/path_config.go

+19
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ func pathConfig(b *backend) *framework.Path {
4848

4949
tokenutil.AddTokenFields(p.Fields)
5050
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."
51+
52+
p.Fields["password_policy"] = &framework.FieldSchema{
53+
Type: framework.TypeString,
54+
Description: "Password policy to use to rotate the root password",
55+
}
56+
5157
return p
5258
}
5359

@@ -118,6 +124,9 @@ func (b *backend) Config(ctx context.Context, req *logical.Request) (*ldapConfig
118124
}
119125

120126
func (b *backend) pathConfigRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
127+
b.mu.RLock()
128+
defer b.mu.RUnlock()
129+
121130
cfg, err := b.Config(ctx, req)
122131
if err != nil {
123132
return nil, err
@@ -128,6 +137,7 @@ func (b *backend) pathConfigRead(ctx context.Context, req *logical.Request, d *f
128137

129138
data := cfg.PasswordlessMap()
130139
cfg.PopulateTokenData(data)
140+
data["password_policy"] = cfg.PasswordPolicy
131141

132142
resp := &logical.Response{
133143
Data: data,
@@ -164,6 +174,9 @@ func (b *backend) checkConfigUserFilter(cfg *ldapConfigEntry) []string {
164174
}
165175

166176
func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
177+
b.mu.Lock()
178+
defer b.mu.Unlock()
179+
167180
cfg, err := b.Config(ctx, req)
168181
if err != nil {
169182
return nil, err
@@ -194,6 +207,10 @@ func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, d *
194207
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
195208
}
196209

210+
if passwordPolicy, ok := d.GetOk("password_policy"); ok {
211+
cfg.PasswordPolicy = passwordPolicy.(string)
212+
}
213+
197214
entry, err := logical.StorageEntryJSON("config", cfg)
198215
if err != nil {
199216
return nil, err
@@ -234,6 +251,8 @@ func (b *backend) getConfigFieldData() (*framework.FieldData, error) {
234251
type ldapConfigEntry struct {
235252
tokenutil.TokenParams
236253
*ldaputil.ConfigEntry
254+
255+
PasswordPolicy string `json:"password_policy"`
237256
}
238257

239258
const pathConfigHelpSyn = `
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: BUSL-2.0
3+
4+
package ldap
5+
6+
import (
7+
"context"
8+
9+
"github.com/go-ldap/ldap/v3"
10+
11+
"github.com/hashicorp/vault/sdk/helper/base62"
12+
"github.com/hashicorp/vault/sdk/helper/ldaputil"
13+
14+
"github.com/hashicorp/vault/sdk/framework"
15+
"github.com/hashicorp/vault/sdk/logical"
16+
)
17+
18+
func pathConfigRotateRoot(b *backend) *framework.Path {
19+
return &framework.Path{
20+
Pattern: "config/rotate-root",
21+
22+
DisplayAttrs: &framework.DisplayAttributes{
23+
OperationPrefix: operationPrefixLDAP,
24+
OperationVerb: "rotate",
25+
OperationSuffix: "root-credentials",
26+
},
27+
28+
Operations: map[logical.Operation]framework.OperationHandler{
29+
logical.UpdateOperation: &framework.PathOperation{
30+
Callback: b.pathConfigRotateRootUpdate,
31+
ForwardPerformanceSecondary: true,
32+
ForwardPerformanceStandby: true,
33+
},
34+
},
35+
36+
HelpSynopsis: pathConfigRotateRootHelpSyn,
37+
HelpDescription: pathConfigRotateRootHelpDesc,
38+
}
39+
}
40+
41+
func (b *backend) pathConfigRotateRootUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
42+
// lock the backend's state - really just the config state - for mutating
43+
b.mu.Lock()
44+
defer b.mu.Unlock()
45+
46+
cfg, err := b.Config(ctx, req)
47+
if err != nil {
48+
return nil, err
49+
}
50+
if cfg == nil {
51+
return logical.ErrorResponse("attempted to rotate root on an undefined config"), nil
52+
}
53+
54+
u, p := cfg.BindDN, cfg.BindPassword
55+
if u == "" || p == "" {
56+
return logical.ErrorResponse("auth is not using authenticated search, no root to rotate"), nil
57+
}
58+
59+
// grab our ldap client
60+
client := ldaputil.Client{
61+
Logger: b.Logger(),
62+
LDAP: ldaputil.NewLDAP(),
63+
}
64+
65+
conn, err := client.DialLDAP(cfg.ConfigEntry)
66+
if err != nil {
67+
return nil, err
68+
}
69+
70+
err = conn.Bind(u, p)
71+
if err != nil {
72+
return nil, err
73+
}
74+
75+
lreq := &ldap.ModifyRequest{
76+
DN: cfg.BindDN,
77+
}
78+
79+
var newPassword string
80+
if cfg.PasswordPolicy != "" {
81+
newPassword, err = b.System().GeneratePasswordFromPolicy(ctx, cfg.PasswordPolicy)
82+
} else {
83+
newPassword, err = base62.Random(defaultPasswordLength)
84+
}
85+
if err != nil {
86+
return nil, err
87+
}
88+
89+
lreq.Replace("userPassword", []string{newPassword})
90+
91+
err = conn.Modify(lreq)
92+
if err != nil {
93+
return nil, err
94+
}
95+
// update config with new password
96+
cfg.BindPassword = newPassword
97+
entry, err := logical.StorageEntryJSON("config", cfg)
98+
if err != nil {
99+
return nil, err
100+
}
101+
if err := req.Storage.Put(ctx, entry); err != nil {
102+
// we might have to roll-back the password here?
103+
return nil, err
104+
}
105+
106+
return nil, nil
107+
}
108+
109+
const pathConfigRotateRootHelpSyn = `
110+
Request to rotate the LDAP credentials used by Vault
111+
`
112+
113+
const pathConfigRotateRootHelpDesc = `
114+
This path attempts to rotate the LDAP bindpass used by Vault for this mount.
115+
`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: BUSL-2.0
3+
4+
package ldap
5+
6+
import (
7+
"context"
8+
"os"
9+
"testing"
10+
11+
"github.com/hashicorp/vault/helper/testhelpers/ldap"
12+
logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical"
13+
"github.com/hashicorp/vault/sdk/logical"
14+
)
15+
16+
// This test relies on a docker ldap server with a suitable person object (cn=admin,dc=planetexpress,dc=com)
17+
// with bindpassword "admin". `PrepareTestContainer` does this for us. - see the backend_test for more details
18+
func TestRotateRoot(t *testing.T) {
19+
if os.Getenv(logicaltest.TestEnvVar) == "" {
20+
t.Skip("skipping rotate root tests because VAULT_ACC is unset")
21+
}
22+
ctx := context.Background()
23+
24+
b, store := createBackendWithStorage(t)
25+
cleanup, cfg := ldap.PrepareTestContainer(t, "latest")
26+
defer cleanup()
27+
// set up auth config
28+
req := &logical.Request{
29+
Operation: logical.UpdateOperation,
30+
Path: "config",
31+
Storage: store,
32+
Data: map[string]interface{}{
33+
"url": cfg.Url,
34+
"binddn": cfg.BindDN,
35+
"bindpass": cfg.BindPassword,
36+
"userdn": cfg.UserDN,
37+
},
38+
}
39+
40+
resp, err := b.HandleRequest(ctx, req)
41+
if err != nil {
42+
t.Fatalf("failed to initialize ldap auth config: %s", err)
43+
}
44+
if resp != nil && resp.IsError() {
45+
t.Fatalf("failed to initialize ldap auth config: %s", resp.Data["error"])
46+
}
47+
48+
req = &logical.Request{
49+
Operation: logical.UpdateOperation,
50+
Path: "config/rotate-root",
51+
Storage: store,
52+
}
53+
54+
_, err = b.HandleRequest(ctx, req)
55+
if err != nil {
56+
t.Fatalf("failed to rotate password: %s", err)
57+
}
58+
59+
newCFG, err := b.Config(ctx, req)
60+
if newCFG.BindDN != cfg.BindDN {
61+
t.Fatalf("a value in config that should have stayed the same changed: %s", cfg.BindDN)
62+
}
63+
if newCFG.BindPassword == cfg.BindPassword {
64+
t.Fatalf("the password should have changed, but it didn't")
65+
}
66+
}

changelog/24099.txt

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

0 commit comments

Comments
 (0)