-
Notifications
You must be signed in to change notification settings - Fork 10
/
path_creds.go
260 lines (226 loc) · 8.14 KB
/
path_creds.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
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package plugin
import (
"context"
"fmt"
"strings"
"time"
"github.com/go-errors/errors"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
const (
credPrefix = "creds/"
storageKey = "creds"
// Since password TTL can be set to as low as 1 second,
// we can't cache passwords for an entire second.
credCacheCleanup = time.Second / 3
credCacheExpiration = time.Second / 2
)
// deleteCred fulfills the DeleteWatcher interface in roles.
// It allows the roleHandler to let us know when a role's been deleted so we can delete its associated creds too.
func (b *backend) deleteCred(ctx context.Context, storage logical.Storage, roleName string) error {
if err := storage.Delete(ctx, storageKey+"/"+roleName); err != nil {
return err
}
b.credCache.Delete(roleName)
return nil
}
func (b *backend) invalidateCred(ctx context.Context, key string) {
if strings.HasPrefix(key, credPrefix) {
roleName := key[len(credPrefix):]
b.credCache.Delete(roleName)
}
}
func (b *backend) pathCreds() *framework.Path {
return &framework.Path{
Pattern: credPrefix + framework.GenericNameRegex("name"),
Fields: map[string]*framework.FieldSchema{
"name": {
Type: framework.TypeString,
Description: "Name of the role",
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: b.credReadOperation,
ForwardPerformanceStandby: true,
ForwardPerformanceSecondary: true,
},
},
HelpSynopsis: credHelpSynopsis,
HelpDescription: credHelpDescription,
}
}
func (b *backend) credReadOperation(ctx context.Context, req *logical.Request, fieldData *framework.FieldData) (*logical.Response, error) {
cred := make(map[string]interface{})
engineConf, err := readConfig(ctx, req.Storage)
if err != nil {
return nil, err
}
if engineConf == nil {
return nil, errors.New("the config is currently unset")
}
roleName := fieldData.Get("name").(string)
// We act upon quite a few things below that could be racy if not locked:
// - Roles. If a new cred is created, the role is updated to include the new LastVaultRotation time,
// effecting role storage (and the role cache, but that's already thread-safe).
// - Creds. New creds involve writing to cred storage and the cred cache (also already thread-safe).
// Rather than setting read locks of different types, and upgrading them to write locks, let's keep complexity
// low and use one simple mutex.
b.credLock.Lock()
defer b.credLock.Unlock()
role, err := b.readRole(ctx, req.Storage, roleName)
if err != nil {
return nil, err
}
if role == nil {
return nil, nil
}
b.Logger().Debug(fmt.Sprintf("role is: %+v", role))
var resp *logical.Response
var respErr error
var unset time.Time
switch {
case role.LastVaultRotation == unset:
b.Logger().Info("rotating password for the first time so Vault will know it")
resp, respErr = b.generateAndReturnCreds(ctx, engineConf, req.Storage, roleName, role, cred)
case role.PasswordLastSet.After(role.LastVaultRotation.Add(time.Second * time.Duration(engineConf.LastRotationTolerance))):
b.Logger().Warn(fmt.Sprintf(
"Vault rotated the password at %s, but it was rotated in AD later at %s, so rotating it again so Vault will know it",
role.LastVaultRotation.String(), role.PasswordLastSet.String()),
)
resp, respErr = b.generateAndReturnCreds(ctx, engineConf, req.Storage, roleName, role, cred)
default:
b.Logger().Debug("determining whether to rotate credential")
credIfc, found := b.credCache.Get(roleName)
if found {
b.Logger().Debug("checking cached credential")
cred = credIfc.(map[string]interface{})
} else {
b.Logger().Debug("checking stored credential")
entry, err := req.Storage.Get(ctx, storageKey+"/"+roleName)
if err != nil {
return nil, err
}
if entry == nil {
// If the creds aren't in storage, but roles are and we've created creds before,
// this is an unexpected state and something has gone wrong.
// Let's be explicit and error about this.
return nil, fmt.Errorf("should have the creds for %+v but they're not found", role)
}
if err := entry.DecodeJSON(&cred); err != nil {
return nil, err
}
b.credCache.SetDefault(roleName, cred)
}
now := time.Now().UTC()
shouldBeRolled := role.LastVaultRotation.Add(time.Duration(role.TTL) * time.Second) // already in UTC
if now.After(shouldBeRolled) {
b.Logger().Info(fmt.Sprintf(
"last Vault rotation was at %s, and since the TTL is %d and it's now %s, it's time to rotate it",
role.LastVaultRotation.String(), role.TTL, now.String()),
)
resp, respErr = b.generateAndReturnCreds(ctx, engineConf, req.Storage, roleName, role, cred)
} else {
b.Logger().Debug("returning previous credential")
resp = &logical.Response{
Data: cred,
}
}
}
if respErr != nil {
return nil, respErr
}
return resp, nil
}
func (b *backend) generateAndReturnCreds(ctx context.Context, engineConf *configuration, storage logical.Storage, roleName string, role *backendRole, previousCred map[string]interface{}) (*logical.Response, error) {
newPassword, err := GeneratePassword(ctx, engineConf.PasswordConf, b.System())
if err != nil {
return nil, err
}
var currentPassword, lastPassword string
if previousCred != nil {
if val, ok := previousCred["current_password"].(string); ok {
currentPassword = val
}
if val, ok := previousCred["last_password"].(string); ok {
lastPassword = val
}
}
wal := rotateCredentialEntry{
CurrentPassword: currentPassword,
LastPassword: lastPassword,
RoleName: roleName,
TTL: role.TTL,
ServiceAccountName: role.ServiceAccountName,
LastVaultRotation: role.LastVaultRotation,
}
// Bail if we can't persist the WAL
walID, err := framework.PutWAL(ctx, storage, rotateCredentialWAL, wal)
if err != nil {
return nil, fmt.Errorf("could not persist WAL before rotation: %s", err)
}
err = b.client.UpdatePassword(engineConf.ADConf, role.ServiceAccountName, newPassword)
if err != nil {
return nil, err
}
// Time recorded is in UTC for easier user comparison to AD's last rotated time, which is set to UTC by Microsoft.
role.LastVaultRotation = time.Now().UTC()
if err := b.writeRoleToStorage(ctx, storage, roleName, role); err != nil {
return nil, err
}
// Cache the full role to minimize Vault storage calls.
b.roleCache.SetDefault(roleName, role)
// Although a service account name is typically my_app@example.com,
// the username it uses is just my_app, or everything before the @.
var username string
if username, err = getUsername(role.ServiceAccountName); err != nil {
return nil, err
}
cred := map[string]interface{}{
"username": username,
"current_password": newPassword,
}
if previousCred != nil && previousCred["current_password"] != nil {
cred["last_password"] = previousCred["current_password"]
}
// Cache and save the cred.
path := fmt.Sprintf("%s/%s", storageKey, roleName)
entry, err := logical.StorageEntryJSON(path, cred)
if err != nil {
return nil, err
}
if err := storage.Put(ctx, entry); err != nil {
return nil, err
}
b.credCache.SetDefault(roleName, cred)
// Delete the WAL entry
if err := framework.DeleteWAL(ctx, storage, walID); err != nil {
// The rotation was successful, so don't return the error.
// The WAL will eventually be discarded by the rollback handler.
b.Logger().Warn("failed to delete password rotation WAL", "error", err.Error())
}
return &logical.Response{
Data: cred,
}, nil
}
// getUsername extracts the username from a service account name by
// splitting on @. For example, if vault@hashicorp.com is the service
// account, vault is the username.
func getUsername(serviceAccount string) (string, error) {
fields := strings.Split(serviceAccount, "@")
if len(fields) > 0 {
return fields[0], nil
}
return "", fmt.Errorf("unable to infer username from service account name: %s", serviceAccount)
}
const (
credHelpSynopsis = `
Retrieve a role's creds by role name.
`
credHelpDescription = `
Read creds using a role's name to view the login, current password, and last password.
`
)