-
Notifications
You must be signed in to change notification settings - Fork 5
/
rotate.go
767 lines (677 loc) · 25.4 KB
/
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
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
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
// Copyright 2020, Square, Inc.
package rotate
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"os"
"path"
"runtime"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/secretsmanager"
"github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface"
"github.com/square/password-rotation-lambda/v2/db"
)
const (
AWSCURRENT = "AWSCURRENT"
AWSPENDING = "AWSPENDING"
AWSPREVIOUS = "AWSPREVIOUS"
)
func init() {
// Don't need data/time because CloudWatch Logs adds it, avoid redundant output:
// 2020-12-17T15:28:55.547-05:00 2020/12/17 20:28:55.547200 setter.go:79: Init call
// First timestamp from CloudWatch, second from log.
log.SetFlags(log.Lshortfile)
}
var (
// ErrInvalidStep is returned if the "Step" value in the Secrets Manager event
// is not one of "createSecret", "setSecret", "testSecret", or "finishSecret".
ErrInvalidStep = errors.New("invalid Step value from event")
)
// Config represents the user-provided configuration for a Rotator.
type Config struct {
// SecretsManager is an AWS Secrets Manager client. Create one by calling
// secretsmanager.New() using package github.com/aws/aws-sdk-go/service/secretsmanager.
// See https://pkg.go.dev/github.com/aws/[email protected]/service/secretsmanager?tab=doc#SecretsManager
// for more details. The client implements this data type.
SecretsManager secretsmanageriface.SecretsManagerAPI
// SecretSetter manages the secret value and rotates the password. This is
// the most important user-provided object. If none is provided, RandomPassword
// is used. See SecretSetter for more details.
SecretSetter SecretSetter
// PasswordSetter sets the new, rotated password on databases. Implementations
// are provided in the db/ directory.
PasswordSetter db.PasswordSetter
// SkipDatabase skips setting the the new, rotated password on databases if true
// but does all the other work. If there is a database issue that blocks a
// Secrets Manager rotation, this lets the Secrets Manager rotation complete.
// Normally, this should be false; only set to true when knowingly fixing an issue
// that requires it.
SkipDatabase bool
// EventReceiver receives events during the four-step password rotation process.
// If none is provided, NullEventReceiver is used. See EventReceiver for more details.
EventReceiver EventReceiver
// ReplicationWait governs the duration password rotation lambda will wait for
// secret replication to secondary regions to complete
ReplicationWait time.Duration
}
// InvokedBySecretsManager returns true if the event is from Secrets Manager.
func InvokedBySecretsManager(event map[string]string) bool {
_, haveToken := event["ClientRequestToken"]
_, haveSecretId := event["SecretId"]
_, haveStep := event["Step"]
return haveToken && haveSecretId && haveStep
}
// Generic return error because errors are logged when/whey they occur so the log
// output in CloudWatch Logs reads in the correct order. Lambda logs the return
// error last, of course, which makes it appear after "SetSecret return:" logs in
// from defer funcs.
var errRotationFailed = errors.New("Password rotation failed, see previous log output")
// Rotator is the AWS Lambda function and handler. Create a new Rotator by
// calling NewRotator, then use it in your main.go by calling lambda.Start(r.Handler)
// where "r" is the new Rotator. See the documentation and examples for more details.
//
// Currently, only secret string, not secret binary, is used and it must be
// a JSON string with key-value pairs. See SecretSetter for details.
type Rotator struct {
sm secretsmanageriface.SecretsManagerAPI
ss SecretSetter
db db.PasswordSetter
event EventReceiver
skipDb bool
// --
clientRequestToken string
secretId string
startTime time.Time
replicationWait time.Duration
}
// NewRotator creates a new Rotator.
func NewRotator(cfg Config) *Rotator {
event := cfg.EventReceiver
if event == nil {
event = NullEventReceiver{}
}
if cfg.EventReceiver == nil {
cfg.EventReceiver = NullEventReceiver{}
}
ss := cfg.SecretSetter
if ss == nil {
ss = RandomPassword{}
}
return &Rotator{
sm: cfg.SecretsManager,
db: cfg.PasswordSetter,
ss: ss,
event: event,
skipDb: cfg.SkipDatabase,
replicationWait: cfg.ReplicationWait,
}
}
// Handler is the entry point for every invocation. This function is hooked into
// the Lambda framework by calling lambda.Start(r.Handler) where "r" is the Rotator
// returned by NewRotator.
//
// Use only this function. The other Rotator functions are exported only for testing.
func (r *Rotator) Handler(ctx context.Context, event map[string]string) (map[string]string, error) {
if !InvokedBySecretsManager(event) {
debug("user event: %+v", event)
return r.ss.Handler(ctx, event)
}
debug("Secrets Manager event: %+v", event)
// Initialize user-provided SecretSetter and PasswordSetter. On first call
// (invocation), these should set up any internal data, e.g. find and connect
// to all the db instances. These must be idempotent because we don't know
// if the lambda is resuming or not.
if err := r.ss.Init(ctx, event); err != nil {
return nil, err
}
if err := r.db.Init(ctx, event); err != nil {
return nil, err
}
r.clientRequestToken = event["ClientRequestToken"]
r.secretId = event["SecretId"]
step := event["Step"]
var err error
switch step {
case "createSecret":
err = r.CreateSecret(ctx, event)
case "setSecret":
err = r.SetSecret(ctx, event)
case "testSecret":
err = r.TestSecret(ctx, event)
case "finishSecret":
err = r.FinishSecret(ctx, event)
default:
return nil, ErrInvalidStep
}
if err != nil {
r.event.Receive(Event{
Name: EVENT_ERROR,
Time: time.Now(),
Step: step,
Error: err,
})
}
return nil, err
}
// CreateSecret is the first step in the Secrets Manager rotation process.
//
// Do not call this function directly. It is exported only for testing.
func (r *Rotator) CreateSecret(ctx context.Context, event map[string]string) error {
t0 := time.Now()
log.Println("CreateSecret call")
defer func() {
d := time.Now().Sub(t0)
log.Printf("CreateSecret return: %dms", d.Milliseconds())
}()
/*
In the simplest case, there's one secret with AWSCURRENT and nothing
else (we can ignore any secrets without stages or ASWPREVIOUS). So we
create our new secret with the AWSPENDING staging label and we're done.
But in the real world we have to handle various edge cases:
1) Current secret has pending label because:
(Optional) Remove the label AWSPENDING from its version of the secret.
https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotating-secrets-one-user-one-password.html
This is ok as long as current secret and our (new) secret have different
version IDs, i.e. they're different secrets.
2) There's a pending secret and it's ours. This can happen because this
(and every) step can be retried. This is ok, too, as long as the pending
secret is our secret (i.e. have same version ID). We can re-create because
it's idempotent as long as the secret values are the same.
3) There's a pending secret and it's _not_ ours. This is an unrecoverable
error. It could happen if another process tries to rotate the same secret
at the same time.
4) Our secret is already current. This shouldn't happen, but it could
if someone manually changes staging labels with the AWS CLI.
"Our secret" = r.clientRequestToken, i.e. it's identified by the client
request token which "becomes the SecretVersionId of the new version of
the secret."
https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotating-secrets-lambda-function-overview.html
*/
r.event.Receive(Event{
Name: EVENT_BEGIN_ROTATION,
Step: "createSecret",
Time: time.Now(),
})
// Get current secret
curSec, curVals, err := r.getSecret(AWSCURRENT)
if err != nil {
return err
}
// Case 4:
// Is our secret the current secret? It shouldn't be.
if r.clientRequestToken == *curSec.VersionId {
return fmt.Errorf("new and current secret have the same version ID: %s; expected different values", r.clientRequestToken)
}
// Case 1:
// Does the current secret also have the pending stage? It can because
// removing it from previous runs is optional. Loop through the current's
// stages to check for pending.
currentHasPending := false
for _, label := range curSec.VersionStages {
if *label == AWSPENDING {
debug("current secret has AWSPENDING stage")
currentHasPending = true
break
}
}
// If the current doesn't have pending label, then check for any pending secret.
// If there's _not_ a pending secret, then we'll rotate the current secret.
// If there is a pending secret and it's ours (due to a retry), then we'll
// use its values and not rotate.
if !currentHasPending {
penSec, _, err := r.getSecret(AWSPENDING)
if err != nil {
if aerr, ok := err.(awserr.Error); ok && aerr.Code() == secretsmanager.ErrCodeResourceNotFoundException {
// *** The simplest case ***
// No secret has the pending staging label. This is probably the
// very first invocation, so we can just create our new secret.
debug("no pending secret, will rotate current secret")
} else {
return err
}
} else {
if *penSec.VersionId == r.clientRequestToken {
// Case 2:
// There's a pending secret and it's our. This must be a retry.
// We do not and cannot rotate the values, else PutSecretValue
// will error. It's only idempotent with the same values.
debug("using pending secret, will not rotate")
// Return early, nothing more to do. Code below is for rotating
// current values, but we already did that in previous try.
return nil
} else {
// Case 3:
// There's a pending secret and it's not ours. Something (or someone)
// else is rotating this secret at the same time.
debug("pending secret has different version id = %s", *penSec.VersionId)
return fmt.Errorf("another pending secret exists (version ID %s); "+
" another process might be rotating this secret, or a previous rotation failed without cleaning up", *penSec.VersionId)
}
}
}
// ----------------------------------------------------------------------
// Code reaches here if current has pending or no secret has pending.
// This is normal case when we need to create new pending secret from
// rotated current values.
debug("rotating current secret")
// MUST COPY curVals to avoid changing cache (r.secets.values) because
// r.ss.Rotate() modifies the map
newVals := map[string]string{}
for k, v := range curVals {
newVals[k] = v
}
// Have user-provided SecretSetter rotate the secret. Normally, it should
// just change the password, but it's free to change any secret values.
if err := r.ss.Rotate(newVals); err != nil {
return err
}
debugSecret("new secret values: %v", newVals)
// Convert secret JSON to string
bytes, err := json.Marshal(newVals)
if err != nil {
return err
}
// Set new secret as PENDING, i.e. current secret has not changed yet.
// We'll set and test the new secret values in the next two steps.
output, err := r.sm.PutSecretValue(&secretsmanager.PutSecretValueInput{
ClientRequestToken: aws.String(r.clientRequestToken),
SecretId: aws.String(r.secretId),
SecretString: aws.String(string(bytes)),
VersionStages: []*string{aws.String(AWSPENDING)}, // must be AWSPENDING
})
if err != nil {
return err
}
log.Printf("new pending secret metadata: %+v", *output)
return nil
}
// SetSecret is the second step in the Secrets Manager rotation process.
//
// Do not call this function directly. It is exported only for testing.
func (r *Rotator) SetSecret(ctx context.Context, event map[string]string) error {
t0 := time.Now()
log.Println("SetSecret call")
defer func() {
d := time.Now().Sub(t0)
log.Printf("SetSecret return: %dms", d.Milliseconds())
}()
if r.skipDb {
log.Println("SkipDatabase is enabled, not rotating password on database")
return nil
}
// Get new, pending secret values from previous (first) step. Then have
// user-provided SecretSetter return the new user and pass from the secret.
_, newVals, err := r.getSecret(AWSPENDING)
if err != nil {
return err
}
newUsername, newPassword := r.ss.Credentials(newVals)
// And get current secret values in case setting new fails and we to roll back
_, curVals, err := r.getSecret(AWSCURRENT)
if err != nil {
return err
}
curUsername, curPassword := r.ss.Credentials(curVals)
curCred := db.Credentials{
Username: curUsername,
Password: curPassword,
}
newCred := db.Credentials{
Username: newUsername,
Password: newPassword,
}
// Combine the current and new credentials. This is plumbed all the way down
// into the db.PassswordSetter implementation.
creds := db.NewPassword{
Current: curCred,
New: newCred,
}
debugSecret("db credentials: %+v", creds)
// Check to see if DB is already set to Pending password.
// This can happen if there's a previous run of the lambda crashed
// in TestSecret or FinishSecret steps.
// Treat this as if SetPassword has completed successfully.
log.Println("Verifying if DB is already set to AWSPENDING version of secret")
if err := r.db.VerifyPassword(ctx, creds); err == nil {
r.event.Receive(Event{
Name: EVENT_END_PASSWORD_ROTATION,
Step: "setSecret",
Time: r.startTime,
})
log.Println("DB is already set to AWSPENDING version of secret, no action")
return nil
}
// Verify that credentials are valid before attempting to update secrets
// this is to guard against scenarios that DB is in mismatch state with secret managers
// AWSCURRENT version of the secret. A couple of example of this is
// 1. Manual update of password in DB
// 2. Secret Manager secret is changed manually
log.Println("Verifying if AWSCURRENT version of secret is valid")
if err := r.db.VerifyPassword(ctx, db.NewPassword{Current: curCred, New: curCred}); err != nil {
log.Printf("ERROR: DB is not set to AWSCURRENT version of secret, attempting to verify AWSPREVIOUS version: %v", err)
// the current version of secret is out of sync with db. check if db is in sync with
// the previous version of the secret
_, prevVals, err := r.getSecret(AWSPREVIOUS)
if err != nil {
r.event.Receive(Event{
Name: EVENT_BEGIN_PASSWORD_ROLLBACK,
Step: "setSecret",
Time: time.Now(),
})
log.Printf("ERROR: unable to retreive previous version of the credential. %v "+
" starting rollback", err)
// calling rollback to remove AWSPENDING Label.
return r.rollback(ctx, creds, "SetSecret")
}
prevUsername, prevPassword := r.ss.Credentials(prevVals)
prevCred := db.Credentials{
Username: prevUsername,
Password: prevPassword,
}
if err := r.db.VerifyPassword(ctx, db.NewPassword{Current: prevCred, New: prevCred}); err != nil {
r.event.Receive(Event{
Name: EVENT_BEGIN_PASSWORD_ROLLBACK,
Step: "setSecret",
Time: time.Now(),
})
log.Printf("ERROR: all versions of credentials in secret manager is out of sync with db; %v starting rollback", err)
// calling rollback to remove AWSPENDING Label.
return r.rollback(ctx, creds, "SetSecret")
}
// update creds used for setting password since we've confirmed that DB is set to previousVersion of secrets
creds = db.NewPassword{
Current: prevCred,
New: newCred,
}
log.Println("DB is set to AWSPREVIOUS version of secret")
}
// Have user-provided PasswordSetter set database password to new value.
// Normally, this is when the database password actually changes.
// The PasswordSetter is responsible for knowing which db instances to change.
// mysql.PasswordSetter, for example, sets every RDS instance in parallel.
r.startTime = time.Now()
r.event.Receive(Event{
Name: EVENT_BEGIN_PASSWORD_ROTATION,
Step: "setSecret",
Time: r.startTime,
})
if err := r.db.SetPassword(ctx, creds); err != nil {
// Roll back to original password since setting the new password failed.
// Depending on how the PasswordSetter is configured, this might be a no-op.
// Normally, we want to roll back so all dbs instances have the same
// password for the given user.
log.Printf("ERROR: SetPassword failed, rollback: %s", err)
r.event.Receive(Event{
Name: EVENT_BEGIN_PASSWORD_ROLLBACK,
Step: "setSecret",
Time: time.Now(),
})
return r.rollback(ctx, creds, "SetSecret")
}
r.event.Receive(Event{
Name: EVENT_END_PASSWORD_ROTATION,
Step: "setSecret",
Time: r.startTime,
})
// At this point, the db password has been changed, but AWS Secrets Manager
// still returns the old password. The next step verifies the new password,
// and the fourth and final step makes the new password current in Secrets Manager.
return nil
}
// TestSecret is the third step in the Secrets Manager rotation process.
//
// Do not call this function directly. It is exported only for testing.
func (r *Rotator) TestSecret(ctx context.Context, event map[string]string) error {
t0 := time.Now()
log.Println("TestSecret call")
defer func() {
d := time.Now().Sub(t0)
log.Printf("TestSecret return: %dms", d.Milliseconds())
}()
if r.skipDb {
log.Println("SkipDatabase is enabled, not verifying password on database")
return nil
}
// Get new, pending secret values from previous (first) step. Then have
// user-provided SecretSetter return the new user and pass from the secret.
_, newVals, err := r.getSecret(AWSPENDING)
if err != nil {
return err
}
newUsername, newPassword := r.ss.Credentials(newVals)
// And get current secret values in case setting new fails and we to roll back
_, curVals, err := r.getSecret(AWSCURRENT)
if err != nil {
return err
}
curUsername, curPassword := r.ss.Credentials(curVals)
// Combine the current and new credentials. This is plumbed all the way down
// into the db.PassswordSetter implementation.
creds := db.NewPassword{
Current: db.Credentials{
Username: curUsername,
Password: curPassword,
},
New: db.Credentials{
Username: newUsername,
Password: newPassword,
},
}
debugSecret("db credentials: %+v", creds)
// Have user-provided PasswordSetter verify that new database password works
r.event.Receive(Event{
Name: EVENT_BEGIN_PASSWORD_VERIFICATION,
Step: "testSecret",
Time: time.Now(),
})
if err := r.db.VerifyPassword(ctx, creds); err != nil {
// Roll back to original password since new password doesn't work
log.Printf("ERROR: VerifyPassword failed, rollback: %s", err)
r.event.Receive(Event{
Name: EVENT_BEGIN_PASSWORD_ROLLBACK,
Step: "testSecret",
Time: time.Now(),
})
return r.rollback(ctx, creds, "TestSecret")
}
r.event.Receive(Event{
Name: EVENT_END_PASSWORD_VERIFICATION,
Step: "testSecret",
Time: time.Now(),
})
// At this point, AWS Secrets Manager still returns the old password.
// The next and final step makes the new password current in Secrets Manager.
return nil
}
// FinishSecret is the fourth and final step in the Secrets Manager rotation process.
//
// Do not call this function directly. It is exported only for testing.
func (r *Rotator) FinishSecret(ctx context.Context, event map[string]string) error {
t0 := time.Now()
log.Println("FinishSecret call")
defer func() {
d := time.Now().Sub(t0)
log.Printf("FinishSecret return: %dms", d.Milliseconds())
}()
// Get current and new secrets so we can move the AWSPENDING/CURRENT label
// by secret ID
curSecret, _, err := r.getSecret(AWSCURRENT)
if err != nil {
return err
}
newSecret, _, err := r.getSecret(AWSPENDING)
if err != nil {
return err
}
// Move AWSCURRENT label from the current secret to the new. This makes the
// new secret current and automatically labels the old secret "previous".
debug("moving AWSCURRENT from version id = %v to version id = %v", *curSecret.VersionId, *newSecret.VersionId)
_, err = r.sm.UpdateSecretVersionStage(&secretsmanager.UpdateSecretVersionStageInput{
SecretId: aws.String(r.secretId),
RemoveFromVersionId: curSecret.VersionId,
MoveToVersionId: newSecret.VersionId,
VersionStage: aws.String(AWSCURRENT),
})
if err != nil {
return err
}
now := time.Now()
r.event.Receive(Event{
Name: EVENT_NEW_PASSWORD_IS_CURRENT,
Step: "finishSecret",
Time: now,
})
downtime := now.Sub(r.startTime)
log.Printf("password downtime: %dms", downtime.Milliseconds())
// Wait for secret replication to complete to all replica regions
err = r.checkSecretReplicationStatus()
if err != nil {
return err
}
// Remove AWSPENDING label
debug("removing AWSPENDING from version id = %v", *newSecret.VersionId)
_, err = r.sm.UpdateSecretVersionStage(&secretsmanager.UpdateSecretVersionStageInput{
SecretId: aws.String(r.secretId),
RemoveFromVersionId: newSecret.VersionId,
VersionStage: aws.String(AWSPENDING),
})
if err != nil {
log.Println(err)
}
r.event.Receive(Event{
Name: EVENT_END_ROTATION,
Step: "finishSecret",
Time: time.Now(),
})
return nil
}
// --------------------------------------------------------------------------
func (r *Rotator) getSecret(stage string) (*secretsmanager.GetSecretValueOutput, map[string]string, error) {
// Fetch secret from Secrets Manager
s, err := r.sm.GetSecretValue(&secretsmanager.GetSecretValueInput{
SecretId: aws.String(r.secretId),
VersionStage: aws.String(stage),
})
if err != nil {
return nil, nil, err
}
debug("%s stage %s version %v", r.secretId, stage, *s.VersionId)
if s.SecretString == nil || *s.SecretString == "" {
return s, nil, fmt.Errorf("secret string is nil or empty string; " +
"it must be valid JSON like '{\"username\":\"foo\",\"password\":\"bar\"}'")
}
var v map[string]string
if err := json.Unmarshal([]byte(*s.SecretString), &v); err != nil {
return nil, nil, err
}
if v == nil {
return s, nil, fmt.Errorf("secret string is 'null' literal; " +
"it must be valid JSON like '{\"username\":\"foo\",\"password\":\"bar\"}'")
}
debugSecret("%s secret values: %v", stage, *s.SecretString)
return s, v, nil
}
func (r *Rotator) rollback(ctx context.Context, creds db.NewPassword, rotationStep string) error {
if err := r.db.Rollback(ctx, creds); err != nil {
log.Printf("ERROR: Rollback failed: %s", err)
return errRotationFailed
}
// Remove pending secret and clear the cache, i.e. roll back Secrets Manager
// to point before this rotation
newSecret, _, err := r.getSecret(AWSPENDING)
if err != nil {
return err
}
debug("removing AWSPENDING from version id = %v", *newSecret.VersionId)
_, err = r.sm.UpdateSecretVersionStage(&secretsmanager.UpdateSecretVersionStageInput{
SecretId: aws.String(r.secretId),
RemoveFromVersionId: newSecret.VersionId,
VersionStage: aws.String(AWSPENDING),
})
if err != nil {
log.Printf("ERROR: failed to remove pending secret: %s", err)
return errRotationFailed
}
log.Printf("%s failed but rollback was successful", rotationStep)
return errRotationFailed // always return this error
}
// checks that secret have been replicated to all replica regions
// this is necessary between multiple calls of UpdateSecretVersionStage
// to guard against arace condition in AWS that leaves secret replication
// stuck indefinitely.
func (r *Rotator) checkSecretReplicationStatus() error {
log.Println("checking secret replication status")
waitDuration := DEFAULT_REPLICATION_WAIT
if r.replicationWait > 0 {
waitDuration = r.replicationWait
}
startTime := time.Now()
for time.Now().Sub(startTime) < waitDuration {
secret, err := r.sm.DescribeSecret(&secretsmanager.DescribeSecretInput{
SecretId: aws.String(r.secretId),
})
if err != nil {
return err
}
if secret == nil {
return fmt.Errorf("expected an non null secret for secretId %v but received null", r.secretId)
}
replicationSyncComplete := true
for _, status := range secret.ReplicationStatus {
if status == nil {
replicationSyncComplete = false
log.Println("encountered null replication status")
break
}
if *status.Status != secretsmanager.StatusTypeInSync {
replicationSyncComplete = false
log.Printf("replication status still in (%v) in region (%v) expecting (%v)\n", *status.Status, *status.Region, secretsmanager.StatusTypeInSync)
break
}
}
// only return success if all secret replica regions are in sync all
// other cases are treated as errors
if replicationSyncComplete {
log.Println("secret replication sync completed successfully")
return nil // success
}
time.Sleep(500 * time.Millisecond)
}
return fmt.Errorf("timeout waiting for secret replication StatusTypeInSync = true")
}
// --------------------------------------------------------------------------
var (
// Debug enables debug output to STDERR. It does not print secret values.
// Debug lines start with "DEBUG". AWS Lambda usually logs all output to CloudWatch Logs.
Debug = false
// DebugSecret IS DANGEROUS: it prints secret values to STDERR when Debug is enabled.
// If Debug is false (disabled), this value is ignored.
//
// Be very careful enabling this!
DebugSecret = false
debugLog = log.New(os.Stderr, "DEBUG ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile|log.LUTC)
// DEFAULT_REPLICATION_WAIT is the default duration that password rotation lambda will
// wait for secret replication to secondary regions to complete
DEFAULT_REPLICATION_WAIT = 30 * time.Second
)
func debugSecret(msg string, v ...interface{}) {
if !Debug || !DebugSecret {
return
}
_, file, line, _ := runtime.Caller(1)
msg = fmt.Sprintf("%s:%d %s", path.Base(file), line, msg)
debugLog.Printf(msg, v...)
}
func debug(msg string, v ...interface{}) {
if !Debug {
return
}
_, file, line, _ := runtime.Caller(1)
msg = fmt.Sprintf("%s:%d %s", path.Base(file), line, msg)
debugLog.Printf(msg, v...)
}