Add HTTP PATCH support for KV key metadata (#13215)

* go get vault-plugin-secrets-kv@vault-4290-patch-metadata

* add kv metadata patch command

* add changelog entry

* success tests for kv metadata patch flags

* add more kv metadata patch flags tests

* add kv metadata patch cas warning test

* add kv-v2 key metadata patch API docs

* add kv metadata patch to docs

* prevent unintentional field overwriting in kv metadata put cmd

* like create/update ops, prevent patch to paths ending in /

* fix kv metadata patch cmd in docs

* fix flag defaults for kv metadata put

* go get vault-plugin-secrets-kv@vault-4290-patch-metadata

* fix TestKvMetadataPatchCommand_Flags test

* doc fixes

* go get vault-plugin-secrets-kv@master; go mod tidy
This commit is contained in:
Chris Capurso 2022-01-12 12:05:27 -05:00 committed by GitHub
parent 2adf4df7d7
commit d52d69e4bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 633 additions and 43 deletions

3
changelog/13215.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
secrets/kv: add patch support for KVv2 key metadata
```

View File

@ -718,6 +718,11 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
BaseCommand: getBaseCommand(),
}, nil
},
"kv metadata patch": func() (cli.Command, error) {
return &KVMetadataPatchCommand{
BaseCommand: getBaseCommand(),
}, nil
},
"kv metadata get": func() (cli.Command, error) {
return &KVMetadataGetCommand{
BaseCommand: getBaseCommand(),

View File

@ -0,0 +1,193 @@
package command
import (
"context"
"fmt"
"io"
"strings"
"time"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var (
_ cli.Command = (*KVMetadataPutCommand)(nil)
_ cli.CommandAutocomplete = (*KVMetadataPutCommand)(nil)
)
type KVMetadataPatchCommand struct {
*BaseCommand
flagMaxVersions int
flagCASRequired BoolPtr
flagDeleteVersionAfter time.Duration
flagCustomMetadata map[string]string
testStdin io.Reader // for tests
}
func (c *KVMetadataPatchCommand) Synopsis() string {
return "Patches key settings in the KV store"
}
func (c *KVMetadataPatchCommand) Help() string {
helpText := `
Usage: vault metadata kv patch [options] KEY
This command can be used to create a blank key in the key-value store or to
update key configuration for a specified key.
Create a key in the key-value store with no data:
$ vault kv metadata patch secret/foo
Set a max versions setting on the key:
$ vault kv metadata patch -max-versions=5 secret/foo
Set delete-version-after on the key:
$ vault kv metadata patch -delete-version-after=3h25m19s secret/foo
Require Check-and-Set for this key:
$ vault kv metadata patch -cas-required secret/foo
Set custom metadata on the key:
$ vault kv metadata patch -custom-metadata=foo=abc -custom-metadata=bar=123 secret/foo
Additional flags and more advanced use cases are detailed below.
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *KVMetadataPatchCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
// Common Options
f := set.NewFlagSet("Common Options")
f.IntVar(&IntVar{
Name: "max-versions",
Target: &c.flagMaxVersions,
Default: -1,
Usage: `The number of versions to keep. If not set, the backends configured max version is used.`,
})
f.BoolPtrVar(&BoolPtrVar{
Name: "cas-required",
Target: &c.flagCASRequired,
Usage: `If true the key will require the cas parameter to be set on all write requests. If false, the backends configuration will be used.`,
})
f.DurationVar(&DurationVar{
Name: "delete-version-after",
Target: &c.flagDeleteVersionAfter,
Default: -1,
EnvVar: "",
Completion: complete.PredictAnything,
Usage: `Specifies the length of time before a version is deleted.
If not set, the backend's configured delete-version-after is used. Cannot be
greater than the backend's delete-version-after. The delete-version-after is
specified as a numeric string with a suffix like "30s" or
"3h25m19s".`,
})
f.StringMapVar(&StringMapVar{
Name: "custom-metadata",
Target: &c.flagCustomMetadata,
Default: map[string]string{},
Usage: `Specifies arbitrary version-agnostic key=value metadata meant to describe a secret.
This can be specified multiple times to add multiple pieces of metadata.`,
})
return set
}
func (c *KVMetadataPatchCommand) AutocompleteArgs() complete.Predictor {
return nil
}
func (c *KVMetadataPatchCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *KVMetadataPatchCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
path := sanitizePath(args[0])
mountPath, v2, err := isKVv2(path, client)
if err != nil {
c.UI.Error(err.Error())
return 2
}
if !v2 {
c.UI.Error("Metadata not supported on KV Version 1")
return 1
}
path = addPrefixToVKVPath(path, mountPath, "metadata")
data := map[string]interface{}{}
if c.flagMaxVersions >= 0 {
data["max_versions"] = c.flagMaxVersions
}
if c.flagCASRequired.IsSet() {
data["cas_required"] = c.flagCASRequired.Get()
}
if c.flagDeleteVersionAfter >= 0 {
data["delete_version_after"] = c.flagDeleteVersionAfter.String()
}
if len(c.flagCustomMetadata) > 0 {
data["custom_metadata"] = c.flagCustomMetadata
}
secret, err := client.Logical().JSONMergePatch(context.Background(), path, data)
if err != nil {
c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err))
if secret != nil {
OutputSecret(c.UI, secret)
}
return 2
}
if secret == nil {
// Don't output anything unless using the "table" format
if Format(c.UI) == "table" {
c.UI.Info(fmt.Sprintf("Success! Data written to: %s", path))
}
return 0
}
return OutputSecret(c.UI, secret)
}

View File

@ -0,0 +1,273 @@
package command
import (
"encoding/json"
"io"
"strings"
"testing"
"github.com/go-test/deep"
"github.com/hashicorp/vault/api"
"github.com/mitchellh/cli"
)
func testKVMetadataPatchCommand(tb testing.TB) (*cli.MockUi, *KVMetadataPatchCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &KVMetadataPatchCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func kvMetadataPatchWithRetry(t *testing.T, client *api.Client, args []string, stdin *io.PipeReader) (int, string) {
t.Helper()
return retryKVCommand(t, func() (int, string) {
ui, cmd := testKVMetadataPatchCommand(t)
cmd.client = client
if stdin != nil {
cmd.testStdin = stdin
}
code := cmd.Run(args)
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
return code, combined
})
}
func kvMetadataPutWithRetry(t *testing.T, client *api.Client, args []string, stdin *io.PipeReader) (int, string) {
t.Helper()
return retryKVCommand(t, func() (int, string) {
ui, cmd := testKVMetadataPutCommand(t)
cmd.client = client
if stdin != nil {
cmd.testStdin = stdin
}
code := cmd.Run(args)
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
return code, combined
})
}
func TestKvMetadataPatchCommand_EmptyArgs(t *testing.T) {
client, closer := testVaultServer(t)
defer closer()
if err := client.Sys().Mount("kv/", &api.MountInput{
Type: "kv-v2",
}); err != nil {
t.Fatalf("kv-v2 mount error: %#v", err)
}
args := make([]string, 0)
code, combined := kvMetadataPatchWithRetry(t, client, args, nil)
expectedCode := 1
expectedOutput := "Not enough arguments"
if code != expectedCode {
t.Fatalf("expected code to be %d but was %d for patch cmd with args %#v", expectedCode, code, args)
}
if !strings.Contains(combined, expectedOutput) {
t.Fatalf("expected output to be %q but was %q for patch cmd with args %#v", expectedOutput, combined, args)
}
}
func TestKvMetadataPatchCommand_Flags(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
expectedUpdates map[string]interface{}
}{
{
"cas_required_success",
[]string{"-cas-required=true"},
"Success!",
0,
map[string]interface{}{
"cas_required": true,
},
},
{
"cas_required_invalid",
[]string{"-cas-required=12345"},
"invalid boolean value",
1,
map[string]interface{}{},
},
{
"custom_metadata_success",
[]string{"-custom-metadata=baz=ghi"},
"Success!",
0,
map[string]interface{}{
"custom_metadata": map[string]interface{}{
"foo": "abc",
"bar": "def",
"baz": "ghi",
},
},
},
{
"delete_version_after_success",
[]string{"-delete-version-after=5s"},
"Success!",
0,
map[string]interface{}{
"delete_version_after": "5s",
},
},
{
"delete_version_after_invalid",
[]string{"-delete-version-after=false"},
"invalid duration",
1,
map[string]interface{}{},
},
{
"max_versions_success",
[]string{"-max-versions=10"},
"Success!",
0,
map[string]interface{}{
"max_versions": json.Number("10"),
},
},
{
"max_versions_invalid",
[]string{"-max-versions=false"},
"invalid syntax",
1,
map[string]interface{}{},
},
{
"multiple_flags_success",
[]string{"-max-versions=20", "-custom-metadata=baz=123"},
"Success!",
0,
map[string]interface{}{
"max_versions": json.Number("20"),
"custom_metadata": map[string]interface{}{
"foo": "abc",
"bar": "def",
"baz": "123",
},
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
client, closer := testVaultServer(t)
defer closer()
basePath := t.Name() + "/"
secretPath := basePath + "my-secret"
metadataPath := basePath + "metadata/" + "my-secret"
if err := client.Sys().Mount(basePath, &api.MountInput{
Type: "kv-v2",
}); err != nil {
t.Fatalf("kv-v2 mount error: %#v", err)
}
putArgs := []string{"-cas-required=true", "-custom-metadata=foo=abc", "-custom-metadata=bar=def", secretPath}
code, combined := kvMetadataPutWithRetry(t, client, putArgs, nil)
if code != 0 {
t.Fatalf("initial metadata put failed, code: %d, output: %s", code, combined)
}
initialMetadata, err := client.Logical().Read(metadataPath)
if err != nil {
t.Fatalf("metadata read failed, err: %#v", err)
}
patchArgs := append(tc.args, secretPath)
code, combined = kvMetadataPatchWithRetry(t, client, patchArgs, nil)
if !strings.Contains(combined, tc.out) {
t.Fatalf("expected output to be %q but was %q for patch cmd with args %#v", tc.out, combined, patchArgs)
}
if code != tc.code {
t.Fatalf("expected code to be %d but was %d for patch cmd with args %#v", tc.code, code, patchArgs)
}
patchedMetadata, err := client.Logical().Read(metadataPath)
if err != nil {
t.Fatalf("metadata read failed, err: %#v", err)
}
for k, v := range patchedMetadata.Data {
var expectedVal interface{}
if inputVal, ok := tc.expectedUpdates[k]; ok {
expectedVal = inputVal
} else {
expectedVal = initialMetadata.Data[k]
}
if diff := deep.Equal(expectedVal, v); len(diff) > 0 {
t.Fatalf("patched %q mismatch, diff: %#v", k, diff)
}
}
})
}
}
func TestKvMetadataPatchCommand_CasWarning(t *testing.T) {
client, closer := testVaultServer(t)
defer closer()
basePath := "kv/"
if err := client.Sys().Mount(basePath, &api.MountInput{
Type: "kv-v2",
}); err != nil {
t.Fatalf("kv-v2 mount error: %#v", err)
}
secretPath := basePath + "my-secret"
args := []string{"-cas-required=true", secretPath}
code, combined := kvMetadataPutWithRetry(t, client, args, nil)
if code != 0 {
t.Fatalf("metadata put failed, code: %d, output: %s", code, combined)
}
casConfig := map[string]interface{}{
"cas_required": true,
}
_, err := client.Logical().Write(basePath + "config", casConfig)
if err != nil {
t.Fatalf("config write failed, err: #%v", err)
}
args = []string{"-cas-required=false", secretPath}
code, combined = kvMetadataPatchWithRetry(t, client, args, nil)
if code != 0 {
t.Fatalf("expected code to be 0 but was %d for patch cmd with args %#v", code, args)
}
expectedOutput := "\"cas_required\" set to false, but is mandated by backend config"
if !strings.Contains(combined, expectedOutput) {
t.Fatalf("expected output to be %q but was %q for patch cmd with args %#v", expectedOutput, combined, args)
}
}

View File

@ -19,7 +19,7 @@ type KVMetadataPutCommand struct {
*BaseCommand
flagMaxVersions int
flagCASRequired bool
flagCASRequired BoolPtr
flagDeleteVersionAfter time.Duration
flagCustomMetadata map[string]string
testStdin io.Reader // for tests
@ -71,14 +71,13 @@ func (c *KVMetadataPutCommand) Flags() *FlagSets {
f.IntVar(&IntVar{
Name: "max-versions",
Target: &c.flagMaxVersions,
Default: 0,
Default: -1,
Usage: `The number of versions to keep. If not set, the backends configured max version is used.`,
})
f.BoolVar(&BoolVar{
f.BoolPtrVar(&BoolPtrVar{
Name: "cas-required",
Target: &c.flagCASRequired,
Default: false,
Usage: `If true the key will require the cas parameter to be set on all write requests. If false, the backends configuration will be used.`,
})
@ -151,16 +150,24 @@ func (c *KVMetadataPutCommand) Run(args []string) int {
}
path = addPrefixToVKVPath(path, mountPath, "metadata")
data := map[string]interface{}{
"max_versions": c.flagMaxVersions,
"cas_required": c.flagCASRequired,
"custom_metadata": c.flagCustomMetadata,
data := map[string]interface{}{}
if c.flagMaxVersions >= 0 {
data["max_versions"] = c.flagMaxVersions
}
if c.flagDeleteVersionAfter >= 0 {
data["delete_version_after"] = c.flagDeleteVersionAfter.String()
}
if c.flagCASRequired.IsSet() {
data["cas_required"] = c.flagCASRequired.Get()
}
if len(c.flagCustomMetadata) > 0 {
data["custom_metadata"] = c.flagCustomMetadata
}
secret, err := client.Logical().Write(path, data)
if err != nil {
c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err))

View File

@ -1,11 +1,13 @@
package command
import (
"encoding/json"
"strings"
"testing"
"github.com/go-test/deep"
"github.com/hashicorp/vault/api"
"github.com/mitchellh/cli"
"strings"
"testing"
)
func testKVMetadataPutCommand(tb testing.TB) (*cli.MockUi, *KVMetadataPutCommand) {
@ -19,7 +21,7 @@ func testKVMetadataPutCommand(tb testing.TB) (*cli.MockUi, *KVMetadataPutCommand
}
}
func TestKvMetadataPutCommandDeleteVersionAfter(t *testing.T) {
func TestKvMetadataPutCommand_DeleteVersionAfter(t *testing.T) {
client, closer := testVaultServer(t)
defer closer()
@ -78,7 +80,7 @@ func TestKvMetadataPutCommandDeleteVersionAfter(t *testing.T) {
}
}
func TestKvMetadataPutCommandCustomMetadata(t *testing.T) {
func TestKvMetadataPutCommand_CustomMetadata(t *testing.T) {
client, closer := testVaultServer(t)
defer closer()
@ -154,3 +156,47 @@ func TestKvMetadataPutCommandCustomMetadata(t *testing.T) {
t.Fatal(diff)
}
}
func TestKvMetadataPutCommand_UnprovidedFlags(t *testing.T) {
client, closer := testVaultServer(t)
defer closer()
basePath := t.Name() + "/"
secretPath := basePath + "my-secret"
if err := client.Sys().Mount(basePath, &api.MountInput{
Type: "kv-v2",
}); err != nil {
t.Fatalf("kv-v2 mount error: %#v", err)
}
_, cmd := testKVMetadataPutCommand(t)
cmd.client = client
args := []string{"-cas-required=true", "-max-versions=10", secretPath}
code, _ := kvMetadataPutWithRetry(t, client, args, nil)
if code != 0 {
t.Fatalf("expected 0 exit status but received %d", code)
}
args = []string{"-custom-metadata=foo=bar", secretPath}
code, _ = kvMetadataPutWithRetry(t, client, args, nil)
if code != 0 {
t.Fatalf("expected 0 exit status but received %d", code)
}
secret, err := client.Logical().Read(basePath + "metadata/" + "my-secret")
if err != nil {
t.Fatal(err)
}
if secret.Data["cas_required"] != true {
t.Fatalf("expected cas_required to be true but received %#v", secret.Data["cas_required"])
}
if secret.Data["max_versions"] != json.Number("10") {
t.Fatalf("expected max_versions to be 10 but received %#v", secret.Data["max_versions"])
}
}

4
go.mod
View File

@ -108,7 +108,7 @@ require (
github.com/hashicorp/vault-plugin-secrets-azure v0.11.2
github.com/hashicorp/vault-plugin-secrets-gcp v0.11.1
github.com/hashicorp/vault-plugin-secrets-gcpkms v0.10.0
github.com/hashicorp/vault-plugin-secrets-kv v0.5.7-0.20211123171606-16933c88368a
github.com/hashicorp/vault-plugin-secrets-kv v0.5.7-0.20220112155832-c2eb38b5f5b6
github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.5.1
github.com/hashicorp/vault-plugin-secrets-openldap v0.6.0
github.com/hashicorp/vault-plugin-secrets-terraform v0.3.0
@ -116,7 +116,7 @@ require (
github.com/hashicorp/vault/api v1.3.1
github.com/hashicorp/vault/api/auth/approle v0.1.0
github.com/hashicorp/vault/api/auth/userpass v0.1.0
github.com/hashicorp/vault/sdk v0.3.0
github.com/hashicorp/vault/sdk v0.3.1-0.20220112143259-b48602fdb885
github.com/influxdata/influxdb v0.0.0-20190411212539-d24b7ba8c4c4
github.com/jcmturner/gokrb5/v8 v8.4.2
github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f

4
go.sum
View File

@ -965,8 +965,8 @@ github.com/hashicorp/vault-plugin-secrets-gcp v0.11.1 h1:v8XfuZVrgP4pIwaZe/GgrPC
github.com/hashicorp/vault-plugin-secrets-gcp v0.11.1/go.mod h1:ndpmRkIPHW5UYqv2nn2AJNVZsucJ8lY2bp5i5Ngvhuc=
github.com/hashicorp/vault-plugin-secrets-gcpkms v0.10.0 h1:0Vi5WEIpZctk/ZoRClodV9WCnM/lCzw9XekMhRZdo8k=
github.com/hashicorp/vault-plugin-secrets-gcpkms v0.10.0/go.mod h1:6DPwGu8oGR1sZRpjwkcAnrQZWQuAJ/Ph+rQHfUo1Yf4=
github.com/hashicorp/vault-plugin-secrets-kv v0.5.7-0.20211123171606-16933c88368a h1:GVA3sY+FRhQrMexWGMCsIfVVMgcdru36WMKvDtKed5I=
github.com/hashicorp/vault-plugin-secrets-kv v0.5.7-0.20211123171606-16933c88368a/go.mod h1:TNPRoB53Twd9tYvlhqqEhMsQPiVN604kZw9jr2zUzDk=
github.com/hashicorp/vault-plugin-secrets-kv v0.5.7-0.20220112155832-c2eb38b5f5b6 h1:Z3NnaIBragxW6iTW7OnvklRzZSZdaidxjs/vkCneGAg=
github.com/hashicorp/vault-plugin-secrets-kv v0.5.7-0.20220112155832-c2eb38b5f5b6/go.mod h1:9V2Ecim3m/qw+YAQelUeFADqZ1GVo8xwoLqfKsqh9pI=
github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.5.1 h1:Maewon4nu0KL1ALBOvL6Rsj+Qyr9hdULWflyMz7+9nk=
github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.5.1/go.mod h1:PLx2vxXukfsKsDRo/PlG4fxmJ1d+H2h82wT3vf4buuI=
github.com/hashicorp/vault-plugin-secrets-openldap v0.6.0 h1:d6N/aMlklMfEacyiIuu5ZnTlADhGkGZkDrOtQXBRuhI=

View File

@ -464,7 +464,8 @@ func (c *Core) handleCancelableRequest(ctx context.Context, req *logical.Request
// backends. Basically, it's all just terrible, so don't allow it.
if strings.HasSuffix(req.Path, "/") &&
(req.Operation == logical.UpdateOperation ||
req.Operation == logical.CreateOperation) {
req.Operation == logical.CreateOperation ||
req.Operation == logical.PatchOperation) {
return logical.ErrorResponse("cannot write to a path ending in '/'"), nil
}

View File

@ -29,7 +29,7 @@ key-value store.
- `max_versions` `(int: 0)` The number of versions to keep per key. This value
applies to all keys, but a key's metadata setting can overwrite this value.
Once a key has more than the configured allowed versions the oldest version
Once a key has more than the configured allowed versions, the oldest version
will be permanently deleted. When 0 is used or the value is unset, Vault
will keep 10 versions.
@ -519,10 +519,10 @@ It does not create a new version.
- `max_versions` `(int: 0)` The number of versions to keep per key. If not
set, the backends configured max version is used. Once a key has more than
the configured allowed versions the oldest version will be permanently
the configured allowed versions, the oldest version will be permanently
deleted.
- `cas_required` `(bool: false)` If true the key will require the cas
- `cas_required` `(bool: false)` If true, the key will require the `cas`
parameter to be set on all write requests. If false, the backends
configuration will be used.
@ -561,6 +561,60 @@ $ curl \
https://127.0.0.1:8200/v1/secret/metadata/my-secret
```
## Patch Metadata
This endpoint patches an existing metadata entry of a secret at the specified
location. The calling token must have an ACL policy granting the `patch`
capability. Currently, only JSON merge patch is supported and must be specified
using a `Content-Type` header value of `application/merge-patch+json`. It does
not create a new version.
| Method | Path |
| :------ | :----------------------- |
| `PATCH` | `/secret/metadata/:path` |
### Parameters
- `max_versions` `(int: 0)` The number of versions to keep per key. If not
set, the backends configured max version is used. Once a key has more than
the configured allowed versions, the oldest version will be permanently
deleted.
- `cas_required` `(bool: false)` If true, the key will require the `cas`
parameter to be set on all write requests. If false, the backends
configuration will be used.
- `delete_version_after` `(string:"0s")` Set the `delete_version_after` value
to a duration to specify the `deletion_time` for all new versions
written to this key. If not set, the backend's `delete_version_after` will be
used. If the value is greater than the backend's `delete_version_after`, the
backend's `delete_version_after` will be used. Accepts [Go duration
format string][duration-godoc].
- `custom_metadata` `(map<string|string>: nil)` - A map of arbitrary string to string valued user-provided metadata meant
to describe the secret.
### Sample Payload
```json
{
"max_versions": 5,
"custom_metadata": {
"bar": "123",
}
}
```
### Sample Request
```shell-session
$ curl \
--header "X-Vault-Token: ..." \
--header "Content-Type: application/merge-patch+json"
--request PATCH \
--data @payload.json \
https://127.0.0.1:8200/v1/secret/metadata/my-secret
```
## Delete Metadata and All Versions
This endpoint permanently deletes the key metadata and all version data for the

View File

@ -169,7 +169,7 @@ allows for writing keys with arbitrary values.
1. Write arbitrary data:
```text
```shell-session
$ vault kv put secret/my-secret foo=a bar=b
Key Value
--- -----
@ -182,7 +182,7 @@ allows for writing keys with arbitrary values.
1. Read arbitrary data:
```text
```shell-session
$ vault kv get secret/my-secret
====== Metadata ======
Key Value
@ -206,7 +206,7 @@ allows for writing keys with arbitrary values.
allowed if the keys current version matches the version specified in the
cas parameter.
```text
```shell-session
$ vault kv put -cas=1 secret/my-secret foo=aa bar=bb
Key Value
--- -----
@ -219,7 +219,7 @@ allows for writing keys with arbitrary values.
1. Reading now will return the newest version of the data:
```text
```shell-session
$ vault kv get secret/my-secret
====== Metadata ======
Key Value
@ -249,7 +249,7 @@ allows for writing keys with arbitrary values.
read-then-write flow will use the `version` value from the secret returned by
the read to perform a check-and-set operation in the subsequent write.
```text
```shell-session
$ vault kv patch -cas=2 secret/my-secret bar=bbb
Key Value
--- -----
@ -266,7 +266,7 @@ allows for writing keys with arbitrary values.
Perform a patch using the `patch` method:
```text
```shell-session
$ vault kv patch -method=patch -cas=2 secret/my-secret bar=bbb
Key Value
--- -----
@ -278,7 +278,7 @@ allows for writing keys with arbitrary values.
```
Perform a patch using the read-then-write method:
```text
```shell-session
$ vault kv patch -method=rw secret/my-secret bar=bbb
Key Value
--- -----
@ -292,7 +292,7 @@ allows for writing keys with arbitrary values.
1. Reading after a patch will return the newest version of the data in which
only the specified fields were updated:
```text
```shell-session
$ vault kv get secret/my-secret
====== Metadata ======
Key Value
@ -312,7 +312,7 @@ allows for writing keys with arbitrary values.
1. Previous versions can be accessed with the `-version` flag:
```text
```shell-session
$ vault kv get -version=1 secret/my-secret
====== Metadata ======
Key Value
@ -349,14 +349,14 @@ See the commands below for more information:
1. The latest version of a key can be deleted with the delete command, this also
takes a `-versions` flag to delete prior versions:
```text
```shell-session
$ vault kv delete secret/my-secret
t
Success! Data deleted (if it existed) at: secret/my-secret
```
1. Versions can be undeleted:
```text
```shell-session
$ vault kv undelete -versions=2 secret/my-secret
Success! Data written to: secret/undelete/my-secret
@ -378,7 +378,7 @@ See the commands below for more information:
1. Destroying a version permanently deletes the underlying data:
```text
```shell-session
$ vault kv destroy -versions=2 secret/my-secret
Success! Data written to: secret/destroy/my-secret
```
@ -393,7 +393,7 @@ See the commands below for more information:
1. All metadata and versions for a key can be viewed:
```text
```shell-session
$ vault kv metadata get secret/my-secret
========== Metadata ==========
Key Value
@ -424,7 +424,7 @@ See the commands below for more information:
1. The metadata settings for a key can be configured:
```text
```shell-session
$ vault kv metadata put -max-versions 2 -delete-version-after="3h25m19s" secret/my-secret
Success! Data written to: secret/metadata/my-secret
```
@ -432,7 +432,7 @@ See the commands below for more information:
Delete-version-after settings will apply only to new versions. Max versions
changes will be applied on next write:
```text
```shell-session
$ vault kv put secret/my-secret my-value=newer-s3cr3t
Key Value
--- -----
@ -446,7 +446,7 @@ See the commands below for more information:
Once a key has more versions than max versions the oldest versions
are cleaned up:
```text
```shell-session
$ vault kv metadata get secret/my-secret
========== Metadata ==========
Key Value
@ -476,17 +476,25 @@ See the commands below for more information:
```
A secret's key metadata can contain custom metadata used to describe the secret. The
data will be stored as string-to-string key-value pairs. If the `-custom-metadata` flag
is set, the value of `custom_metadata` will be fully overwritten. The `-custom-metadata`
flag can be repeated to add multiple key-value pairs:
data will be stored as string-to-string key-value pairs. The `-custom-metadata`
flag can be repeated to add multiple key-value pairs.
```text
vault kv metadata put -custom-metadata=foo=abc -custom-metadata=bar=123 secret/my-secret
The `vault kv metadata put` command can be used to fully overwrite the value of `custom_metadata`:
```shell-session
$ vault kv metadata put -custom-metadata=foo=abc -custom-metadata=bar=123 secret/my-secret
```
The `vault kv metadata patch` command can be used to partially overwrite the value of `custom_metadata`.
The following invocation will update `custom_metadata` sub-field `foo` but leave `bar` untouched:
```shell-session
$ vault kv metadata patch -custom-metadata=foo=def secret/my-secret
```
1. Permanently delete all metadata and versions for a key:
```text
```shell-session
$ vault kv metadata delete secret/my-secret
Success! Data deleted (if it existed) at: secret/metadata/my-secret
```