diff --git a/CHANGELOG.md b/CHANGELOG.md index 1653fe04bb65..5c0fbb829184 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +## 1.8.2 +### 26 August 2021 + +CHANGES: + +* go: Update go version to 1.16.7 [[GH-12408](https://github.com/hashicorp/vault/pull/12408)] + +BUG FIXES: + +* auth/aws: Fixes ec2 login no longer supporting DSA signature verification [[GH-12340](https://github.com/hashicorp/vault/pull/12340)] +* cli: vault debug now puts newlines after every captured log line. [[GH-12175](https://github.com/hashicorp/vault/pull/12175)] +* database/couchbase: change default template to truncate username at 128 characters [[GH-12300](https://github.com/hashicorp/vault/pull/12300)] +* identity: Fix a panic on arm64 platform when doing identity I/O. [[GH-12371](https://github.com/hashicorp/vault/pull/12371)] +* physical/raft: Fix safeio.Rename error when restoring snapshots on windows [[GH-12377](https://github.com/hashicorp/vault/pull/12377)] +* plugin/snowflake: Fixed bug where plugin would crash on 32 bit systems [[GH-12378](https://github.com/hashicorp/vault/pull/12378)] +* sdk/database: Fix a DeleteUser error message on the gRPC client. [[GH-12351](https://github.com/hashicorp/vault/pull/12351)] +* secrets/gcp: Fixes a potential panic in the service account policy rollback for rolesets. [[GH-12379](https://github.com/hashicorp/vault/pull/12379)] +* ui: Fixed api explorer routing bug [[GH-12354](https://github.com/hashicorp/vault/pull/12354)] +* ui: Fixes metrics page when read on counter config not allowed [[GH-12348](https://github.com/hashicorp/vault/pull/12348)] +* ui: fix issue where on MaskedInput on auth methods if tab it would clear the value. [[GH-12409](https://github.com/hashicorp/vault/pull/12409)] + ## 1.8.1 ### August 5th, 2021 diff --git a/api/sys_raft.go b/api/sys_raft.go index c66ae629e495..faa62eb3e0f9 100644 --- a/api/sys_raft.go +++ b/api/sys_raft.go @@ -1,21 +1,25 @@ package api import ( + "archive/tar" + "compress/gzip" "context" "encoding/json" "errors" "fmt" "io" + "io/ioutil" "net/http" + "sync" "time" + "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/helper/parseutil" - "github.com/mitchellh/mapstructure" - - "github.com/hashicorp/vault/sdk/helper/consts" ) +var ErrIncompleteSnapshot = errors.New("incomplete snapshot, unable to read SHA256SUMS.sealed file") + // RaftJoinResponse represents the response of the raft join API type RaftJoinResponse struct { Joined bool `json:"joined"` @@ -210,11 +214,60 @@ func (c *Sys) RaftSnapshot(snapWriter io.Writer) error { return err } - _, err = io.Copy(snapWriter, resp.Body) + // Make sure that the last file in the archive, SHA256SUMS.sealed, is present + // and non-empty. This is to catch cases where the snapshot failed midstream, + // e.g. due to a problem with the seal that prevented encryption of that file. + var wg sync.WaitGroup + wg.Add(1) + var verified bool + + rPipe, wPipe := io.Pipe() + dup := io.TeeReader(resp.Body, wPipe) + go func() { + defer func() { + io.Copy(ioutil.Discard, rPipe) + rPipe.Close() + wg.Done() + }() + + uncompressed, err := gzip.NewReader(rPipe) + if err != nil { + return + } + + t := tar.NewReader(uncompressed) + var h *tar.Header + for { + h, err = t.Next() + if err != nil { + return + } + if h.Name != "SHA256SUMS.sealed" { + continue + } + var b []byte + b, err = ioutil.ReadAll(t) + if err != nil || len(b) == 0 { + return + } + verified = true + return + } + }() + + // Copy bytes from dup to snapWriter. This will have a side effect that + // everything read from dup will be written to wPipe. + _, err = io.Copy(snapWriter, dup) + wPipe.Close() if err != nil { + rPipe.CloseWithError(err) return err } + wg.Wait() + if !verified { + return ErrIncompleteSnapshot + } return nil } diff --git a/builtin/logical/database/backend_test.go b/builtin/logical/database/backend_test.go index 83e8b87c3ae4..6d42fcbfd6e3 100644 --- a/builtin/logical/database/backend_test.go +++ b/builtin/logical/database/backend_test.go @@ -45,7 +45,7 @@ func getCluster(t *testing.T) (*vault.TestCluster, logical.SystemView) { os.Setenv(pluginutil.PluginCACertPEMEnv, cluster.CACertPEMFile) - sys := vault.TestDynamicSystemView(cores[0].Core) + sys := vault.TestDynamicSystemView(cores[0].Core, nil) vault.TestAddTestPlugin(t, cores[0].Core, "postgresql-database-plugin", consts.PluginTypeDatabase, "TestBackend_PluginMain_Postgres", []string{}, "") vault.TestAddTestPlugin(t, cores[0].Core, "mongodb-database-plugin", consts.PluginTypeDatabase, "TestBackend_PluginMain_Mongo", []string{}, "") vault.TestAddTestPlugin(t, cores[0].Core, "mongodbatlas-database-plugin", consts.PluginTypeDatabase, "TestBackend_PluginMain_MongoAtlas", []string{}, "") @@ -345,7 +345,7 @@ func TestBackend_BadConnectionString(t *testing.T) { } defer b.Cleanup(context.Background()) - cleanup, _ := postgreshelper.PrepareTestContainer(t, "latest") + cleanup, _ := postgreshelper.PrepareTestContainer(t, "13.4-buster") defer cleanup() respCheck := func(req *logical.Request) { @@ -394,7 +394,7 @@ func TestBackend_basic(t *testing.T) { } defer b.Cleanup(context.Background()) - cleanup, connURL := postgreshelper.PrepareTestContainer(t, "latest") + cleanup, connURL := postgreshelper.PrepareTestContainer(t, "13.4-buster") defer cleanup() // Configure a connection @@ -601,7 +601,7 @@ func TestBackend_connectionCrud(t *testing.T) { } defer b.Cleanup(context.Background()) - cleanup, connURL := postgreshelper.PrepareTestContainer(t, "latest") + cleanup, connURL := postgreshelper.PrepareTestContainer(t, "13.4-buster") defer cleanup() // Configure a connection @@ -789,7 +789,7 @@ func TestBackend_roleCrud(t *testing.T) { } defer b.Cleanup(context.Background()) - cleanup, connURL := postgreshelper.PrepareTestContainer(t, "latest") + cleanup, connURL := postgreshelper.PrepareTestContainer(t, "13.4-buster") defer cleanup() // Configure a connection @@ -1038,7 +1038,7 @@ func TestBackend_allowedRoles(t *testing.T) { } defer b.Cleanup(context.Background()) - cleanup, connURL := postgreshelper.PrepareTestContainer(t, "latest") + cleanup, connURL := postgreshelper.PrepareTestContainer(t, "13.4-buster") defer cleanup() // Configure a connection @@ -1235,7 +1235,7 @@ func TestBackend_RotateRootCredentials(t *testing.T) { } defer b.Cleanup(context.Background()) - cleanup, connURL := postgreshelper.PrepareTestContainer(t, "latest") + cleanup, connURL := postgreshelper.PrepareTestContainer(t, "13.4-buster") defer cleanup() connURL = strings.Replace(connURL, "postgres:secret", "{{username}}:{{password}}", -1) diff --git a/builtin/logical/database/dbplugin/plugin_test.go b/builtin/logical/database/dbplugin/plugin_test.go index 754f82b40fb7..e96f55deb274 100644 --- a/builtin/logical/database/dbplugin/plugin_test.go +++ b/builtin/logical/database/dbplugin/plugin_test.go @@ -109,7 +109,7 @@ func getCluster(t *testing.T) (*vault.TestCluster, logical.SystemView) { cluster.Start() cores := cluster.Cores - sys := vault.TestDynamicSystemView(cores[0].Core) + sys := vault.TestDynamicSystemView(cores[0].Core, nil) vault.TestAddTestPlugin(t, cores[0].Core, "test-plugin", consts.PluginTypeDatabase, "TestPlugin_GRPC_Main", []string{}, "") return cluster, sys diff --git a/builtin/logical/database/path_roles.go b/builtin/logical/database/path_roles.go index 3aefe822670c..32f30082f9c4 100644 --- a/builtin/logical/database/path_roles.go +++ b/builtin/logical/database/path_roles.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/hashicorp/go-multierror" v4 "github.com/hashicorp/vault/sdk/database/dbplugin" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/helper/locksutil" @@ -215,7 +216,28 @@ func (b *databaseBackend) pathStaticRoleDelete(ctx context.Context, req *logical return nil, err } - return nil, nil + walIDs, err := framework.ListWAL(ctx, req.Storage) + if err != nil { + return nil, err + } + var merr *multierror.Error + for _, walID := range walIDs { + wal, err := b.findStaticWAL(ctx, req.Storage, walID) + if err != nil { + merr = multierror.Append(merr, err) + continue + } + if wal != nil && name == wal.RoleName { + b.Logger().Debug("deleting WAL for deleted role", "WAL ID", walID, "role", name) + err = framework.DeleteWAL(ctx, req.Storage, walID) + if err != nil { + b.Logger().Debug("failed to delete WAL for deleted role", "WAL ID", walID, "error", err) + merr = multierror.Append(merr, err) + } + } + } + + return nil, merr.ErrorOrNil() } func (b *databaseBackend) pathStaticRoleRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { @@ -482,19 +504,34 @@ func (b *databaseBackend) pathStaticRoleCreateUpdate(ctx context.Context, req *l // Only call setStaticAccount if we're creating the role for the // first time + var item *queue.Item switch req.Operation { case logical.CreateOperation: // setStaticAccount calls Storage.Put and saves the role to storage resp, err := b.setStaticAccount(ctx, req.Storage, &setStaticAccountInput{ - RoleName: name, - Role: role, - CreateUser: createRole, + RoleName: name, + Role: role, }) if err != nil { + if resp != nil && resp.WALID != "" { + b.Logger().Debug("deleting WAL for failed role creation", "WAL ID", resp.WALID, "role", name) + walDeleteErr := framework.DeleteWAL(ctx, req.Storage, resp.WALID) + if walDeleteErr != nil { + b.Logger().Debug("failed to delete WAL for failed role creation", "WAL ID", resp.WALID, "error", walDeleteErr) + var merr *multierror.Error + merr = multierror.Append(merr, err) + merr = multierror.Append(merr, fmt.Errorf("failed to clean up WAL from failed role creation: %w", walDeleteErr)) + err = merr.ErrorOrNil() + } + } + return nil, err } // guard against RotationTime not being set or zero-value lvr = resp.RotationTime + item = &queue.Item{ + Key: name, + } case logical.UpdateOperation: // store updated Role entry, err := logical.StorageEntryJSON(databaseStaticRolePath+name, role) @@ -504,17 +541,16 @@ func (b *databaseBackend) pathStaticRoleCreateUpdate(ctx context.Context, req *l if err := req.Storage.Put(ctx, entry); err != nil { return nil, err } - - // In case this is an update, remove any previous version of the item from - // the queue - b.popFromRotationQueueByKey(name) + item, err = b.popFromRotationQueueByKey(name) + if err != nil { + return nil, err + } } + item.Priority = lvr.Add(role.StaticAccount.RotationPeriod).Unix() + // Add their rotation to the queue - if err := b.pushItem(&queue.Item{ - Key: name, - Priority: lvr.Add(role.StaticAccount.RotationPeriod).Unix(), - }); err != nil { + if err := b.pushItem(item); err != nil { return nil, err } diff --git a/builtin/logical/database/path_roles_test.go b/builtin/logical/database/path_roles_test.go index f4b6d56b6be7..a2c87ec7f480 100644 --- a/builtin/logical/database/path_roles_test.go +++ b/builtin/logical/database/path_roles_test.go @@ -3,13 +3,16 @@ package database import ( "context" "errors" + "strings" "testing" "time" "github.com/go-test/deep" "github.com/hashicorp/vault/helper/namespace" postgreshelper "github.com/hashicorp/vault/helper/testhelpers/postgresql" + v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5" "github.com/hashicorp/vault/sdk/logical" + "github.com/stretchr/testify/mock" ) var dataKeys = []string{"username", "password", "last_vault_rotation", "rotation_period"} @@ -43,7 +46,7 @@ func TestBackend_StaticRole_Config(t *testing.T) { "connection_url": connURL, "plugin_name": "postgresql-database-plugin", "verify_connection": false, - "allowed_roles": []string{"*"}, + "allowed_roles": []string{"plugin-role-test"}, "name": "plugin-test", } req := &logical.Request{ @@ -61,6 +64,7 @@ func TestBackend_StaticRole_Config(t *testing.T) { // ordering, so each case cleans up by deleting the role testCases := map[string]struct { account map[string]interface{} + path string expected map[string]interface{} err error }{ @@ -69,6 +73,7 @@ func TestBackend_StaticRole_Config(t *testing.T) { "username": dbUser, "rotation_period": "5400s", }, + path: "plugin-role-test", expected: map[string]interface{}{ "username": dbUser, "rotation_period": float64(5400), @@ -78,7 +83,16 @@ func TestBackend_StaticRole_Config(t *testing.T) { account: map[string]interface{}{ "username": dbUser, }, - err: errors.New("rotation_period is required to create static accounts"), + path: "plugin-role-test", + err: errors.New("rotation_period is required to create static accounts"), + }, + "disallowed role config": { + account: map[string]interface{}{ + "username": dbUser, + "rotation_period": "5400s", + }, + path: "disallowed-role", + err: errors.New("\"disallowed-role\" is not an allowed role"), }, } @@ -94,9 +108,11 @@ func TestBackend_StaticRole_Config(t *testing.T) { data[k] = v } + path := "static-roles/" + tc.path + req := &logical.Request{ Operation: logical.CreateOperation, - Path: "static-roles/plugin-role-test", + Path: path, Storage: config.StorageView, Data: data, } @@ -510,6 +526,135 @@ func TestBackend_StaticRole_Role_name_check(t *testing.T) { } } +func TestWALsStillTrackedAfterUpdate(t *testing.T) { + ctx := context.Background() + b, storage, mockDB := getBackend(t) + defer b.Cleanup(ctx) + configureDBMount(t, storage) + + createRole(t, b, storage, mockDB, "hashicorp") + + generateWALFromFailedRotation(t, b, storage, mockDB, "hashicorp") + requireWALs(t, storage, 1) + + resp, err := b.HandleRequest(ctx, &logical.Request{ + Operation: logical.UpdateOperation, + Path: "static-roles/hashicorp", + Storage: storage, + Data: map[string]interface{}{ + "username": "hashicorp", + "dn": "uid=hashicorp,ou=users,dc=hashicorp,dc=com", + "rotation_period": "600s", + }, + }) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatal(resp, err) + } + walIDs := requireWALs(t, storage, 1) + + // Now when we trigger a manual rotate, it should use the WAL's new password + // which will tell us that the in-memory structure still kept track of the + // WAL in addition to it still being in storage. + wal, err := b.findStaticWAL(ctx, storage, walIDs[0]) + if err != nil { + t.Fatal(err) + } + rotateRole(t, b, storage, mockDB, "hashicorp") + role, err := b.StaticRole(ctx, storage, "hashicorp") + if err != nil { + t.Fatal(err) + } + if role.StaticAccount.Password != wal.NewPassword { + t.Fatal() + } + requireWALs(t, storage, 0) +} + +func TestWALsDeletedOnRoleCreationFailed(t *testing.T) { + ctx := context.Background() + b, storage, mockDB := getBackend(t) + defer b.Cleanup(ctx) + configureDBMount(t, storage) + + for i := 0; i < 3; i++ { + mockDB.On("UpdateUser", mock.Anything, mock.Anything). + Return(v5.UpdateUserResponse{}, errors.New("forced error")). + Once() + resp, err := b.HandleRequest(ctx, &logical.Request{ + Operation: logical.CreateOperation, + Path: "static-roles/hashicorp", + Storage: storage, + Data: map[string]interface{}{ + "username": "hashicorp", + "db_name": "mockv5", + "rotation_period": "5s", + }, + }) + if err == nil { + t.Fatal("expected error from DB") + } + if !strings.Contains(err.Error(), "forced error") { + t.Fatal("expected forced error message", resp, err) + } + } + + requireWALs(t, storage, 0) +} + +func TestWALsDeletedOnRoleDeletion(t *testing.T) { + ctx := context.Background() + b, storage, mockDB := getBackend(t) + defer b.Cleanup(ctx) + configureDBMount(t, storage) + + // Create the roles + roleNames := []string{"hashicorp", "2"} + for _, roleName := range roleNames { + createRole(t, b, storage, mockDB, roleName) + } + + // Fail to rotate the roles + for _, roleName := range roleNames { + generateWALFromFailedRotation(t, b, storage, mockDB, roleName) + } + + // Should have 2 WALs hanging around + requireWALs(t, storage, 2) + + // Delete one of the static roles + resp, err := b.HandleRequest(ctx, &logical.Request{ + Operation: logical.DeleteOperation, + Path: "static-roles/hashicorp", + Storage: storage, + }) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatal(resp, err) + } + + // 1 WAL should be cleared by the delete + requireWALs(t, storage, 1) +} + +func createRole(t *testing.T, b *databaseBackend, storage logical.Storage, mockDB *mockNewDatabase, roleName string) { + t.Helper() + mockDB.On("UpdateUser", mock.Anything, mock.Anything). + Return(v5.UpdateUserResponse{}, nil). + Once() + resp, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.CreateOperation, + Path: "static-roles/" + roleName, + Storage: storage, + Data: map[string]interface{}{ + "username": roleName, + "db_name": "mockv5", + "rotation_period": "86400s", + }, + }) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatal(resp, err) + } +} + const testRoleStaticCreate = ` CREATE ROLE "{{name}}" WITH LOGIN diff --git a/builtin/logical/database/path_rotate_credentials.go b/builtin/logical/database/path_rotate_credentials.go index 5774ea863460..040784a731fa 100644 --- a/builtin/logical/database/path_rotate_credentials.go +++ b/builtin/logical/database/path_rotate_credentials.go @@ -169,10 +169,14 @@ func (b *databaseBackend) pathRotateRoleCredentialsUpdate() framework.OperationF } } - resp, err := b.setStaticAccount(ctx, req.Storage, &setStaticAccountInput{ + input := &setStaticAccountInput{ RoleName: name, Role: role, - }) + } + if walID, ok := item.Value.(string); ok { + input.WALID = walID + } + resp, err := b.setStaticAccount(ctx, req.Storage, input) // if err is not nil, we need to attempt to update the priority and place // this item back on the queue. The err should still be returned at the end // of this method. @@ -188,6 +192,8 @@ func (b *databaseBackend) pathRotateRoleCredentialsUpdate() framework.OperationF } } 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 @@ -195,8 +201,13 @@ func (b *databaseBackend) pathRotateRoleCredentialsUpdate() framework.OperationF return nil, err } + 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) + } + // return any err from the setStaticAccount call - return nil, err + return nil, nil } } diff --git a/builtin/logical/database/rotation.go b/builtin/logical/database/rotation.go index 79ba9dee2524..72eab0d0edfc 100644 --- a/builtin/logical/database/rotation.go +++ b/builtin/logical/database/rotation.go @@ -7,8 +7,7 @@ import ( "strconv" "time" - "github.com/hashicorp/go-multierror" - v4 "github.com/hashicorp/vault/sdk/database/dbplugin" + "github.com/hashicorp/errwrap" v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/helper/consts" @@ -66,21 +65,34 @@ func (b *databaseBackend) populateQueue(ctx context.Context, s logical.Storage) item := queue.Item{ Key: roleName, - Priority: role.StaticAccount.LastVaultRotation.Add(role.StaticAccount.RotationPeriod).Unix(), + Priority: role.StaticAccount.NextRotationTime().Unix(), } // Check if role name is in map walEntry := walMap[roleName] if walEntry != nil { // Check walEntry last vault time - if !walEntry.LastVaultRotation.IsZero() && walEntry.LastVaultRotation.Before(role.StaticAccount.LastVaultRotation) { + if walEntry.LastVaultRotation.IsZero() { + // A WAL's last Vault rotation can only ever be 0 for a role that + // was never successfully created. So we know this WAL couldn't + // have been created for this role we just retrieved from storage. + // i.e. it must be a hangover from a previous attempt at creating + // a role with the same name + log.Debug("deleting WAL with zero last rotation time", "WAL ID", walEntry.walID, "created", walEntry.walCreatedAt) + if err := framework.DeleteWAL(ctx, s, walEntry.walID); err != nil { + log.Warn("unable to delete zero-time WAL", "error", err, "WAL ID", walEntry.walID) + } + } else if walEntry.LastVaultRotation.Before(role.StaticAccount.LastVaultRotation) { // WAL's last vault rotation record is older than the role's data, so // delete and move on + log.Debug("deleting outdated WAL", "WAL ID", walEntry.walID, "created", walEntry.walCreatedAt) if err := framework.DeleteWAL(ctx, s, walEntry.walID); err != nil { log.Warn("unable to delete WAL", "error", err, "WAL ID", walEntry.walID) } } else { - log.Info("adjusting priority for Role") + log.Info("found WAL for role", + "role", item.Key, + "WAL ID", walEntry.walID) item.Value = walEntry.walID item.Priority = time.Now().Unix() } @@ -114,13 +126,14 @@ func (b *databaseBackend) runTicker(ctx context.Context, queueTickInterval time. // credential setting or rotation in the event of partial failure. type setCredentialsWAL struct { NewPassword string `json:"new_password"` - OldPassword string `json:"old_password"` RoleName string `json:"role_name"` Username string `json:"username"` LastVaultRotation time.Time `json:"last_vault_rotation"` - walID string + // Private fields which will not be included in json.Marshal/Unmarshal. + walID string + walCreatedAt int64 // Unix time at which the WAL was created. } // rotateCredentials sets a new password for a static account. This method is @@ -194,18 +207,7 @@ func (b *databaseBackend) rotateCredential(ctx context.Context, s logical.Storag // If there is a WAL entry related to this Role, the corresponding WAL ID // should be stored in the Item's Value field. if walID, ok := item.Value.(string); ok { - walEntry, err := b.findStaticWAL(ctx, s, walID) - if err != nil { - b.logger.Error("error finding static WAL", "error", err) - item.Priority = time.Now().Add(10 * time.Second).Unix() - if err := b.pushItem(item); err != nil { - b.logger.Error("unable to push item on to queue", "error", err) - } - } - if walEntry != nil && walEntry.NewPassword != "" { - input.Password = walEntry.NewPassword - input.WALID = walID - } + input.WALID = walID } resp, err := b.setStaticAccount(ctx, s, input) @@ -226,6 +228,8 @@ func (b *databaseBackend) rotateCredential(ctx context.Context, s logical.Storag // Go to next item return true } + // Clear any stored WAL ID as we must have successfully deleted our WAL to get here. + item.Value = "" lvr := resp.RotationTime if lvr.IsZero() { @@ -255,11 +259,11 @@ func (b *databaseBackend) findStaticWAL(ctx context.Context, s logical.Storage, data := wal.Data.(map[string]interface{}) walEntry := setCredentialsWAL{ - walID: id, - NewPassword: data["new_password"].(string), - OldPassword: data["old_password"].(string), - RoleName: data["role_name"].(string), - Username: data["username"].(string), + walID: id, + walCreatedAt: wal.CreatedAt, + NewPassword: data["new_password"].(string), + RoleName: data["role_name"].(string), + Username: data["username"].(string), } lvr, err := time.Parse(time.RFC3339, data["last_vault_rotation"].(string)) if err != nil { @@ -271,16 +275,13 @@ func (b *databaseBackend) findStaticWAL(ctx context.Context, s logical.Storage, } type setStaticAccountInput struct { - RoleName string - Role *roleEntry - Password string - CreateUser bool - WALID string + RoleName string + Role *roleEntry + WALID string } type setStaticAccountOutput struct { RotationTime time.Time - Password string // Optional return field, in the event WAL was created and not destroyed // during the operation WALID string @@ -300,7 +301,6 @@ type setStaticAccountOutput struct { // This method does not perform any operations on the priority queue. Those // tasks must be handled outside of this method. func (b *databaseBackend) setStaticAccount(ctx context.Context, s logical.Storage, input *setStaticAccountInput) (*setStaticAccountOutput, error) { - var merr error if input == nil || input.Role == nil || input.RoleName == "" { return nil, errors.New("input was empty when attempting to set credentials for static account") } @@ -311,6 +311,9 @@ func (b *databaseBackend) setStaticAccount(ctx context.Context, s logical.Storag if err != nil { return output, err } + if dbConfig == nil { + return output, errors.New("the config is currently unset") + } // If role name isn't in the database's allowed roles, send back a // permission denied. @@ -330,31 +333,47 @@ func (b *databaseBackend) setStaticAccount(ctx context.Context, s logical.Storag // Use password 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 - newPassword := input.Password - if newPassword == "" { - newPassword, err = dbi.database.GeneratePassword(ctx, b.System(), dbConfig.PasswordPolicy) + var newPassword string + if output.WALID != "" { + wal, err := b.findStaticWAL(ctx, s, output.WALID) if err != nil { - return output, err + return output, errwrap.Wrapf("error retrieving WAL entry: {{err}}", err) } - } - output.Password = newPassword - config := v4.StaticUserConfig{ - Username: input.Role.StaticAccount.Username, - Password: newPassword, + switch { + case wal != nil && wal.NewPassword != "": + newPassword = wal.NewPassword + default: + if wal == nil { + b.Logger().Error("expected role to have WAL, but WAL not found in storage", "role", input.RoleName, "WAL ID", output.WALID) + } else { + b.Logger().Error("expected WAL to have a new password set, but empty", "role", input.RoleName, "WAL ID", output.WALID) + err = framework.DeleteWAL(ctx, s, output.WALID) + if err != nil { + b.Logger().Warn("failed to delete WAL with no new password", "error", err, "WAL ID", output.WALID) + } + } + // If there's anything wrong with the WAL in storage, we'll need + // to generate a fresh WAL and password + output.WALID = "" + } } if output.WALID == "" { + newPassword, err = dbi.database.GeneratePassword(ctx, b.System(), dbConfig.PasswordPolicy) + if err != nil { + return output, err + } output.WALID, err = framework.PutWAL(ctx, s, staticWALKey, &setCredentialsWAL{ RoleName: input.RoleName, - Username: config.Username, - NewPassword: config.Password, - OldPassword: input.Role.StaticAccount.Password, + Username: input.Role.StaticAccount.Username, + NewPassword: newPassword, LastVaultRotation: input.Role.StaticAccount.LastVaultRotation, }) if err != nil { return output, fmt.Errorf("error writing WAL entry: %w", err) } + b.Logger().Debug("writing WAL", "role", input.RoleName, "WAL ID", output.WALID) } updateReq := v5.UpdateUserRequest{ @@ -389,12 +408,13 @@ func (b *databaseBackend) setStaticAccount(ctx context.Context, s logical.Storag // Cleanup WAL after successfully rotating and pushing new item on to queue if err := framework.DeleteWAL(ctx, s, output.WALID); err != nil { - merr = multierror.Append(merr, err) - return output, merr + b.Logger().Warn("error deleting WAL", "WAL ID", output.WALID, "error", err) + return output, err } + b.Logger().Debug("deleted WAL", "WAL ID", output.WALID) // The WAL has been deleted, return new setStaticAccountOutput without it - return &setStaticAccountOutput{RotationTime: lvr}, merr + return &setStaticAccountOutput{RotationTime: lvr}, nil } // initQueue preforms the necessary checks and initializations needed to perform @@ -493,13 +513,39 @@ func (b *databaseBackend) loadStaticWALs(ctx context.Context, s logical.Storage) continue } if role == nil || role.StaticAccount == nil { + b.Logger().Debug("deleting WAL with nil role or static account", "WAL ID", walEntry.walID) if err := framework.DeleteWAL(ctx, s, walEntry.walID); err != nil { b.Logger().Warn("unable to delete WAL", "error", err, "WAL ID", walEntry.walID) } continue } - walEntry.walID = walID + if existingWALEntry, exists := walMap[walEntry.RoleName]; exists { + b.Logger().Debug("multiple WALs detected for role", "role", walEntry.RoleName, + "loaded WAL ID", existingWALEntry.walID, "created at", existingWALEntry.walCreatedAt, "last vault rotation", existingWALEntry.LastVaultRotation, + "candidate WAL ID", walEntry.walID, "created at", walEntry.walCreatedAt, "last vault rotation", walEntry.LastVaultRotation) + + if walEntry.walCreatedAt > existingWALEntry.walCreatedAt { + // If the existing WAL is older, delete it from storage and fall + // through to inserting our current WAL into the map. + b.Logger().Debug("deleting stale loaded WAL", "WAL ID", existingWALEntry.walID) + err = framework.DeleteWAL(ctx, s, existingWALEntry.walID) + if err != nil { + b.Logger().Warn("unable to delete loaded WAL", "error", err, "WAL ID", existingWALEntry.walID) + } + } else { + // If we already have a more recent WAL entry in the map, delete + // this one and continue onto the next WAL. + b.Logger().Debug("deleting stale candidate WAL", "WAL ID", walEntry.walID) + err = framework.DeleteWAL(ctx, s, walID) + if err != nil { + b.Logger().Warn("unable to delete candidate WAL", "error", err, "WAL ID", walEntry.walID) + } + continue + } + } + + b.Logger().Debug("loaded WAL", "WAL ID", walID) walMap[walEntry.RoleName] = walEntry } return walMap, nil diff --git a/builtin/logical/database/rotation_test.go b/builtin/logical/database/rotation_test.go index 28314c8fe2b0..37751587b56b 100644 --- a/builtin/logical/database/rotation_test.go +++ b/builtin/logical/database/rotation_test.go @@ -3,6 +3,8 @@ package database import ( "context" "database/sql" + "errors" + "fmt" "log" "os" "strings" @@ -13,11 +15,15 @@ import ( "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/helper/testhelpers/mongodb" postgreshelper "github.com/hashicorp/vault/helper/testhelpers/postgresql" + v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5" "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/helper/dbtxn" "github.com/hashicorp/vault/sdk/logical" + "github.com/hashicorp/vault/sdk/queue" "github.com/lib/pq" mongodbatlasapi "github.com/mongodb/go-client-mongodb-atlas/mongodbatlas" + "github.com/stretchr/testify/mock" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) @@ -695,7 +701,7 @@ func assertWALCount(t *testing.T, s logical.Storage, expected int, key string) { type userCreator func(t *testing.T, username, password string) func TestBackend_StaticRole_Rotations_PostgreSQL(t *testing.T) { - cleanup, connURL := postgreshelper.PrepareTestContainer(t, "latest") + cleanup, connURL := postgreshelper.PrepareTestContainer(t, "13.4-buster") defer cleanup() uc := userCreator(func(t *testing.T, username, password string) { createTestPGUser(t, connURL, username, password, testRoleStaticCreate) @@ -974,15 +980,25 @@ func TestBackend_StaticRole_LockRegression(t *testing.T) { } createTestPGUser(t, connURL, dbUser, dbUserDefaultPassword, testRoleStaticCreate) - for i := 0; i < 25; i++ { - data := map[string]interface{}{ - "name": "plugin-role-test", - "db_name": "plugin-test", - "rotation_statements": testRoleStaticUpdate, - "username": dbUser, - "rotation_period": "7s", - } + data = map[string]interface{}{ + "name": "plugin-role-test", + "db_name": "plugin-test", + "rotation_statements": testRoleStaticUpdate, + "username": dbUser, + "rotation_period": "7s", + } + req = &logical.Request{ + Operation: logical.CreateOperation, + Path: "static-roles/plugin-role-test", + Storage: config.StorageView, + 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) + } + for i := 0; i < 25; i++ { req = &logical.Request{ Operation: logical.UpdateOperation, Path: "static-roles/plugin-role-test", @@ -1094,6 +1110,300 @@ func TestBackend_StaticRole_Rotate_Invalid_Role(t *testing.T) { } } +func TestRollsPasswordForwardsUsingWAL(t *testing.T) { + ctx := context.Background() + b, storage, mockDB := getBackend(t) + defer b.Cleanup(ctx) + configureDBMount(t, storage) + createRole(t, b, storage, mockDB, "hashicorp") + + role, err := b.StaticRole(ctx, storage, "hashicorp") + if err != nil { + t.Fatal(err) + } + oldPassword := role.StaticAccount.Password + + generateWALFromFailedRotation(t, b, storage, mockDB, "hashicorp") + + walIDs := requireWALs(t, storage, 1) + wal, err := b.findStaticWAL(ctx, storage, walIDs[0]) + if err != nil { + t.Fatal(err) + } + role, err = b.StaticRole(ctx, storage, "hashicorp") + if err != nil { + t.Fatal(err) + } + // Role's password should still be the WAL's old password + if role.StaticAccount.Password != oldPassword { + t.Fatal(role.StaticAccount.Password, oldPassword) + } + + rotateRole(t, b, storage, mockDB, "hashicorp") + + role, err = b.StaticRole(ctx, storage, "hashicorp") + if err != nil { + t.Fatal(err) + } + if role.StaticAccount.Password != wal.NewPassword { + t.Fatal("role password", role.StaticAccount.Password, "WAL new password", wal.NewPassword) + } + // WAL should be cleared by the successful rotate + requireWALs(t, storage, 0) +} + +func TestStoredWALsCorrectlyProcessed(t *testing.T) { + const walNewPassword = "new-password-from-wal" + for _, tc := range []struct { + name string + shouldRotate bool + wal *setCredentialsWAL + }{ + { + "WAL is kept and used for roll forward", + true, + &setCredentialsWAL{ + RoleName: "hashicorp", + Username: "hashicorp", + NewPassword: walNewPassword, + LastVaultRotation: time.Now().Add(time.Hour), + }, + }, + { + "zero-time WAL is discarded on load", + false, + &setCredentialsWAL{ + RoleName: "hashicorp", + Username: "hashicorp", + NewPassword: walNewPassword, + LastVaultRotation: time.Time{}, + }, + }, + { + "empty-password WAL is kept but a new password is generated", + true, + &setCredentialsWAL{ + RoleName: "hashicorp", + Username: "hashicorp", + NewPassword: "", + LastVaultRotation: time.Now().Add(time.Hour), + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + b := Backend(config) + defer b.Cleanup(ctx) + mockDB := setupMockDB(b) + if err := b.Setup(ctx, config); err != nil { + t.Fatal(err) + } + b.credRotationQueue = queue.New() + configureDBMount(t, config.StorageView) + createRole(t, b, config.StorageView, mockDB, "hashicorp") + role, err := b.StaticRole(ctx, config.StorageView, "hashicorp") + if err != nil { + t.Fatal(err) + } + initialPassword := role.StaticAccount.Password + + // Set up a WAL for our test case + framework.PutWAL(ctx, config.StorageView, staticWALKey, tc.wal) + requireWALs(t, config.StorageView, 1) + // Reset the rotation queue to simulate startup memory state + b.credRotationQueue = queue.New() + + // Now finish the startup process by populating the queue, which should discard the WAL + b.initQueue(ctx, config, consts.ReplicationUnknown) + + if tc.shouldRotate { + requireWALs(t, storage, 1) + } else { + requireWALs(t, storage, 0) + } + + // Run one tick + mockDB.On("UpdateUser", mock.Anything, mock.Anything). + Return(v5.UpdateUserResponse{}, nil). + Once() + b.rotateCredentials(ctx, storage) + requireWALs(t, storage, 0) + + role, err = b.StaticRole(ctx, storage, "hashicorp") + if err != nil { + t.Fatal(err) + } + item, err := b.popFromRotationQueueByKey("hashicorp") + if err != nil { + t.Fatal(err) + } + + if tc.shouldRotate { + if tc.wal.NewPassword != "" { + // Should use WAL's new_password field + if role.StaticAccount.Password != walNewPassword { + t.Fatal() + } + } else { + // Should rotate but ignore WAL's new_password field + if role.StaticAccount.Password == initialPassword { + t.Fatal() + } + if role.StaticAccount.Password == walNewPassword { + t.Fatal() + } + } + } else { + // Ensure the role was not promoted for early rotation + if item.Priority < time.Now().Add(time.Hour).Unix() { + t.Fatal("priority should be for about a week away, but was", item.Priority) + } + if role.StaticAccount.Password != initialPassword { + t.Fatal("password should not have been rotated yet") + } + } + }) + } +} + +func TestDeletesOlderWALsOnLoad(t *testing.T) { + ctx := context.Background() + b, storage, mockDB := getBackend(t) + defer b.Cleanup(ctx) + configureDBMount(t, storage) + createRole(t, b, storage, mockDB, "hashicorp") + + // Create 4 WALs, with a clear winner for most recent. + wal := &setCredentialsWAL{ + RoleName: "hashicorp", + Username: "hashicorp", + NewPassword: "some-new-password", + LastVaultRotation: time.Now(), + } + for i := 0; i < 3; i++ { + _, err := framework.PutWAL(ctx, storage, staticWALKey, wal) + if err != nil { + t.Fatal(err) + } + } + time.Sleep(2 * time.Second) + // We expect this WAL to have the latest createdAt timestamp + walID, err := framework.PutWAL(ctx, storage, staticWALKey, wal) + if err != nil { + t.Fatal(err) + } + requireWALs(t, storage, 4) + + walMap, err := b.loadStaticWALs(ctx, storage) + if err != nil { + t.Fatal(err) + } + if len(walMap) != 1 || walMap["hashicorp"] == nil || walMap["hashicorp"].walID != walID { + t.Fatal() + } + requireWALs(t, storage, 1) +} + +func generateWALFromFailedRotation(t *testing.T, b *databaseBackend, storage logical.Storage, mockDB *mockNewDatabase, roleName string) { + t.Helper() + mockDB.On("UpdateUser", mock.Anything, mock.Anything). + Return(v5.UpdateUserResponse{}, errors.New("forced error")). + Once() + _, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.UpdateOperation, + Path: "rotate-role/" + roleName, + Storage: storage, + }) + if err == nil { + t.Fatal("expected error") + } +} + +func rotateRole(t *testing.T, b *databaseBackend, storage logical.Storage, mockDB *mockNewDatabase, roleName string) { + t.Helper() + mockDB.On("UpdateUser", mock.Anything, mock.Anything). + Return(v5.UpdateUserResponse{}, nil). + Once() + resp, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.UpdateOperation, + Path: "rotate-role/" + roleName, + Storage: storage, + }) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatal(resp, err) + } +} + +// returns a slice of the WAL IDs in storage +func requireWALs(t *testing.T, storage logical.Storage, expectedCount int) []string { + t.Helper() + wals, err := storage.List(context.Background(), "wal/") + if err != nil { + t.Fatal(err) + } + if len(wals) != expectedCount { + t.Fatal("expected WALs", expectedCount, "got", len(wals)) + } + + return wals +} + +func getBackend(t *testing.T) (*databaseBackend, logical.Storage, *mockNewDatabase) { + t.Helper() + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + // Create and init the backend ourselves instead of using a Factory because + // the factory function kicks off threads that cause racy tests. + b := Backend(config) + if err := b.Setup(context.Background(), config); err != nil { + t.Fatal(err) + } + b.credRotationQueue = queue.New() + b.populateQueue(context.Background(), config.StorageView) + + mockDB := setupMockDB(b) + + return b, config.StorageView, mockDB +} + +func setupMockDB(b *databaseBackend) *mockNewDatabase { + mockDB := &mockNewDatabase{} + mockDB.On("Initialize", mock.Anything, mock.Anything).Return(v5.InitializeResponse{}, nil) + mockDB.On("Close").Return(nil) + dbw := databaseVersionWrapper{ + v5: mockDB, + } + + dbi := &dbPluginInstance{ + database: dbw, + id: "foo-id", + name: "mockV5", + } + b.connections["mockv5"] = dbi + + return mockDB +} + +// configureDBMount puts config directly into storage to avoid the DB engine's +// plugin init code paths, allowing us to use a manually populated mock DB object. +func configureDBMount(t *testing.T, storage logical.Storage) { + t.Helper() + entry, err := logical.StorageEntryJSON(fmt.Sprintf("config/mockv5"), &DatabaseConfig{ + AllowedRoles: []string{"*"}, + }) + if err != nil { + t.Fatal(err) + } + + err = storage.Put(context.Background(), entry) + if err != nil { + t.Fatal(err) + } +} + // capturePasswords captures the current passwords at the time of calling, and // returns a map of username / passwords building off of the input map func capturePasswords(t *testing.T, b logical.Backend, config *logical.BackendConfig, testCases []string, pws map[string][]string) map[string][]string { diff --git a/builtin/logical/database/versioning_large_test.go b/builtin/logical/database/versioning_large_test.go index 7b3cc82eee93..db2941308507 100644 --- a/builtin/logical/database/versioning_large_test.go +++ b/builtin/logical/database/versioning_large_test.go @@ -81,8 +81,12 @@ func TestPlugin_lifecycle(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - cleanupReqs := []*logical.Request{} - defer cleanup(t, b, cleanupReqs) + var cleanupReqs []*logical.Request + defer func() { + // Do not defer cleanup directly so that we can populate the + // slice before the function gets executed. + cleanup(t, b, cleanupReqs) + }() // ///////////////////////////////////////////////////////////////// // Configure @@ -173,7 +177,7 @@ func TestPlugin_lifecycle(t *testing.T) { // Create static role staticRoleName := "static-role" req = &logical.Request{ - Operation: logical.UpdateOperation, + Operation: logical.CreateOperation, Path: fmt.Sprintf("static-roles/%s", staticRoleName), Storage: config.StorageView, Data: map[string]interface{}{ diff --git a/builtin/logical/pki/backend_test.go b/builtin/logical/pki/backend_test.go index a3c85764e87c..235f39a22f0f 100644 --- a/builtin/logical/pki/backend_test.go +++ b/builtin/logical/pki/backend_test.go @@ -2102,20 +2102,151 @@ func TestBackend_SignSelfIssued(t *testing.T) { t.Fatal(err) } - getSelfSigned := func(subject, issuer *x509.Certificate) (string, *x509.Certificate) { - selfSigned, err := x509.CreateCertificate(rand.Reader, subject, issuer, key.Public(), key) - if err != nil { - t.Fatal(err) - } - cert, err := x509.ParseCertificate(selfSigned) - if err != nil { - t.Fatal(err) - } - pemSS := strings.TrimSpace(string(pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: selfSigned, - }))) - return pemSS, cert + template := &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "foo.bar.com", + }, + SerialNumber: big.NewInt(1234), + IsCA: false, + BasicConstraintsValid: true, + } + + ss, _ := getSelfSigned(t, template, template, key) + resp, err = b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.UpdateOperation, + Path: "root/sign-self-issued", + Storage: storage, + Data: map[string]interface{}{ + "certificate": ss, + }, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("got nil response") + } + if !resp.IsError() { + t.Fatalf("expected error due to non-CA; got: %#v", *resp) + } + + // Set CA to true, but leave issuer alone + template.IsCA = true + + issuer := &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "bar.foo.com", + }, + SerialNumber: big.NewInt(2345), + IsCA: true, + BasicConstraintsValid: true, + } + ss, ssCert := getSelfSigned(t, template, issuer, key) + resp, err = b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.UpdateOperation, + Path: "root/sign-self-issued", + Storage: storage, + Data: map[string]interface{}{ + "certificate": ss, + }, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("got nil response") + } + if !resp.IsError() { + t.Fatalf("expected error due to different issuer; cert info is\nIssuer\n%#v\nSubject\n%#v\n", ssCert.Issuer, ssCert.Subject) + } + + ss, ssCert = getSelfSigned(t, template, template, key) + resp, err = b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.UpdateOperation, + Path: "root/sign-self-issued", + Storage: storage, + Data: map[string]interface{}{ + "certificate": ss, + }, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("got nil response") + } + if resp.IsError() { + t.Fatalf("error in response: %s", resp.Error().Error()) + } + + newCertString := resp.Data["certificate"].(string) + block, _ := pem.Decode([]byte(newCertString)) + newCert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + t.Fatal(err) + } + + signingBundle, err := fetchCAInfo(context.Background(), &logical.Request{Storage: storage}) + if err != nil { + t.Fatal(err) + } + if reflect.DeepEqual(newCert.Subject, newCert.Issuer) { + t.Fatal("expected different subject/issuer") + } + if !reflect.DeepEqual(newCert.Issuer, signingBundle.Certificate.Subject) { + t.Fatalf("expected matching issuer/CA subject\n\nIssuer:\n%#v\nSubject:\n%#v\n", newCert.Issuer, signingBundle.Certificate.Subject) + } + if bytes.Equal(newCert.AuthorityKeyId, newCert.SubjectKeyId) { + t.Fatal("expected different authority/subject") + } + if !bytes.Equal(newCert.AuthorityKeyId, signingBundle.Certificate.SubjectKeyId) { + t.Fatal("expected authority on new cert to be same as signing subject") + } + if newCert.Subject.CommonName != "foo.bar.com" { + t.Fatalf("unexpected common name on new cert: %s", newCert.Subject.CommonName) + } +} + +// TestBackend_SignSelfIssued_DifferentTypes is a copy of +// TestBackend_SignSelfIssued, but uses a different key type for the internal +// root (EC instead of RSA). This verifies that we can cross-sign CAs that are +// different key types, at the cost of verifying the algorithm used +func TestBackend_SignSelfIssued_DifferentTypes(t *testing.T) { + // create the backend + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + + b := Backend(config) + err := b.Setup(context.Background(), config) + if err != nil { + t.Fatal(err) + } + + // generate root + rootData := map[string]interface{}{ + "common_name": "test.com", + "ttl": "172800", + "key_type": "ec", + "key_bits": "521", + } + + resp, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.UpdateOperation, + Path: "root/generate/internal", + Storage: storage, + Data: rootData, + }) + if resp != nil && resp.IsError() { + t.Fatalf("failed to generate root, %#v", *resp) + } + if err != nil { + t.Fatal(err) + } + + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) } template := &x509.Certificate{ @@ -2127,7 +2258,7 @@ func TestBackend_SignSelfIssued(t *testing.T) { BasicConstraintsValid: true, } - ss, _ := getSelfSigned(template, template) + ss, _ := getSelfSigned(t, template, template, key) resp, err = b.HandleRequest(context.Background(), &logical.Request{ Operation: logical.UpdateOperation, Path: "root/sign-self-issued", @@ -2157,7 +2288,8 @@ func TestBackend_SignSelfIssued(t *testing.T) { IsCA: true, BasicConstraintsValid: true, } - ss, ssCert := getSelfSigned(template, issuer) + + ss, ssCert := getSelfSigned(t, template, issuer, key) resp, err = b.HandleRequest(context.Background(), &logical.Request{ Operation: logical.UpdateOperation, Path: "root/sign-self-issued", @@ -2176,7 +2308,7 @@ func TestBackend_SignSelfIssued(t *testing.T) { t.Fatalf("expected error due to different issuer; cert info is\nIssuer\n%#v\nSubject\n%#v\n", ssCert.Issuer, ssCert.Subject) } - ss, ssCert = getSelfSigned(template, template) + ss, ssCert = getSelfSigned(t, template, template, key) resp, err = b.HandleRequest(context.Background(), &logical.Request{ Operation: logical.UpdateOperation, Path: "root/sign-self-issued", @@ -2185,6 +2317,20 @@ func TestBackend_SignSelfIssued(t *testing.T) { "certificate": ss, }, }) + if err == nil { + t.Fatal("expected error due to different signature algo but not opted-in") + } + + ss, ssCert = getSelfSigned(t, template, template, key) + resp, err = b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.UpdateOperation, + Path: "root/sign-self-issued", + Storage: storage, + Data: map[string]interface{}{ + "certificate": ss, + "allow_different_signature_algorithm": "true", + }, + }) if err != nil { t.Fatal(err) } @@ -2223,6 +2369,23 @@ func TestBackend_SignSelfIssued(t *testing.T) { } } +func getSelfSigned(t *testing.T, subject, issuer *x509.Certificate, key *rsa.PrivateKey) (string, *x509.Certificate) { + t.Helper() + selfSigned, err := x509.CreateCertificate(rand.Reader, subject, issuer, key.Public(), key) + if err != nil { + t.Fatal(err) + } + cert, err := x509.ParseCertificate(selfSigned) + if err != nil { + t.Fatal(err) + } + pemSS := strings.TrimSpace(string(pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: selfSigned, + }))) + return pemSS, cert +} + // This is a really tricky test because the Go stdlib asn1 package is incapable // of doing the right thing with custom OID SANs (see comments in the package, // it's readily admitted that it's too magic) but that means that any diff --git a/builtin/logical/pki/path_root.go b/builtin/logical/pki/path_root.go index 29bee4baae10..1964013d4c96 100644 --- a/builtin/logical/pki/path_root.go +++ b/builtin/logical/pki/path_root.go @@ -2,11 +2,17 @@ package pki import ( "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" + "crypto/rsa" "crypto/x509" "encoding/base64" "encoding/pem" + "errors" "fmt" + "golang.org/x/crypto/ed25519" "reflect" "strings" "time" @@ -103,6 +109,10 @@ func pathSignSelfIssued(b *backend) *framework.Path { Type: framework.TypeString, Description: `PEM-format self-issued certificate to be signed.`, }, + "allow_different_signature_algorithm": &framework.FieldSchema{ + Type: framework.TypeBool, + Description: `If true, allow the public key type of the signer to differ from the self issued certificate.`, + }, }, HelpSynopsis: pathSignSelfIssuedHelpSyn, @@ -428,6 +438,25 @@ func (b *backend) pathCASignSelfIssued(ctx context.Context, req *logical.Request cert.CRLDistributionPoints = urls.CRLDistributionPoints cert.OCSPServer = urls.OCSPServers + // If the requested signature algorithm isn't the same as the signing certificate, and + // the user has requested a cross-algorithm signature, reset the template's signing algorithm + // to that of the signing key + signingPubType, signingAlgorithm, err := publicKeyType(signingBundle.Certificate.PublicKey) + if err != nil { + return nil, fmt.Errorf("error determining signing certificate algorithm type: %e", err) + } + certPubType, _, err := publicKeyType(cert.PublicKey) + if err != nil { + return nil, fmt.Errorf("error determining template algorithm type: %e", err) + } + + if signingPubType != certPubType { + b, ok := data.GetOk("allow_different_signature_algorithm") + if ok && b.(bool) { + cert.SignatureAlgorithm = signingAlgorithm + } + } + newCert, err := x509.CreateCertificate(rand.Reader, cert, signingBundle.Certificate, cert.PublicKey, signingBundle.PrivateKey) if err != nil { return nil, fmt.Errorf("error signing self-issued certificate: %w", err) @@ -448,6 +477,34 @@ func (b *backend) pathCASignSelfIssued(ctx context.Context, req *logical.Request }, nil } +// Adapted from similar code in https://github.com/golang/go/blob/4a4221e8187189adcc6463d2d96fe2e8da290132/src/crypto/x509/x509.go#L1342, +// may need to be updated in the future. +func publicKeyType(pub crypto.PublicKey) (pubType x509.PublicKeyAlgorithm, sigAlgo x509.SignatureAlgorithm, err error) { + switch pub := pub.(type) { + case *rsa.PublicKey: + pubType = x509.RSA + sigAlgo = x509.SHA256WithRSA + case *ecdsa.PublicKey: + pubType = x509.ECDSA + switch pub.Curve { + case elliptic.P224(), elliptic.P256(): + sigAlgo = x509.ECDSAWithSHA256 + case elliptic.P384(): + sigAlgo = x509.ECDSAWithSHA384 + case elliptic.P521(): + sigAlgo = x509.ECDSAWithSHA512 + default: + err = errors.New("x509: unknown elliptic curve") + } + case ed25519.PublicKey: + pubType = x509.Ed25519 + sigAlgo = x509.PureEd25519 + default: + err = errors.New("x509: only RSA, ECDSA and Ed25519 keys supported") + } + return +} + const pathGenerateRootHelpSyn = ` Generate a new CA certificate and private key used for signing. ` diff --git a/builtin/logical/transit/backend.go b/builtin/logical/transit/backend.go index f438ac1b0290..d91df7df1863 100644 --- a/builtin/logical/transit/backend.go +++ b/builtin/logical/transit/backend.go @@ -3,13 +3,18 @@ package transit import ( "context" "fmt" + "io" "strings" + "sync" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/helper/keysutil" "github.com/hashicorp/vault/sdk/logical" ) +// Minimum cache size for transit backend +const minCacheSize = 10 + func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { b, err := Backend(ctx, conf) if err != nil { @@ -68,6 +73,11 @@ func Backend(ctx context.Context, conf *logical.BackendConfig) (*backend, error) if err != nil { return nil, fmt.Errorf("Error retrieving cache size from storage: %w", err) } + + if cacheSize != 0 && cacheSize < minCacheSize { + b.Logger().Warn("size %d is less than minimum %d. Cache size is set to %d", cacheSize, minCacheSize, minCacheSize) + cacheSize = minCacheSize + } } var err error @@ -82,6 +92,9 @@ func Backend(ctx context.Context, conf *logical.BackendConfig) (*backend, error) type backend struct { *framework.Backend lm *keysutil.LockManager + // Lock to make changes to any of the backend's cache configuration. + configMutex sync.RWMutex + cacheSizeChanged bool } func GetCacheSizeFromStorage(ctx context.Context, s logical.Storage) (int, error) { @@ -100,7 +113,41 @@ func GetCacheSizeFromStorage(ctx context.Context, s logical.Storage) (int, error return size, nil } -func (b *backend) invalidate(_ context.Context, key string) { +// Update cache size and get policy +func (b *backend) GetPolicy(ctx context.Context, polReq keysutil.PolicyRequest, rand io.Reader) (retP *keysutil.Policy, retUpserted bool, retErr error) { + // Acquire read lock to read cacheSizeChanged + b.configMutex.RLock() + if b.lm.GetUseCache() && b.cacheSizeChanged { + var err error + currentCacheSize := b.lm.GetCacheSize() + storedCacheSize, err := GetCacheSizeFromStorage(ctx, polReq.Storage) + if err != nil { + b.configMutex.RUnlock() + return nil, false, err + } + if currentCacheSize != storedCacheSize { + err = b.lm.InitCache(storedCacheSize) + if err != nil { + b.configMutex.RUnlock() + return nil, false, err + } + } + // Release the read lock and acquire the write lock + b.configMutex.RUnlock() + b.configMutex.Lock() + defer b.configMutex.Unlock() + b.cacheSizeChanged = false + } else { + b.configMutex.RUnlock() + } + p, _, err := b.lm.GetPolicy(ctx, polReq, rand) + if err != nil { + return p, false, err + } + return p, true, nil +} + +func (b *backend) invalidate(ctx context.Context, key string) { if b.Logger().IsDebug() { b.Logger().Debug("invalidating key", "key", key) } @@ -108,5 +155,10 @@ func (b *backend) invalidate(_ context.Context, key string) { case strings.HasPrefix(key, "policy/"): name := strings.TrimPrefix(key, "policy/") b.lm.InvalidatePolicy(name) + case strings.HasPrefix(key, "cache-config/"): + // Acquire the lock to set the flag to indicate that cache size needs to be refreshed from storage + b.configMutex.Lock() + defer b.configMutex.Unlock() + b.cacheSizeChanged = true } } diff --git a/builtin/logical/transit/path_cache_config.go b/builtin/logical/transit/path_cache_config.go index 6239555b3766..6a1f791e0c03 100644 --- a/builtin/logical/transit/path_cache_config.go +++ b/builtin/logical/transit/path_cache_config.go @@ -3,7 +3,6 @@ package transit import ( "context" "errors" - "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/logical" ) @@ -45,8 +44,8 @@ func (b *backend) pathCacheConfig() *framework.Path { func (b *backend) pathCacheConfigWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { // get target size cacheSize := d.Get("size").(int) - if cacheSize < 0 { - return logical.ErrorResponse("size must be greater or equal to 0"), logical.ErrInvalidRequest + if cacheSize != 0 && cacheSize < minCacheSize { + return logical.ErrorResponse("size must be 0 or a value greater or equal to %d", minCacheSize), logical.ErrInvalidRequest } // store cache size @@ -60,11 +59,12 @@ func (b *backend) pathCacheConfigWrite(ctx context.Context, req *logical.Request return nil, err } - resp := &logical.Response{ - Warnings: []string{"cache configurations will be applied when this backend is restarted"}, + err = b.lm.InitCache(cacheSize) + if err != nil { + return nil, err } - return resp, nil + return nil, nil } type configCache struct { @@ -86,16 +86,19 @@ func (b *backend) pathCacheConfigRead(ctx context.Context, req *logical.Request, return nil, err } + if currentCacheSize != storedCacheSize { + err = b.lm.InitCache(storedCacheSize) + if err != nil { + return nil, err + } + } + resp := &logical.Response{ Data: map[string]interface{}{ "size": storedCacheSize, }, } - if currentCacheSize != storedCacheSize { - resp.Warnings = []string{"This cache size will not be applied until the transit mount is reloaded"} - } - return resp, nil } diff --git a/builtin/logical/transit/path_cache_config_test.go b/builtin/logical/transit/path_cache_config_test.go index 6cca1b265676..2d74129f95bf 100644 --- a/builtin/logical/transit/path_cache_config_test.go +++ b/builtin/logical/transit/path_cache_config_test.go @@ -8,6 +8,7 @@ import ( ) const targetCacheSize = 12345 +const smallCacheSize = 3 func TestTransit_CacheConfig(t *testing.T) { b1, storage := createBackendWithSysView(t) @@ -58,17 +59,51 @@ func TestTransit_CacheConfig(t *testing.T) { }, } + writeSmallCacheSizeReq := &logical.Request{ + Storage: storage, + Operation: logical.UpdateOperation, + Path: "cache-config", + Data: map[string]interface{}{ + "size": smallCacheSize, + }, + } + readReq := &logical.Request{ Storage: storage, Operation: logical.ReadOperation, Path: "cache-config", } + polReq := &logical.Request{ + Storage: storage, + Operation: logical.UpdateOperation, + Path: "keys/aes256", + Data: map[string]interface{}{ + "derived": true, + }, + } + + // test steps // b1 should spin up with an unlimited cache validateResponse(doReq(b1, readReq), 0, false) + + // Change cache size to targetCacheSize 12345 and validate that cache size is updated doReq(b1, writeReq) - validateResponse(doReq(b1, readReq), targetCacheSize, true) + validateResponse(doReq(b1, readReq), targetCacheSize, false) + b1.invalidate(context.Background(), "cache-config/") + + // Change the cache size to 1000 to mock the scenario where + // current cache size and stored cache size are different and + // a cache update is needed + b1.lm.InitCache(1000) + + // Write a new policy which in its code path detects that cache size has changed + // and refreshes the cache to 12345 + doReq(b1, polReq) + + // Validate that cache size is updated to 12345 + validateResponse(doReq(b1, readReq), targetCacheSize, false) // b2 should spin up with a configured cache b2 := createBackendWithSysViewWithStorage(t, storage) @@ -77,4 +112,10 @@ func TestTransit_CacheConfig(t *testing.T) { // b3 enables transit without a cache, trying to read it should error b3 := createBackendWithForceNoCacheWithSysViewWithStorage(t, storage) doErrReq(b3, readReq) + + // b4 should spin up with a size less than minimum cache size (10) + b4, storage := createBackendWithSysView(t) + doErrReq(b4, writeSmallCacheSizeReq) + + } diff --git a/builtin/logical/transit/path_config.go b/builtin/logical/transit/path_config.go index 4641e2b6a7cd..1c41cd0d49dd 100644 --- a/builtin/logical/transit/path_config.go +++ b/builtin/logical/transit/path_config.go @@ -62,7 +62,7 @@ func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, d * name := d.Get("name").(string) // Check if the policy already exists before we lock everything - p, _, err := b.lm.GetPolicy(ctx, keysutil.PolicyRequest{ + p, _, err := b.GetPolicy(ctx, keysutil.PolicyRequest{ Storage: req.Storage, Name: name, }, b.GetRandomReader()) diff --git a/builtin/logical/transit/path_datakey.go b/builtin/logical/transit/path_datakey.go index a287bea34415..9e9ef2c17340 100644 --- a/builtin/logical/transit/path_datakey.go +++ b/builtin/logical/transit/path_datakey.go @@ -99,7 +99,7 @@ func (b *backend) pathDatakeyWrite(ctx context.Context, req *logical.Request, d } // Get the policy - p, _, err := b.lm.GetPolicy(ctx, keysutil.PolicyRequest{ + p, _, err := b.GetPolicy(ctx, keysutil.PolicyRequest{ Storage: req.Storage, Name: name, }, b.GetRandomReader()) diff --git a/builtin/logical/transit/path_decrypt.go b/builtin/logical/transit/path_decrypt.go index 5d8510da89f4..cf6d45060406 100644 --- a/builtin/logical/transit/path_decrypt.go +++ b/builtin/logical/transit/path_decrypt.go @@ -121,7 +121,7 @@ func (b *backend) pathDecryptWrite(ctx context.Context, req *logical.Request, d } // Get the policy - p, _, err := b.lm.GetPolicy(ctx, keysutil.PolicyRequest{ + p, _, err := b.GetPolicy(ctx, keysutil.PolicyRequest{ Storage: req.Storage, Name: d.Get("name").(string), }, b.GetRandomReader()) diff --git a/builtin/logical/transit/path_encrypt.go b/builtin/logical/transit/path_encrypt.go index 321e92099894..c328951645a1 100644 --- a/builtin/logical/transit/path_encrypt.go +++ b/builtin/logical/transit/path_encrypt.go @@ -217,7 +217,7 @@ func decodeBatchRequestItems(src interface{}, dst *[]BatchRequestItem) error { func (b *backend) pathEncryptExistenceCheck(ctx context.Context, req *logical.Request, d *framework.FieldData) (bool, error) { name := d.Get("name").(string) - p, _, err := b.lm.GetPolicy(ctx, keysutil.PolicyRequest{ + p, _, err := b.GetPolicy(ctx, keysutil.PolicyRequest{ Storage: req.Storage, Name: name, }, b.GetRandomReader()) @@ -336,7 +336,7 @@ func (b *backend) pathEncryptWrite(ctx context.Context, req *logical.Request, d } } - p, upserted, err = b.lm.GetPolicy(ctx, polReq, b.GetRandomReader()) + p, upserted, err = b.GetPolicy(ctx, polReq, b.GetRandomReader()) if err != nil { return nil, err } diff --git a/builtin/logical/transit/path_export.go b/builtin/logical/transit/path_export.go index 33a76cf33b73..3b0d97e15e73 100644 --- a/builtin/logical/transit/path_export.go +++ b/builtin/logical/transit/path_export.go @@ -64,7 +64,7 @@ func (b *backend) pathPolicyExportRead(ctx context.Context, req *logical.Request return logical.ErrorResponse(fmt.Sprintf("invalid export type: %s", exportType)), logical.ErrInvalidRequest } - p, _, err := b.lm.GetPolicy(ctx, keysutil.PolicyRequest{ + p, _, err := b.GetPolicy(ctx, keysutil.PolicyRequest{ Storage: req.Storage, Name: name, }, b.GetRandomReader()) diff --git a/builtin/logical/transit/path_hmac.go b/builtin/logical/transit/path_hmac.go index 025a39efdf01..30a79a40789a 100644 --- a/builtin/logical/transit/path_hmac.go +++ b/builtin/logical/transit/path_hmac.go @@ -96,7 +96,7 @@ func (b *backend) pathHMACWrite(ctx context.Context, req *logical.Request, d *fr } // Get the policy - p, _, err := b.lm.GetPolicy(ctx, keysutil.PolicyRequest{ + p, _, err := b.GetPolicy(ctx, keysutil.PolicyRequest{ Storage: req.Storage, Name: name, }, b.GetRandomReader()) @@ -224,7 +224,7 @@ func (b *backend) pathHMACVerify(ctx context.Context, req *logical.Request, d *f } // Get the policy - p, _, err := b.lm.GetPolicy(ctx, keysutil.PolicyRequest{ + p, _, err := b.GetPolicy(ctx, keysutil.PolicyRequest{ Storage: req.Storage, Name: name, }, b.GetRandomReader()) diff --git a/builtin/logical/transit/path_hmac_test.go b/builtin/logical/transit/path_hmac_test.go index b9d6bbc81332..756dc77e559f 100644 --- a/builtin/logical/transit/path_hmac_test.go +++ b/builtin/logical/transit/path_hmac_test.go @@ -26,7 +26,7 @@ func TestTransit_HMAC(t *testing.T) { } // Now, change the key value to something we control - p, _, err := b.lm.GetPolicy(context.Background(), keysutil.PolicyRequest{ + p, _, err := b.GetPolicy(context.Background(), keysutil.PolicyRequest{ Storage: storage, Name: "foo", }, b.GetRandomReader()) @@ -196,7 +196,7 @@ func TestTransit_batchHMAC(t *testing.T) { } // Now, change the key value to something we control - p, _, err := b.lm.GetPolicy(context.Background(), keysutil.PolicyRequest{ + p, _, err := b.GetPolicy(context.Background(), keysutil.PolicyRequest{ Storage: storage, Name: "foo", }, b.GetRandomReader()) diff --git a/builtin/logical/transit/path_keys.go b/builtin/logical/transit/path_keys.go index 4cc25f66c400..8c43ab593b5b 100644 --- a/builtin/logical/transit/path_keys.go +++ b/builtin/logical/transit/path_keys.go @@ -162,7 +162,7 @@ func (b *backend) pathPolicyWrite(ctx context.Context, req *logical.Request, d * return logical.ErrorResponse(fmt.Sprintf("unknown key type %v", keyType)), logical.ErrInvalidRequest } - p, upserted, err := b.lm.GetPolicy(ctx, polReq, b.GetRandomReader()) + p, upserted, err := b.GetPolicy(ctx, polReq, b.GetRandomReader()) if err != nil { return nil, err } @@ -191,7 +191,7 @@ type asymKey struct { func (b *backend) pathPolicyRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { name := d.Get("name").(string) - p, _, err := b.lm.GetPolicy(ctx, keysutil.PolicyRequest{ + p, _, err := b.GetPolicy(ctx, keysutil.PolicyRequest{ Storage: req.Storage, Name: name, }, b.GetRandomReader()) diff --git a/builtin/logical/transit/path_rewrap.go b/builtin/logical/transit/path_rewrap.go index 9d473d256948..c32fddc99976 100644 --- a/builtin/logical/transit/path_rewrap.go +++ b/builtin/logical/transit/path_rewrap.go @@ -114,7 +114,7 @@ func (b *backend) pathRewrapWrite(ctx context.Context, req *logical.Request, d * } // Get the policy - p, _, err := b.lm.GetPolicy(ctx, keysutil.PolicyRequest{ + p, _, err := b.GetPolicy(ctx, keysutil.PolicyRequest{ Storage: req.Storage, Name: d.Get("name").(string), }, b.GetRandomReader()) diff --git a/builtin/logical/transit/path_rotate.go b/builtin/logical/transit/path_rotate.go index 3d2c2cdf4045..a74e69980512 100644 --- a/builtin/logical/transit/path_rotate.go +++ b/builtin/logical/transit/path_rotate.go @@ -31,7 +31,7 @@ func (b *backend) pathRotateWrite(ctx context.Context, req *logical.Request, d * name := d.Get("name").(string) // Get the policy - p, _, err := b.lm.GetPolicy(ctx, keysutil.PolicyRequest{ + p, _, err := b.GetPolicy(ctx, keysutil.PolicyRequest{ Storage: req.Storage, Name: name, }, b.GetRandomReader()) diff --git a/builtin/logical/transit/path_sign_verify.go b/builtin/logical/transit/path_sign_verify.go index 659e6a2091c6..265d63cec198 100644 --- a/builtin/logical/transit/path_sign_verify.go +++ b/builtin/logical/transit/path_sign_verify.go @@ -246,7 +246,7 @@ func (b *backend) pathSignWrite(ctx context.Context, req *logical.Request, d *fr sigAlgorithm := d.Get("signature_algorithm").(string) // Get the policy - p, _, err := b.lm.GetPolicy(ctx, keysutil.PolicyRequest{ + p, _, err := b.GetPolicy(ctx, keysutil.PolicyRequest{ Storage: req.Storage, Name: name, }, b.GetRandomReader()) @@ -464,7 +464,7 @@ func (b *backend) pathVerifyWrite(ctx context.Context, req *logical.Request, d * sigAlgorithm := d.Get("signature_algorithm").(string) // Get the policy - p, _, err := b.lm.GetPolicy(ctx, keysutil.PolicyRequest{ + p, _, err := b.GetPolicy(ctx, keysutil.PolicyRequest{ Storage: req.Storage, Name: name, }, b.GetRandomReader()) diff --git a/builtin/logical/transit/path_sign_verify_test.go b/builtin/logical/transit/path_sign_verify_test.go index 652dc186c916..40df90838b19 100644 --- a/builtin/logical/transit/path_sign_verify_test.go +++ b/builtin/logical/transit/path_sign_verify_test.go @@ -55,7 +55,7 @@ func testTransit_SignVerify_ECDSA(t *testing.T, bits int) { } // Now, change the key value to something we control - p, _, err := b.lm.GetPolicy(context.Background(), keysutil.PolicyRequest{ + p, _, err := b.GetPolicy(context.Background(), keysutil.PolicyRequest{ Storage: storage, Name: "foo", }, b.GetRandomReader()) @@ -377,7 +377,7 @@ func TestTransit_SignVerify_ED25519(t *testing.T) { } // Get the keys for later - fooP, _, err := b.lm.GetPolicy(context.Background(), keysutil.PolicyRequest{ + fooP, _, err := b.GetPolicy(context.Background(), keysutil.PolicyRequest{ Storage: storage, Name: "foo", }, b.GetRandomReader()) @@ -385,7 +385,7 @@ func TestTransit_SignVerify_ED25519(t *testing.T) { t.Fatal(err) } - barP, _, err := b.lm.GetPolicy(context.Background(), keysutil.PolicyRequest{ + barP, _, err := b.GetPolicy(context.Background(), keysutil.PolicyRequest{ Storage: storage, Name: "bar", }, b.GetRandomReader()) diff --git a/builtin/logical/transit/path_trim.go b/builtin/logical/transit/path_trim.go index cec7a5648ef7..d8587f1c18d4 100644 --- a/builtin/logical/transit/path_trim.go +++ b/builtin/logical/transit/path_trim.go @@ -40,7 +40,7 @@ func (b *backend) pathTrimUpdate() framework.OperationFunc { return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (resp *logical.Response, retErr error) { name := d.Get("name").(string) - p, _, err := b.lm.GetPolicy(ctx, keysutil.PolicyRequest{ + p, _, err := b.GetPolicy(ctx, keysutil.PolicyRequest{ Storage: req.Storage, Name: name, }, b.GetRandomReader()) diff --git a/builtin/logical/transit/path_trim_test.go b/builtin/logical/transit/path_trim_test.go index 6b3dfaa9ec2f..be989b164245 100644 --- a/builtin/logical/transit/path_trim_test.go +++ b/builtin/logical/transit/path_trim_test.go @@ -36,7 +36,7 @@ func TestTransit_Trim(t *testing.T) { doReq(t, req) // Get the policy and check that the archive has correct number of keys - p, _, err := b.lm.GetPolicy(namespace.RootContext(nil), keysutil.PolicyRequest{ + p, _, err := b.GetPolicy(namespace.RootContext(nil), keysutil.PolicyRequest{ Storage: storage, Name: "aes", }, b.GetRandomReader()) diff --git a/builtin/plugin/backend_test.go b/builtin/plugin/backend_test.go index 600df860472c..87bbdb2c4c6d 100644 --- a/builtin/plugin/backend_test.go +++ b/builtin/plugin/backend_test.go @@ -80,7 +80,7 @@ func testConfig(t *testing.T) (*logical.BackendConfig, func()) { core := cores[0] - sys := vault.TestDynamicSystemView(core.Core) + sys := vault.TestDynamicSystemView(core.Core, nil) config := &logical.BackendConfig{ Logger: logging.NewVaultLogger(log.Debug), diff --git a/changelog/11984.txt b/changelog/11984.txt new file mode 100644 index 000000000000..11b23e8d2b2d --- /dev/null +++ b/changelog/11984.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: Show day of month instead of day of year in the expiration warning dialog +``` diff --git a/changelog/12339.txt b/changelog/12339.txt new file mode 100644 index 000000000000..2e812ab68b27 --- /dev/null +++ b/changelog/12339.txt @@ -0,0 +1,3 @@ +```release-note:bug + core (enterprise): Only delete quotas on primary cluster. + ``` \ No newline at end of file diff --git a/changelog/12388.txt b/changelog/12388.txt new file mode 100644 index 000000000000..f384c90bdb94 --- /dev/null +++ b/changelog/12388.txt @@ -0,0 +1,3 @@ +```release-note:bug +storage/raft: Detect incomplete raft snapshots in api.RaftSnapshot(), and thereby in `vault operator raft snapshot save`. +``` diff --git a/changelog/12418.txt b/changelog/12418.txt new file mode 100644 index 000000000000..e6c07e9b9515 --- /dev/null +++ b/changelog/12418.txt @@ -0,0 +1,3 @@ +```release-note:bug +secrets/transit: Enforce minimum cache size for transit backend and init cache size on transit backend without restart. +``` \ No newline at end of file diff --git a/changelog/12473.txt b/changelog/12473.txt new file mode 100644 index 000000000000..3a0ecddb5e71 --- /dev/null +++ b/changelog/12473.txt @@ -0,0 +1,3 @@ +```release-note:bug +identity: Fail alias rename if the resulting (name,accessor) exists already +``` \ No newline at end of file diff --git a/changelog/12478.txt b/changelog/12478.txt new file mode 100644 index 000000000000..3a0e302c864b --- /dev/null +++ b/changelog/12478.txt @@ -0,0 +1,3 @@ +```release-note:fix +ui: fix missing navbar items on login to namespace +``` \ No newline at end of file diff --git a/changelog/12514.txt b/changelog/12514.txt new file mode 100644 index 000000000000..8f00061e2ef3 --- /dev/null +++ b/changelog/12514.txt @@ -0,0 +1,3 @@ +```release-note:improvement +secrets/pki: Allow signing of self-issued certs with a different signature algorithm. +``` diff --git a/changelog/12534.txt b/changelog/12534.txt new file mode 100644 index 000000000000..d7c05f641502 --- /dev/null +++ b/changelog/12534.txt @@ -0,0 +1,3 @@ +```release-note:bug +agent: Avoid possible `unexpected fault address` panic when using persistent cache. +``` diff --git a/changelog/12550.txt b/changelog/12550.txt new file mode 100644 index 000000000000..171a34b6185d --- /dev/null +++ b/changelog/12550.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: Fix bug where capabilities check on secret-delete-menu was encoding the forward slashes. +``` diff --git a/changelog/12563.txt b/changelog/12563.txt new file mode 100644 index 000000000000..9298e82460be --- /dev/null +++ b/changelog/12563.txt @@ -0,0 +1,3 @@ +```release-note:bug +secrets/db: Fix bug where Vault can rotate static role passwords early during start up under certain conditions. +``` diff --git a/changelog/12599.txt b/changelog/12599.txt new file mode 100644 index 000000000000..45d34e9a67e3 --- /dev/null +++ b/changelog/12599.txt @@ -0,0 +1,3 @@ +```release-note:bug +secrets/openldap: Fix bug where Vault can rotate static role passwords early during start up under certain conditions. [#28](https://github.com/hashicorp/vault-plugin-secrets-openldap/pull/28) +``` \ No newline at end of file diff --git a/changelog/12635.txt b/changelog/12635.txt new file mode 100644 index 000000000000..9e1a7d7fe15e --- /dev/null +++ b/changelog/12635.txt @@ -0,0 +1,3 @@ +```release-note:bug +core (enterprise): Fix bug where password generation through password policies do not work on namespaces if performed outside a request callback or from an external plugin. +``` \ No newline at end of file diff --git a/command/agent/cache/cacheboltdb/bolt.go b/command/agent/cache/cacheboltdb/bolt.go index 69a438c1808e..0a39c9cc15c6 100644 --- a/command/agent/cache/cacheboltdb/bolt.go +++ b/command/agent/cache/cacheboltdb/bolt.go @@ -219,7 +219,11 @@ func (b *BoltStorage) GetAutoAuthToken(ctx context.Context) ([]byte, error) { if meta == nil { return fmt.Errorf("bucket %q not found", metaBucketName) } - encryptedToken = meta.Get([]byte(AutoAuthToken)) + value := meta.Get([]byte(AutoAuthToken)) + if value != nil { + encryptedToken = make([]byte, len(value)) + copy(encryptedToken, value) + } return nil }) if err != nil { @@ -247,7 +251,11 @@ func (b *BoltStorage) GetRetrievalToken() ([]byte, error) { if keyBucket == nil { return fmt.Errorf("bucket %q not found", metaBucketName) } - token = keyBucket.Get([]byte(RetrievalTokenMaterial)) + value := keyBucket.Get([]byte(RetrievalTokenMaterial)) + if value != nil { + token = make([]byte, len(value)) + copy(token, value) + } return nil }) if err != nil { diff --git a/go.mod b/go.mod index 548c0590f446..8697829fee30 100644 --- a/go.mod +++ b/go.mod @@ -103,10 +103,10 @@ require ( github.com/hashicorp/vault-plugin-secrets-gcpkms v0.9.0 github.com/hashicorp/vault-plugin-secrets-kv v0.9.0 github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.4.0 - github.com/hashicorp/vault-plugin-secrets-openldap v0.5.1 + github.com/hashicorp/vault-plugin-secrets-openldap v0.5.2 github.com/hashicorp/vault-plugin-secrets-terraform v0.2.0 github.com/hashicorp/vault/api v1.1.2-0.20210713235431-1fc8af4c041f - github.com/hashicorp/vault/sdk v0.2.2-0.20210825150427-9b1f4d486f5d + github.com/hashicorp/vault/sdk v0.2.2-0.20210927220632-193e27e9cc1e github.com/influxdata/influxdb v0.0.0-20190411212539-d24b7ba8c4c4 github.com/jcmturner/gokrb5/v8 v8.0.0 github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f diff --git a/go.sum b/go.sum index c5b3b410d671..e2d26e5e08c1 100644 --- a/go.sum +++ b/go.sum @@ -742,8 +742,8 @@ github.com/hashicorp/vault-plugin-secrets-kv v0.9.0 h1:nCw2IfWw2bWUGFZsNk8BvTEg9 github.com/hashicorp/vault-plugin-secrets-kv v0.9.0/go.mod h1:B/Cybh5aVF7LNAMHwVBxY8t7r2eL0C6HVGgTyP4nKK4= github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.4.0 h1:6ve+7hZmGn7OpML81iZUxYj2AaJptwys323S5XsvVas= github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.4.0/go.mod h1:4mdgPqlkO+vfFX1cFAWcxkeqz6JAtZgKxL/67q/58Oo= -github.com/hashicorp/vault-plugin-secrets-openldap v0.5.1 h1:iUJU3D/sA5qNBZnhXI5jFdwoWXMhgb6jeABDLYw631Y= -github.com/hashicorp/vault-plugin-secrets-openldap v0.5.1/go.mod h1:GiFI8Bxwx3+fn0A3SyVp9XdYQhm3cOgN8GzwKxyJ9So= +github.com/hashicorp/vault-plugin-secrets-openldap v0.5.2 h1:5f3Gh/gcsC2sbn5PhE3YbeXB0cfC3TlZpkrBUSf90p4= +github.com/hashicorp/vault-plugin-secrets-openldap v0.5.2/go.mod h1:GiFI8Bxwx3+fn0A3SyVp9XdYQhm3cOgN8GzwKxyJ9So= github.com/hashicorp/vault-plugin-secrets-terraform v0.2.0 h1:U5hT6xUUbIhI12v+tjzmUz47gpzg5yxbdf+q62sIIvc= github.com/hashicorp/vault-plugin-secrets-terraform v0.2.0/go.mod h1:7r/0t51X/ZtSRh/TjBk7gCm1CUMk50aqLAx811OsGQ8= github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443 h1:O/pT5C1Q3mVXMyuqg7yuAWUg/jMZR1/0QTzTRdNR6Uw= diff --git a/plugins/database/postgresql/postgresql_test.go b/plugins/database/postgresql/postgresql_test.go index ca800a0dddb4..8c79debf3b69 100644 --- a/plugins/database/postgresql/postgresql_test.go +++ b/plugins/database/postgresql/postgresql_test.go @@ -16,7 +16,7 @@ import ( ) func getPostgreSQL(t *testing.T, options map[string]interface{}) (*PostgreSQL, func()) { - cleanup, connURL := postgresql.PrepareTestContainer(t, "latest") + cleanup, connURL := postgresql.PrepareTestContainer(t, "13.4-buster") connectionDetails := map[string]interface{}{ "connection_url": connURL, @@ -867,7 +867,7 @@ func TestUsernameGeneration(t *testing.T) { } func TestNewUser_CustomUsername(t *testing.T) { - cleanup, connURL := postgresql.PrepareTestContainer(t, "latest") + cleanup, connURL := postgresql.PrepareTestContainer(t, "13.4-buster") defer cleanup() type testCase struct { diff --git a/sdk/helper/keysutil/lock_manager.go b/sdk/helper/keysutil/lock_manager.go index 039b05ad0535..c6a0a23d6145 100644 --- a/sdk/helper/keysutil/lock_manager.go +++ b/sdk/helper/keysutil/lock_manager.go @@ -101,6 +101,24 @@ func (lm *LockManager) InvalidatePolicy(name string) { } } +func (lm *LockManager) InitCache(cacheSize int) error { + if lm.useCache { + switch { + case cacheSize < 0: + return errors.New("cache size must be greater or equal to zero") + case cacheSize == 0: + lm.cache = NewTransitSyncMap() + case cacheSize > 0: + newLRUCache, err := NewTransitLRU(cacheSize) + if err != nil { + return errwrap.Wrapf("failed to create cache: {{err}}", err) + } + lm.cache = newLRUCache + } + } + return nil +} + // RestorePolicy acquires an exclusive lock on the policy name and restores the // given policy along with the archive. func (lm *LockManager) RestorePolicy(ctx context.Context, storage logical.Storage, name, backup string, force bool) error { diff --git a/sdk/version/version_base.go b/sdk/version/version_base.go index 5f4a85815419..77268352dadd 100644 --- a/sdk/version/version_base.go +++ b/sdk/version/version_base.go @@ -8,7 +8,7 @@ var ( // Whether cgo is enabled or not; set at build time CgoEnabled bool - Version = "1.8.2" + Version = "1.8.3" VersionPrerelease = "" VersionMetadata = "" ) diff --git a/ui/app/components/secret-delete-menu.js b/ui/app/components/secret-delete-menu.js index e5feb32e7634..bc1e56944d5f 100644 --- a/ui/app/components/secret-delete-menu.js +++ b/ui/app/components/secret-delete-menu.js @@ -47,7 +47,7 @@ export default class SecretDeleteMenu extends Component { if (!context.args || !context.args.modelForData || !context.args.modelForData.id) return; let [backend, id] = JSON.parse(context.args.modelForData.id); return { - id: `${encodeURIComponent(backend)}/delete/${encodeURIComponent(id)}`, + id: `${backend}/delete/${id}`, }; }, 'model.id' @@ -61,7 +61,7 @@ export default class SecretDeleteMenu extends Component { if (!context.args || !context.args.modelForData || !context.args.modelForData.id) return; let [backend, id] = JSON.parse(context.args.modelForData.id); return { - id: `${encodeURIComponent(backend)}/undelete/${encodeURIComponent(id)}`, + id: `${backend}/undelete/${id}`, }; }, 'model.id' @@ -75,7 +75,7 @@ export default class SecretDeleteMenu extends Component { if (!context.args || !context.args.modelForData || !context.args.modelForData.id) return; let [backend, id] = JSON.parse(context.args.modelForData.id); return { - id: `${encodeURIComponent(backend)}/destroy/${encodeURIComponent(id)}`, + id: `${backend}/destroy/${id}`, }; }, 'model.id' @@ -90,7 +90,7 @@ export default class SecretDeleteMenu extends Component { let backend = context.args.model.engine.id; let id = context.args.model.id; return { - id: `${encodeURIComponent(backend)}/metadata/${encodeURIComponent(id)}`, + id: `${backend}/metadata/${id}`, }; }, 'model', diff --git a/ui/app/routes/vault/cluster.js b/ui/app/routes/vault/cluster.js index c9403be2a08f..e523bd395e5b 100644 --- a/ui/app/routes/vault/cluster.js +++ b/ui/app/routes/vault/cluster.js @@ -110,5 +110,17 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, { } return true; }, + loading(transition) { + if (transition.queryParamsOnly || Ember.testing) { + return; + } + // eslint-disable-next-line ember/no-controller-access-in-routes + let controller = this.controllerFor('vault.cluster'); + controller.set('currentlyLoading', true); + + transition.finally(function() { + controller.set('currentlyLoading', false); + }); + }, }, }); diff --git a/ui/app/templates/components/auth-info.hbs b/ui/app/templates/components/auth-info.hbs index 5d2b37741479..a158e7b9d2f2 100644 --- a/ui/app/templates/components/auth-info.hbs +++ b/ui/app/templates/components/auth-info.hbs @@ -10,8 +10,8 @@
  • -
  • + On {{date-format this.auth.tokenExpirationDate 'MMMM do yyyy, h:mm:ss a'}}" /> + {{/if}}