1package openldap 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 "time" 8 9 "github.com/hashicorp/errwrap" 10 "github.com/hashicorp/go-multierror" 11 "github.com/hashicorp/vault/sdk/framework" 12 "github.com/hashicorp/vault/sdk/helper/base62" 13 "github.com/hashicorp/vault/sdk/helper/consts" 14 "github.com/hashicorp/vault/sdk/helper/locksutil" 15 "github.com/hashicorp/vault/sdk/logical" 16 "github.com/hashicorp/vault/sdk/queue" 17) 18 19const ( 20 // Interval to check the queue for items needing rotation 21 queueTickSeconds = 5 22 queueTickInterval = queueTickSeconds * time.Second 23 24 // WAL storage key used for static account rotations 25 staticWALKey = "staticRotationKey" 26) 27 28// populateQueue loads the priority queue with existing static accounts. This 29// occurs at initialization, after any WAL entries of failed or interrupted 30// rotations have been processed. It lists the roles from storage and searches 31// for any that have an associated static account, then adds them to the 32// priority queue for rotations. 33func (b *backend) populateQueue(ctx context.Context, s logical.Storage) { 34 log := b.Logger() 35 log.Info("populating role rotation queue") 36 37 // Build map of role name / wal entries 38 walMap, err := b.loadStaticWALs(ctx, s) 39 if err != nil { 40 log.Warn("unable to load rotation WALs", "error", err) 41 } 42 43 roles, err := s.List(ctx, staticRolePath) 44 if err != nil { 45 log.Warn("unable to list role for enqueueing", "error", err) 46 return 47 } 48 49 for _, roleName := range roles { 50 select { 51 case <-ctx.Done(): 52 log.Info("rotation queue restore cancelled") 53 return 54 default: 55 } 56 57 role, err := b.staticRole(ctx, s, roleName) 58 if err != nil { 59 log.Warn("unable to read static role", "error", err, "role", roleName) 60 continue 61 } 62 63 item := queue.Item{ 64 Key: roleName, 65 Priority: role.StaticAccount.LastVaultRotation.Add(role.StaticAccount.RotationPeriod).Unix(), 66 } 67 68 // Check if role name is in map 69 walEntry := walMap[roleName] 70 if walEntry != nil { 71 // Check walEntry last vault time 72 if !walEntry.LastVaultRotation.IsZero() && walEntry.LastVaultRotation.Before(role.StaticAccount.LastVaultRotation) { 73 // WAL's last vault rotation record is older than the role's data, so 74 // delete and move on 75 if err := framework.DeleteWAL(ctx, s, walEntry.walID); err != nil { 76 log.Warn("unable to delete WAL", "error", err, "WAL ID", walEntry.walID) 77 } 78 } else { 79 log.Info("adjusting priority for Role") 80 item.Value = walEntry.walID 81 item.Priority = time.Now().Unix() 82 } 83 } 84 85 if err := b.pushItem(&item); err != nil { 86 log.Warn("unable to enqueue item", "error", err, "role", roleName) 87 } 88 } 89} 90 91// runTicker kicks off a periodic ticker that invoke the automatic credential 92// rotation method at a determined interval. The default interval is 5 seconds. 93func (b *backend) runTicker(ctx context.Context, s logical.Storage) { 94 b.Logger().Info("starting periodic ticker") 95 tick := time.NewTicker(queueTickInterval) 96 defer tick.Stop() 97 for { 98 select { 99 case <-tick.C: 100 b.rotateCredentials(ctx, s) 101 102 case <-ctx.Done(): 103 b.Logger().Info("stopping periodic ticker") 104 return 105 } 106 } 107} 108 109// setCredentialsWAL is used to store information in a WAL that can retry a 110// credential setting or rotation in the event of partial failure. 111type setCredentialsWAL struct { 112 NewPassword string `json:"new_password"` 113 OldPassword string `json:"old_password"` 114 RoleName string `json:"role_name"` 115 Username string `json:"username"` 116 DN string `json:"dn"` 117 118 LastVaultRotation time.Time `json:"last_vault_rotation"` 119 120 walID string 121} 122 123// rotateCredentials sets a new password for a static account. This method is 124// invoked in the runTicker method, which is in it's own go-routine, and invoked 125// periodically (approximately every 5 seconds). 126// 127// This method loops through the priority queue, popping the highest priority 128// item until it encounters the first item that does not yet need rotation, 129// based on the current time. 130func (b *backend) rotateCredentials(ctx context.Context, s logical.Storage) { 131 for b.rotateCredential(ctx, s) { 132 } 133} 134 135func (b *backend) rotateCredential(ctx context.Context, s logical.Storage) bool { 136 // Quit rotating credentials if shutdown has started 137 select { 138 case <-ctx.Done(): 139 return false 140 default: 141 } 142 item, err := b.popFromRotationQueue() 143 if err != nil { 144 if err != queue.ErrEmpty { 145 b.Logger().Error("error popping item from queue", "err", err) 146 } 147 return false 148 } 149 150 // Guard against possible nil item 151 if item == nil { 152 return false 153 } 154 155 // Grab the exclusive lock for this Role, to make sure we don't incur and 156 // writes during the rotation process 157 lock := locksutil.LockForKey(b.roleLocks, item.Key) 158 lock.Lock() 159 defer lock.Unlock() 160 161 // Validate the role still exists 162 role, err := b.staticRole(ctx, s, item.Key) 163 if err != nil { 164 b.Logger().Error("unable to load role", "role", item.Key, "error", err) 165 item.Priority = time.Now().Add(10 * time.Second).Unix() 166 if err := b.pushItem(item); err != nil { 167 b.Logger().Error("unable to push item on to queue", "error", err) 168 } 169 return true 170 } 171 if role == nil { 172 b.Logger().Warn("role not found", "role", item.Key, "error", err) 173 return true 174 } 175 176 // If "now" is less than the Item priority, then this item does not need to 177 // be rotated 178 if time.Now().Unix() < item.Priority { 179 if err := b.pushItem(item); err != nil { 180 b.Logger().Error("unable to push item on to queue", "error", err) 181 } 182 // Break out of the for loop 183 return false 184 } 185 186 input := &setStaticAccountInput{ 187 RoleName: item.Key, 188 Role: role, 189 } 190 191 // If there is a WAL entry related to this Role, the corresponding WAL ID 192 // should be stored in the Item's Value field. 193 if walID, ok := item.Value.(string); ok { 194 walEntry, err := b.findStaticWAL(ctx, s, walID) 195 if err != nil { 196 b.Logger().Error("error finding static WAL", "error", err) 197 item.Priority = time.Now().Add(10 * time.Second).Unix() 198 if err := b.pushItem(item); err != nil { 199 b.Logger().Error("unable to push item on to queue", "error", err) 200 } 201 } 202 if walEntry != nil && walEntry.NewPassword != "" { 203 input.Password = walEntry.NewPassword 204 input.WALID = walID 205 } 206 } 207 208 resp, err := b.setStaticAccountPassword(ctx, s, input) 209 if err != nil { 210 b.Logger().Error("unable to rotate credentials in periodic function", "error", err) 211 // Increment the priority enough so that the next call to this method 212 // likely will not attempt to rotate it, as a back-off of sorts 213 item.Priority = time.Now().Add(10 * time.Second).Unix() 214 215 // Preserve the WALID if it was returned 216 if resp != nil && resp.WALID != "" { 217 item.Value = resp.WALID 218 } 219 220 if err := b.pushItem(item); err != nil { 221 b.Logger().Error("unable to push item on to queue", "error", err) 222 } 223 // Go to next item 224 return true 225 } 226 227 lvr := resp.RotationTime 228 if lvr.IsZero() { 229 lvr = time.Now() 230 } 231 232 // Update priority and push updated Item to the queue 233 nextRotation := lvr.Add(role.StaticAccount.RotationPeriod) 234 item.Priority = nextRotation.Unix() 235 if err := b.pushItem(item); err != nil { 236 b.Logger().Warn("unable to push item on to queue", "error", err) 237 } 238 return true 239} 240 241// findStaticWAL loads a WAL entry by ID. If found, only return the WAL if it 242// is of type staticWALKey, otherwise return nil 243func (b *backend) findStaticWAL(ctx context.Context, s logical.Storage, id string) (*setCredentialsWAL, error) { 244 wal, err := framework.GetWAL(ctx, s, id) 245 if err != nil { 246 return nil, err 247 } 248 249 if wal == nil || wal.Kind != staticWALKey { 250 return nil, nil 251 } 252 253 data := wal.Data.(map[string]interface{}) 254 walEntry := setCredentialsWAL{ 255 walID: id, 256 NewPassword: data["new_password"].(string), 257 OldPassword: data["old_password"].(string), 258 RoleName: data["role_name"].(string), 259 Username: data["username"].(string), 260 DN: data["dn"].(string), 261 } 262 lvr, err := time.Parse(time.RFC3339, data["last_vault_rotation"].(string)) 263 if err != nil { 264 return nil, err 265 } 266 walEntry.LastVaultRotation = lvr 267 268 return &walEntry, nil 269} 270 271type setStaticAccountInput struct { 272 RoleName string 273 Role *roleEntry 274 Password string 275 CreateUser bool 276 WALID string 277} 278 279type setStaticAccountOutput struct { 280 RotationTime time.Time 281 Password string 282 // Optional return field, in the event WAL was created and not destroyed 283 // during the operation 284 WALID string 285} 286 287// setStaticAccountPassword sets the password for a static account associated with a 288// Role. This method does many things: 289// - verifies role exists and is in the allowed roles list 290// - loads an existing WAL entry if WALID input is given, otherwise creates a 291// new WAL entry 292// - gets a database connection 293// - accepts an input password, otherwise generates a new one via gRPC to the 294// database plugin 295// - sets new password for the static account 296// - uses WAL for ensuring passwords are not lost if storage to Vault fails 297// 298// This method does not perform any operations on the priority queue. Those 299// tasks must be handled outside of this method. 300func (b *backend) setStaticAccountPassword(ctx context.Context, s logical.Storage, input *setStaticAccountInput) (*setStaticAccountOutput, error) { 301 var merr error 302 if input == nil || input.Role == nil || input.RoleName == "" { 303 return nil, errors.New("input was empty when attempting to set credentials for static account") 304 } 305 306 if _, hasTimeout := ctx.Deadline(); !hasTimeout { 307 var cancel func() 308 ctx, cancel = context.WithTimeout(ctx, defaultCtxTimeout) 309 defer cancel() 310 } 311 312 // Re-use WAL ID if present, otherwise PUT a new WAL 313 output := &setStaticAccountOutput{WALID: input.WALID} 314 315 config, err := readConfig(ctx, s) 316 if err != nil { 317 return nil, err 318 } 319 if config == nil { 320 return nil, errors.New("the config is currently unset") 321 } 322 323 newPassword, err := b.GeneratePassword(ctx, config) 324 if err != nil { 325 return nil, err 326 } 327 328 oldPassword := input.Role.StaticAccount.Password 329 330 // Take out the backend lock since we are swapping out the connection 331 b.Lock() 332 defer b.Unlock() 333 334 if output.WALID == "" { 335 output.WALID, err = framework.PutWAL(ctx, s, staticWALKey, &setCredentialsWAL{ 336 RoleName: input.RoleName, 337 Username: input.Role.StaticAccount.Username, 338 DN: input.Role.StaticAccount.DN, 339 NewPassword: newPassword, 340 OldPassword: oldPassword, 341 LastVaultRotation: input.Role.StaticAccount.LastVaultRotation, 342 }) 343 if err != nil { 344 return output, errwrap.Wrapf("error writing WAL entry: {{err}}", err) 345 } 346 } 347 348 // Update the password remotely. 349 if err := b.client.UpdatePassword(config.LDAP, input.Role.StaticAccount.DN, newPassword); err != nil { 350 return nil, err 351 } 352 353 // Update the password locally. 354 if pwdStoringErr := storePassword(ctx, s, config); pwdStoringErr != nil { 355 // We were unable to store the new password locally. We can't continue in this state because we won't be able 356 // to roll any passwords, including our own to get back into a state of working. So, we need to roll back to 357 // the last password we successfully got into storage. 358 if rollbackErr := b.rollBackPassword(ctx, config, oldPassword); rollbackErr != nil { 359 return nil, fmt.Errorf(`unable to store new password due to %s and unable to return to previous password due 360to %s, configure a new binddn and bindpass to restore openldap function`, pwdStoringErr, rollbackErr) 361 } 362 return nil, fmt.Errorf("unable to update password due to storage err: %s", pwdStoringErr) 363 } 364 365 // Store updated role information 366 // lvr is the known LastVaultRotation 367 lvr := time.Now() 368 input.Role.StaticAccount.LastVaultRotation = lvr 369 input.Role.StaticAccount.Password = newPassword 370 output.RotationTime = lvr 371 372 entry, err := logical.StorageEntryJSON(staticRolePath+input.RoleName, input.Role) 373 if err != nil { 374 return output, err 375 } 376 if err := s.Put(ctx, entry); err != nil { 377 return output, err 378 } 379 380 // Cleanup WAL after successfully rotating and pushing new item on to queue 381 if err := framework.DeleteWAL(ctx, s, output.WALID); err != nil { 382 merr = multierror.Append(merr, err) 383 return output, merr 384 } 385 386 // The WAL has been deleted, return new setStaticAccountOutput without it 387 return &setStaticAccountOutput{RotationTime: lvr}, merr 388} 389 390func (b *backend) GeneratePassword(ctx context.Context, cfg *config) (string, error) { 391 if cfg.PasswordPolicy == "" { 392 if cfg.PasswordLength == 0 { 393 return base62.Random(defaultPasswordLength) 394 } 395 return base62.Random(cfg.PasswordLength) 396 } 397 398 password, err := b.System().GeneratePasswordFromPolicy(ctx, cfg.PasswordPolicy) 399 if err != nil { 400 return "", fmt.Errorf("unable to generate password: %w", err) 401 } 402 return password, nil 403} 404 405// initQueue preforms the necessary checks and initializations needed to preform 406// automatic credential rotation for roles associated with static accounts. This 407// method verifies if a queue is needed (primary server or local mount), and if 408// so initializes the queue and launches a go-routine to periodically invoke a 409// method to preform the rotations. 410// 411// initQueue is invoked by the Factory method in a go-routine. The Factory does 412// not wait for success or failure of it's tasks before continuing. This is to 413// avoid blocking the mount process while loading and evaluating existing roles, 414// etc. 415func (b *backend) initQueue(ctx context.Context, conf *logical.InitializationRequest) { 416 // Verify this mount is on the primary server, or is a local mount. If not, do 417 // not create a queue or launch a ticker. Both processing the WAL list and 418 // populating the queue are done sequentially and before launching a 419 // go-routine to run the periodic ticker. 420 replicationState := b.System().ReplicationState() 421 if (b.System().LocalMount() || !replicationState.HasState(consts.ReplicationPerformanceSecondary)) && 422 !replicationState.HasState(consts.ReplicationDRSecondary) && 423 !replicationState.HasState(consts.ReplicationPerformanceStandby) { 424 b.Logger().Info("initializing database rotation queue") 425 426 // Load roles and populate queue with static accounts 427 b.populateQueue(ctx, conf.Storage) 428 429 // Launch ticker 430 go b.runTicker(ctx, conf.Storage) 431 } 432} 433 434// loadStaticWALs reads WAL entries and returns a map of roles and their 435// setCredentialsWAL, if found. 436func (b *backend) loadStaticWALs(ctx context.Context, s logical.Storage) (map[string]*setCredentialsWAL, error) { 437 keys, err := framework.ListWAL(ctx, s) 438 if err != nil { 439 return nil, err 440 } 441 if len(keys) == 0 { 442 b.Logger().Debug("no WAL entries found") 443 return nil, nil 444 } 445 446 walMap := make(map[string]*setCredentialsWAL) 447 // Loop through WAL keys and process any rotation ones 448 for _, walID := range keys { 449 walEntry, err := b.findStaticWAL(ctx, s, walID) 450 if err != nil { 451 b.Logger().Error("error loading static WAL", "id", walID, "error", err) 452 continue 453 } 454 if walEntry == nil { 455 continue 456 } 457 458 // Verify the static role still exists 459 roleName := walEntry.RoleName 460 role, err := b.staticRole(ctx, s, roleName) 461 if err != nil { 462 b.Logger().Warn("unable to read static role", "error", err, "role", roleName) 463 continue 464 } 465 if role == nil || role.StaticAccount == nil { 466 if err := framework.DeleteWAL(ctx, s, walEntry.walID); err != nil { 467 b.Logger().Warn("unable to delete WAL", "error", err, "WAL ID", walEntry.walID) 468 } 469 continue 470 } 471 472 walEntry.walID = walID 473 walMap[walEntry.RoleName] = walEntry 474 } 475 return walMap, nil 476} 477 478// pushItem wraps the internal queue's Push call, to make sure a queue is 479// actually available. This is needed because both runTicker and initQueue 480// operate in go-routines, and could be accessing the queue concurrently 481func (b *backend) pushItem(item *queue.Item) error { 482 b.RLock() 483 defer b.RUnlock() 484 485 if b.credRotationQueue != nil { 486 return b.credRotationQueue.Push(item) 487 } 488 489 b.Logger().Warn("no queue found during push item") 490 return nil 491} 492 493// popFromRotationQueue wraps the internal queue's Pop call, to make sure a queue is 494// actually available. This is needed because both runTicker and initQueue 495// operate in go-routines, and could be accessing the queue concurrently 496func (b *backend) popFromRotationQueue() (*queue.Item, error) { 497 b.RLock() 498 defer b.RUnlock() 499 if b.credRotationQueue != nil { 500 return b.credRotationQueue.Pop() 501 } 502 return nil, queue.ErrEmpty 503} 504 505// popFromRotationQueueByKey wraps the internal queue's PopByKey call, to make sure a queue is 506// actually available. This is needed because both runTicker and initQueue 507// operate in go-routines, and could be accessing the queue concurrently 508func (b *backend) popFromRotationQueueByKey(name string) (*queue.Item, error) { 509 b.RLock() 510 defer b.RUnlock() 511 if b.credRotationQueue != nil { 512 item, err := b.credRotationQueue.PopByKey(name) 513 if err != nil { 514 return nil, err 515 } 516 if item != nil { 517 return item, nil 518 } 519 } 520 return nil, queue.ErrEmpty 521} 522