-
Notifications
You must be signed in to change notification settings - Fork 7
/
path_rotate.go
220 lines (197 loc) · 7.1 KB
/
path_rotate.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package openldap
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/backoff"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/sdk/queue"
)
var (
rollbackAttempts = 10
minRollbackDuration = 1 * time.Second
maxRollbackDuration = 100 * time.Second
)
const (
rotateRootPath = "rotate-root"
rotateRolePath = "rotate-role/"
)
func (b *backend) pathRotateCredentials() []*framework.Path {
return []*framework.Path{
{
Pattern: rotateRootPath,
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixLDAP,
OperationVerb: "rotate",
OperationSuffix: "root-credentials",
},
Fields: map[string]*framework.FieldSchema{},
Operations: map[logical.Operation]framework.OperationHandler{
logical.UpdateOperation: &framework.PathOperation{
Callback: b.pathRotateRootCredentialsUpdate,
ForwardPerformanceStandby: true,
ForwardPerformanceSecondary: true,
},
},
HelpSynopsis: "Request to rotate the root credentials Vault uses for the LDAP administrator account.",
HelpDescription: "This path attempts to rotate the root credentials of the administrator account " +
"(binddn) used by Vault to manage LDAP.",
},
{
Pattern: strings.TrimSuffix(rotateRolePath, "/") + genericNameWithForwardSlashRegex("name"),
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixLDAP,
OperationVerb: "rotate",
OperationSuffix: "static-role",
},
Fields: fieldsForType(rotateRolePath),
Operations: map[logical.Operation]framework.OperationHandler{
logical.UpdateOperation: &framework.PathOperation{
Callback: b.pathRotateRoleCredentialsUpdate,
ForwardPerformanceStandby: true,
ForwardPerformanceSecondary: true,
},
},
HelpSynopsis: "Request to rotate the credentials for a static user account.",
HelpDescription: "This path attempts to rotate the credentials for the given LDAP static user account.",
},
}
}
func (b *backend) pathRotateRootCredentialsUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
if _, hasTimeout := ctx.Deadline(); !hasTimeout {
var cancel func()
ctx, cancel = context.WithTimeout(ctx, defaultCtxTimeout)
defer cancel()
}
config, err := readConfig(ctx, req.Storage)
if err != nil {
return nil, err
}
if config == nil {
return nil, errors.New("the config is currently unset")
}
newPassword, err := b.GeneratePassword(ctx, config)
if err != nil {
return nil, err
}
oldPassword := config.LDAP.BindPassword
// Take out the backend lock since we are swapping out the connection
b.Lock()
defer b.Unlock()
// Update the password remotely.
if err := b.client.UpdateDNPassword(config.LDAP, config.LDAP.BindDN, newPassword); err != nil {
return nil, err
}
config.LDAP.BindPassword = newPassword
config.LDAP.LastBindPassword = oldPassword
config.LDAP.LastBindPasswordRotation = time.Now()
// Update the password locally.
if pwdStoringErr := storePassword(ctx, req.Storage, config); pwdStoringErr != nil {
// We were unable to store the new password locally. We can't continue in this state because we won't be able
// to roll any passwords, including our own to get back into a state of working. So, we need to roll back to
// the last password we successfully got into storage.
if rollbackErr := b.rollbackPassword(ctx, config, oldPassword); rollbackErr != nil {
return nil, fmt.Errorf(`unable to store new password due to %s and unable to return to previous password
due to %s, configure a new binddn and bindpass to restore ldap function`, pwdStoringErr, rollbackErr)
}
return nil, fmt.Errorf("unable to update password due to storage err: %s", pwdStoringErr)
}
// Respond with a 204.
return nil, nil
}
func (b *backend) pathRotateRoleCredentialsUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
name := data.Get("name").(string)
if name == "" {
return logical.ErrorResponse("empty role name attribute given"), nil
}
role, err := b.staticRole(ctx, req.Storage, name)
if err != nil {
return nil, err
}
if role == nil {
return logical.ErrorResponse("role doesn't exist: %s", name), nil
}
// In create/update of static accounts, we only care if the operation
// err'd , and this call does not return credentials
item, err := b.popFromRotationQueueByKey(name)
if err != nil {
item = &queue.Item{
Key: name,
}
}
input := &setStaticAccountInput{
RoleName: name,
Role: role,
}
if walID, ok := item.Value.(string); ok {
input.WALID = walID
}
resp, err := b.setStaticAccountPassword(ctx, req.Storage, input)
if err != nil {
b.Logger().Warn("unable to rotate credentials in rotate-role", "error", err)
// Update the priority to re-try this rotation and re-add the item to
// the queue
item.Priority = time.Now().Add(10 * time.Second).Unix()
// Preserve the WALID if it was returned
if resp != nil && resp.WALID != "" {
item.Value = resp.WALID
}
} else {
item.Priority = resp.RotationTime.Add(role.StaticAccount.RotationPeriod).Unix()
// Clear any stored WAL ID as we must have successfully deleted our WAL to get here.
item.Value = ""
}
// Add their rotation to the queue. We use pushErr here to distinguish between
// the error returned from setStaticAccount. They are scoped differently but
// it's more clear to developers that err above can still be non nil, and not
// overwritten or reused here.
if pushErr := b.pushItem(item); pushErr != nil {
return nil, pushErr
}
if err != nil {
return nil, fmt.Errorf("unable to finish rotating credentials; retries will "+
"continue in the background but it is also safe to retry manually: %w", err)
}
// We're not returning creds here because we do not know if its been processed
// by the queue.
return nil, nil
}
// rollbackPassword uses exponential backoff to retry updating to an old password,
// because LDAP may still be propagating the previous password change.
func (b *backend) rollbackPassword(ctx context.Context, config *config, oldPassword string) error {
expbackoff := backoff.NewBackoff(rollbackAttempts, minRollbackDuration, maxRollbackDuration)
var err error
for {
nextsleep, terr := expbackoff.Next()
if terr != nil {
// exponential backoff has failed every attempt; return last error
return err
}
timer := time.NewTimer(nextsleep)
select {
case <-timer.C:
case <-ctx.Done():
if !timer.Stop() {
<-timer.C // drain the channel so that it will be garbage collected
}
// Outer environment is closing.
return fmt.Errorf("unable to rollback password because enclosing environment is shutting down")
}
err = b.client.UpdateDNPassword(config.LDAP, config.LDAP.BindDN, oldPassword)
if err == nil {
return nil
}
}
}
func storePassword(ctx context.Context, s logical.Storage, config *config) error {
entry, err := logical.StorageEntryJSON(configPath, config)
if err != nil {
return err
}
return s.Put(ctx, entry)
}