diff --git a/builtin/logical/transit/backend.go b/builtin/logical/transit/backend.go index 1bb7f4c92714..76e2bea1c46b 100644 --- a/builtin/logical/transit/backend.go +++ b/builtin/logical/transit/backend.go @@ -46,6 +46,7 @@ func Backend(conf *logical.BackendConfig) *backend { b.pathVerify(), b.pathBackup(), b.pathRestore(), + b.pathTrim(), }, Secrets: []*framework.Secret{}, diff --git a/builtin/logical/transit/path_config.go b/builtin/logical/transit/path_config.go index e97a0a698a88..ae5624b63f96 100644 --- a/builtin/logical/transit/path_config.go +++ b/builtin/logical/transit/path_config.go @@ -58,7 +58,7 @@ the latest version of the key is allowed.`, } } -func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { +func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (resp *logical.Response, retErr error) { name := d.Get("name").(string) // Check if the policy already exists before we lock everything @@ -79,7 +79,23 @@ func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, d * } defer p.Unlock() - resp := &logical.Response{} + originalMinDecryptionVersion := p.MinDecryptionVersion + originalMinEncryptionVersion := p.MinEncryptionVersion + originalDeletionAllowed := p.DeletionAllowed + originalExportable := p.Exportable + originalAllowPlaintextBackup := p.AllowPlaintextBackup + + defer func() { + if retErr != nil || (resp != nil && resp.IsError()) { + p.MinDecryptionVersion = originalMinDecryptionVersion + p.MinEncryptionVersion = originalMinEncryptionVersion + p.DeletionAllowed = originalDeletionAllowed + p.Exportable = originalExportable + p.AllowPlaintextBackup = originalAllowPlaintextBackup + } + }() + + resp = &logical.Response{} persistNeeded := false @@ -173,6 +189,13 @@ func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, d * return nil, nil } + switch { + case p.MinAvailableVersion > p.MinEncryptionVersion: + return logical.ErrorResponse("min encryption version should not be less than min available version"), nil + case p.MinAvailableVersion > p.MinDecryptionVersion: + return logical.ErrorResponse("min decryption version should not be less then min available version"), nil + } + if len(resp.Warnings) == 0 { return nil, p.Persist(ctx, req.Storage) } diff --git a/builtin/logical/transit/path_config_test.go b/builtin/logical/transit/path_config_test.go index 7da7dfcd482f..8dce5d0c45d3 100644 --- a/builtin/logical/transit/path_config_test.go +++ b/builtin/logical/transit/path_config_test.go @@ -14,7 +14,7 @@ func TestTransit_ConfigSettings(t *testing.T) { doReq := func(req *logical.Request) *logical.Response { resp, err := b.HandleRequest(context.Background(), req) - if err != nil { + if err != nil || (resp != nil && resp.IsError()) { t.Fatalf("got err:\n%#v\nreq:\n%#v\n", err, *req) } return resp diff --git a/builtin/logical/transit/path_keys.go b/builtin/logical/transit/path_keys.go index 332d05bb82a3..94068e631688 100644 --- a/builtin/logical/transit/path_keys.go +++ b/builtin/logical/transit/path_keys.go @@ -206,6 +206,7 @@ func (b *backend) pathPolicyRead(ctx context.Context, req *logical.Request, d *f "type": p.Type.String(), "derived": p.Derived, "deletion_allowed": p.DeletionAllowed, + "min_available_version": p.MinAvailableVersion, "min_decryption_version": p.MinDecryptionVersion, "min_encryption_version": p.MinEncryptionVersion, "latest_version": p.LatestVersion, diff --git a/builtin/logical/transit/path_trim.go b/builtin/logical/transit/path_trim.go new file mode 100644 index 000000000000..363ad0e6a4f4 --- /dev/null +++ b/builtin/logical/transit/path_trim.go @@ -0,0 +1,99 @@ +package transit + +import ( + "context" + + "github.com/hashicorp/vault/helper/keysutil" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func (b *backend) pathTrim() *framework.Path { + return &framework.Path{ + Pattern: "keys/" + framework.GenericNameRegex("name") + "/trim", + Fields: map[string]*framework.FieldSchema{ + "name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Name of the key", + }, + "min_available_version": &framework.FieldSchema{ + Type: framework.TypeInt, + Description: ` +The minimum available version for the key ring. All versions before this +version will be permanently deleted. This value can at most be equal to the +lesser of 'min_decryption_version' and 'min_encryption_version'. This is not +allowed to be set when either 'min_encryption_version' or +'min_decryption_version' is set to zero.`, + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.pathTrimUpdate(), + }, + + HelpSynopsis: pathTrimHelpSyn, + HelpDescription: pathTrimHelpDesc, + } +} + +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{ + Storage: req.Storage, + Name: name, + }) + if err != nil { + return nil, err + } + if p == nil { + return logical.ErrorResponse("invalid key name"), logical.ErrInvalidRequest + } + if !b.System().CachingDisabled() { + p.Lock(true) + } + defer p.Unlock() + + minAvailableVersionRaw, ok := d.GetOk("min_available_version") + if !ok { + return logical.ErrorResponse("missing min_available_version"), nil + } + minAvailableVersion := minAvailableVersionRaw.(int) + + originalMinAvailableVersion := p.MinAvailableVersion + + switch { + case minAvailableVersion < originalMinAvailableVersion: + return logical.ErrorResponse("minimum available version cannot be decremented"), nil + case p.MinEncryptionVersion == 0: + return logical.ErrorResponse("minimum available version cannot be set when minimum encryption version is not set"), nil + case p.MinDecryptionVersion == 0: + return logical.ErrorResponse("minimum available version cannot be set when minimum decryption version is not set"), nil + case minAvailableVersion > p.MinEncryptionVersion: + return logical.ErrorResponse("minimum available version cannot be greater than minmum encryption version"), nil + case minAvailableVersion > p.MinDecryptionVersion: + return logical.ErrorResponse("minimum available version cannot be greater than minimum decryption version"), nil + case minAvailableVersion < 0: + return logical.ErrorResponse("minimum available version cannot be negative"), nil + case minAvailableVersion == 0: + return logical.ErrorResponse("minimum available version should be positive"), nil + } + + // Ensure that cache doesn't get corrupted in error cases + p.MinAvailableVersion = minAvailableVersion + if err := p.Persist(ctx, req.Storage); err != nil { + p.MinAvailableVersion = originalMinAvailableVersion + return nil, err + } + + return nil, nil + } +} + +const pathTrimHelpSyn = `Trim key versions of a named key` + +const pathTrimHelpDesc = ` +This path is used to trim key versions of a named key. Trimming only happens +from the lower end of version numbers. +` diff --git a/builtin/logical/transit/path_trim_test.go b/builtin/logical/transit/path_trim_test.go new file mode 100644 index 000000000000..cab6fba56b17 --- /dev/null +++ b/builtin/logical/transit/path_trim_test.go @@ -0,0 +1,256 @@ +package transit + +import ( + "testing" + + "github.com/hashicorp/vault/helper/keysutil" + "github.com/hashicorp/vault/helper/namespace" + "github.com/hashicorp/vault/logical" +) + +func TestTransit_Trim(t *testing.T) { + b, storage := createBackendWithSysView(t) + + doReq := func(t *testing.T, req *logical.Request) *logical.Response { + t.Helper() + resp, err := b.HandleRequest(namespace.RootContext(nil), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("got err:\n%#v\nresp:\n%#v\n", err, resp) + } + return resp + } + doErrReq := func(t *testing.T, req *logical.Request) { + t.Helper() + resp, err := b.HandleRequest(namespace.RootContext(nil), req) + if err == nil && (resp == nil || !resp.IsError()) { + t.Fatalf("expected error; resp:\n%#v\n", resp) + } + } + + // Create a key + req := &logical.Request{ + Path: "keys/aes", + Storage: storage, + Operation: logical.UpdateOperation, + } + 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{ + Storage: storage, + Name: "aes", + }) + if err != nil { + t.Fatal(err) + } + + // Archive: 0, 1 + archive, err := p.LoadArchive(namespace.RootContext(nil), storage) + if err != nil { + t.Fatal(err) + } + // Index "0" in the archive is unused. Hence the length of the archived + // keys will always be 1 more than the actual number of keys. + if len(archive.Keys) != 2 { + t.Fatalf("bad: len of archived keys; expected: 2, actual: %d", len(archive.Keys)) + } + + // Ensure that there are 5 key versions, by rotating the key 4 times + for i := 0; i < 4; i++ { + req.Path = "keys/aes/rotate" + req.Data = nil + doReq(t, req) + } + + // Archive: 0, 1, 2, 3, 4, 5 + archive, err = p.LoadArchive(namespace.RootContext(nil), storage) + if err != nil { + t.Fatal(err) + } + if len(archive.Keys) != 6 { + t.Fatalf("bad: len of archived keys; expected: 6, actual: %d", len(archive.Keys)) + } + + // Min available version should not be set when min_encryption_version is not + // set + req.Path = "keys/aes/trim" + req.Data = map[string]interface{}{ + "min_available_version": 1, + } + doErrReq(t, req) + + // Set min_encryption_version to 4 + req.Path = "keys/aes/config" + req.Data = map[string]interface{}{ + "min_encryption_version": 4, + } + doReq(t, req) + + // Set min_decryption_version to 3 + req.Data = map[string]interface{}{ + "min_decryption_version": 3, + } + doReq(t, req) + + // Min available version cannot be greater than min encryption version + req.Path = "keys/aes/trim" + req.Data = map[string]interface{}{ + "min_available_version": 5, + } + doErrReq(t, req) + + // Min available version cannot be greater than min decryption version + req.Data["min_available_version"] = 4 + doErrReq(t, req) + + // Min available version cannot be negative + req.Data["min_available_version"] = -1 + doErrReq(t, req) + + // Min available version should be positive + req.Data["min_available_version"] = 0 + doErrReq(t, req) + + // Trim all keys before version 3. Index 0 and index 1 will be deleted from + // archived keys. + req.Data["min_available_version"] = 3 + doReq(t, req) + + // Archive: 3, 4, 5 + archive, err = p.LoadArchive(namespace.RootContext(nil), storage) + if err != nil { + t.Fatal(err) + } + if len(archive.Keys) != 3 { + t.Fatalf("bad: len of archived keys; expected: 3, actual: %d", len(archive.Keys)) + } + + // Min decryption version should not be less than min available version + req.Path = "keys/aes/config" + req.Data = map[string]interface{}{ + "min_decryption_version": 1, + } + doErrReq(t, req) + + // Min encryption version should not be less than min available version + req.Data = map[string]interface{}{ + "min_encryption_version": 2, + } + doErrReq(t, req) + + // Rotate 5 more times + for i := 0; i < 5; i++ { + doReq(t, &logical.Request{ + Path: "keys/aes/rotate", + Storage: storage, + Operation: logical.UpdateOperation, + }) + } + + // Archive: 3, 4, 5, 6, 7, 8, 9, 10 + archive, err = p.LoadArchive(namespace.RootContext(nil), storage) + if err != nil { + t.Fatal(err) + } + if len(archive.Keys) != 8 { + t.Fatalf("bad: len of archived keys; expected: 8, actual: %d", len(archive.Keys)) + } + + // Set min encryption version to 7 + req.Data = map[string]interface{}{ + "min_encryption_version": 7, + } + doReq(t, req) + + // Set min decryption version to 7 + req.Data = map[string]interface{}{ + "min_decryption_version": 7, + } + doReq(t, req) + + // Trim all versions before 7 + req.Path = "keys/aes/trim" + req.Data = map[string]interface{}{ + "min_available_version": 7, + } + doReq(t, req) + + // Archive: 7, 8, 9, 10 + archive, err = p.LoadArchive(namespace.RootContext(nil), storage) + if err != nil { + t.Fatal(err) + } + if len(archive.Keys) != 4 { + t.Fatalf("bad: len of archived keys; expected: 4, actual: %d", len(archive.Keys)) + } + + // Read the key + req.Path = "keys/aes" + req.Operation = logical.ReadOperation + resp := doReq(t, req) + keys := resp.Data["keys"].(map[string]int64) + if len(keys) != 4 { + t.Fatalf("bad: number of keys; expected: 4, actual: %d", len(keys)) + } + + // Test if moving the min_encryption_version and min_decryption_versions + // are working fine + + // Set min encryption version to 10 + req.Path = "keys/aes/config" + req.Operation = logical.UpdateOperation + req.Data = map[string]interface{}{ + "min_encryption_version": 10, + } + doReq(t, req) + if p.MinEncryptionVersion != 10 { + t.Fatalf("failed to set min encryption version") + } + + // Set min decryption version to 9 + req.Data = map[string]interface{}{ + "min_decryption_version": 9, + } + doReq(t, req) + if p.MinDecryptionVersion != 9 { + t.Fatalf("failed to set min encryption version") + } + + // Reduce the min decryption version to 8 + req.Data = map[string]interface{}{ + "min_decryption_version": 8, + } + doReq(t, req) + if p.MinDecryptionVersion != 8 { + t.Fatalf("failed to set min encryption version") + } + + // Reduce the min encryption version to 8 + req.Data = map[string]interface{}{ + "min_encryption_version": 8, + } + doReq(t, req) + if p.MinDecryptionVersion != 8 { + t.Fatalf("failed to set min decryption version") + } + + // Read the key to ensure that the keys are properly copied from the + // archive into the policy + req.Path = "keys/aes" + req.Operation = logical.ReadOperation + resp = doReq(t, req) + keys = resp.Data["keys"].(map[string]int64) + if len(keys) != 3 { + t.Fatalf("bad: number of keys; expected: 3, actual: %d", len(keys)) + } + + // Ensure that archive has remained unchanged + // Archive: 7, 8, 9, 10 + archive, err = p.LoadArchive(namespace.RootContext(nil), storage) + if err != nil { + t.Fatal(err) + } + if len(archive.Keys) != 4 { + t.Fatalf("bad: len of archived keys; expected: 4, actual: %d", len(archive.Keys)) + } +} diff --git a/helper/keysutil/policy.go b/helper/keysutil/policy.go index edba36a73821..4654c647b32a 100644 --- a/helper/keysutil/policy.go +++ b/helper/keysutil/policy.go @@ -326,6 +326,13 @@ type Policy struct { // a max. ArchiveVersion int `json:"archive_version"` + // ArchiveMinVersion is the minimum version of the key in the archive. + ArchiveMinVersion int `json:"archive_min_version"` + + // MinAvailableVersion is the minimum version of the key present. All key + // versions before this would have been deleted. + MinAvailableVersion int `json:"min_available_version"` + // Whether the key is allowed to be deleted DeletionAllowed bool `json:"deletion_allowed"` @@ -462,7 +469,7 @@ func (p *Policy) handleArchiving(ctx context.Context, storage logical.Storage) e if !keysContainsMinimum { // Need to move keys *from* archive for i := p.MinDecryptionVersion; i <= p.LatestVersion; i++ { - p.Keys[strconv.Itoa(i)] = archive.Keys[i] + p.Keys[strconv.Itoa(i)] = archive.Keys[i-p.MinAvailableVersion] } return nil @@ -473,9 +480,9 @@ func (p *Policy) handleArchiving(ctx context.Context, storage logical.Storage) e // We need a size that is equivalent to the latest version (number of keys) // but adding one since slice numbering starts at 0 and we're indexing by // key version - if len(archive.Keys) < p.LatestVersion+1 { + if len(archive.Keys)+p.MinAvailableVersion < p.LatestVersion+1 { // Increase the size of the archive slice - newKeys := make([]KeyEntry, p.LatestVersion+1) + newKeys := make([]KeyEntry, p.LatestVersion-p.MinAvailableVersion+1) copy(newKeys, archive.Keys) archive.Keys = newKeys } @@ -483,10 +490,16 @@ func (p *Policy) handleArchiving(ctx context.Context, storage logical.Storage) e // We are storing all keys in the archive, so we ensure that it is up to // date up to p.LatestVersion for i := p.ArchiveVersion + 1; i <= p.LatestVersion; i++ { - archive.Keys[i] = p.Keys[strconv.Itoa(i)] + archive.Keys[i-p.MinAvailableVersion] = p.Keys[strconv.Itoa(i)] p.ArchiveVersion = i } + // Trim the keys if required + if p.ArchiveMinVersion < p.MinAvailableVersion { + archive.Keys = archive.Keys[p.MinAvailableVersion-p.ArchiveMinVersion:] + p.ArchiveMinVersion = p.MinAvailableVersion + } + err = p.storeArchive(ctx, storage, archive) if err != nil { return err diff --git a/website/source/api/secret/transit/index.html.md b/website/source/api/secret/transit/index.html.md index d0f37c5c290a..537ee546edd5 100644 --- a/website/source/api/secret/transit/index.html.md +++ b/website/source/api/secret/transit/index.html.md @@ -997,3 +997,38 @@ $ curl \ --data @payload.json \ http://127.0.0.1:8200/v1/transit/restore ``` + +## Trim Key + +This endpoint trims older key versions setting a minimum version for the +keyring. Once trimmed, previous versions of the key cannot be recovered. + +| Method | Path | Produces | +| :------- | :------------------------- | :--------------------- | +| `POST` | `/transit/keys/:name/trim` | `200 application/json` | + +### Parameters + +- `min_version` `(int: )` - The minimum version for the key ring. All + versions before this version will be permanently deleted. This value can at + most be equal to the lesser of `min_decryption_version` and + `min_encryption_version`. This is not allowed to be set when either + `min_encryption_version` or `min_decryption_version` is set to zero. + +### Sample Payload + +```json +{ + "min_version": 2 +} +``` + +### Sample Request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request POST \ + --data @payload.json \ + http://127.0.0.1:8200/v1/transit/keys/my-key/trim +```