diff --git a/builtin/logical/database/path_roles_test.go b/builtin/logical/database/path_roles_test.go index 41a2e99758aa..0ad01efe02bc 100644 --- a/builtin/logical/database/path_roles_test.go +++ b/builtin/logical/database/path_roles_test.go @@ -1087,6 +1087,76 @@ func TestBackend_StaticRole_Role_name_check(t *testing.T) { } } +// TestStaticRole_NewCredentialGeneration verifies that new +// credentials are generated if a retried credential continues +// to fail +func TestStaticRole_NewCredentialGeneration(t *testing.T) { + ctx := context.Background() + b, storage, mockDB := getBackend(t) + defer b.Cleanup(ctx) + configureDBMount(t, storage) + + roleName := "hashicorp" + createRole(t, b, storage, mockDB, "hashicorp") + + t.Run("rotation failures should generate new password on retry", func(t *testing.T) { + // Fail to rotate the role + generateWALFromFailedRotation(t, b, storage, mockDB, roleName) + + // Get WAL + walIDs := requireWALs(t, storage, 1) + wal, err := b.findStaticWAL(ctx, storage, walIDs[0]) + if err != nil || wal == nil { + t.Fatal(err) + } + + // Store password + initialPassword := wal.NewPassword + + // Rotate role manually and fail again #1 with same password + generateWALFromFailedRotation(t, b, storage, mockDB, roleName) + + // Ensure WAL is deleted since retrying password failed + requireWALs(t, storage, 0) + + // Successfully rotate the role + mockDB.On("UpdateUser", mock.Anything, mock.Anything). + Return(v5.UpdateUserResponse{}, nil). + Once() + _, err = b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.UpdateOperation, + Path: "rotate-role/" + roleName, + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + + // Ensure WAL is flushed since request was successful + requireWALs(t, storage, 0) + + // Read the credential + data := map[string]interface{}{} + req := &logical.Request{ + Operation: logical.ReadOperation, + Path: "static-creds/" + roleName, + Storage: storage, + Data: data, + } + + resp, err := b.HandleRequest(namespace.RootContext(nil), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%s resp:%#v\n", err, resp) + } + + // Confirm successful rotation used new credential + // Assert previous failing credential is not being used + if resp.Data["password"] == initialPassword { + t.Fatalf("expected password to be different after second retry") + } + }) +} + func TestWALsStillTrackedAfterUpdate(t *testing.T) { ctx := context.Background() b, storage, mockDB := getBackend(t) diff --git a/builtin/logical/database/rotation.go b/builtin/logical/database/rotation.go index d4d41cf570b5..3d460915a6c0 100644 --- a/builtin/logical/database/rotation.go +++ b/builtin/logical/database/rotation.go @@ -421,6 +421,7 @@ func (b *databaseBackend) setStaticAccount(ctx context.Context, s logical.Storag // Use credential from input if available. This happens if we're restoring from // a WAL item or processing the rotation queue with an item that has a WAL // associated with it + var usedCredentialFromPreviousRotation bool if output.WALID != "" { wal, err := b.findStaticWAL(ctx, s, output.WALID) if err != nil { @@ -448,6 +449,7 @@ func (b *databaseBackend) setStaticAccount(ctx context.Context, s logical.Storag Statements: statements, } input.Role.StaticAccount.Password = wal.NewPassword + usedCredentialFromPreviousRotation = true case wal.CredentialType == v5.CredentialTypeRSAPrivateKey: // Roll forward by using the credential in the existing WAL entry updateReq.CredentialType = v5.CredentialTypeRSAPrivateKey @@ -456,6 +458,7 @@ func (b *databaseBackend) setStaticAccount(ctx context.Context, s logical.Storag Statements: statements, } input.Role.StaticAccount.PrivateKey = wal.NewPrivateKey + usedCredentialFromPreviousRotation = true } } @@ -530,6 +533,15 @@ func (b *databaseBackend) setStaticAccount(ctx context.Context, s logical.Storag _, err = dbi.database.UpdateUser(ctx, updateReq, false) if err != nil { b.CloseIfShutdown(dbi, err) + if usedCredentialFromPreviousRotation { + b.Logger().Debug("credential stored in WAL failed, deleting WAL", "role", input.RoleName, "WAL ID", output.WALID) + if err := framework.DeleteWAL(ctx, s, output.WALID); err != nil { + b.Logger().Warn("failed to delete WAL", "error", err, "WAL ID", output.WALID) + } + + // Generate a new WAL entry and credential for next attempt + output.WALID = "" + } return output, fmt.Errorf("error setting credentials: %w", err) } modified = true diff --git a/changelog/28989.txt b/changelog/28989.txt new file mode 100644 index 000000000000..2e5068baeaa8 --- /dev/null +++ b/changelog/28989.txt @@ -0,0 +1,3 @@ +```release-note:bug +secret/db: Update static role rotation to generate a new password after 2 failed attempts. +``` \ No newline at end of file