Sägetstrasse 18, 3123 Belp, Switzerland +41 79 173 36 84 info@ict.technology

    HashiCorp Vault Deep Dive – Part 2b: Practical Work with the Key/Value Secrets Engine

    The Key/Value Secrets Engine is an integral part of almost every Vault implementation. It forms the foundation for securely storing static secrets and is used far more frequently in practice than many dynamic engines.

    Following the theoretical introduction in part 2a, this article turns to the practical work with the KV Engine. We demonstrate how to write, read, update and delete secrets, and provide a practical analysis of the differences between KV Version 1 and Version 2. The focus is on production-relevant commands, realistic pitfalls and concrete recommendations for day-to-day operations, which is why I present this knowledge as a mixture of tutorial and cheat sheet.

    The vault kv Command: Your Day-to-Day Toolbox

    Interaction with the KV Engine is handled via the vault kv command. This command abstracts the internal differences between the versions but clearly indicates in its output whether a v1 or v2 engine is in use. The basic subcommands can be divided into two groups:

    Basic operations (available in KV v1 and v2):

    • put: Write or update secrets
    • get: Retrieve secrets
    • delete: Delete secrets (not necessarily final)
    • list: Display all available paths

    Advanced operations (KV v2 only):

    • patch: Update individual keys without overwriting others
    • rollback: Revert to a specific version
    • undelete: Restore deleted versions
    • destroy: Irrevocably remove versions

    These advanced commands are only available in KV v2 because they rely on internally stored metadata. The older v1 does not yet support metadata.

    Writing Secrets with put: Simple, but with Pitfalls

    The write command vault kv put is the central method for writing secrets to the KV Engine:


    vault kv put <path> <key>=<value> [<key>=<value>...]

    A simple example, first we enable the secrets engines:


    $ vault secrets enable kv
    Success! Enabled the kv secrets engine at: kv/
    $
    $ vault secrets enable -path=kvv2 kv-v2
    Success! Enabled the kv-v2 secrets engine at: kvv2/

    The following command stores the secret under the path kv/app/db with the key pass and the value 123. The output differs depending on the version used:

    KV Version 1:


    $ vault kv put kv/app/db pass=123 Success! Data written to: kv/app/db $

    KV Version 2:


    $ vault kv put kvv2/app/db pass=123
    == Secret Path ==
    kvv2/data/app/db
    
    ======= Metadata =======
    Key                Value
    ---                -----
    created_time       2025-07-01T10:52:07.551872783Z
    custom_metadata    <nil>
    deletion_time      n/a
    destroyed          false
    version            1
    $ 

    Version 2 provides metadata such as creation time and current version.

    Important note: With KV v2, storage is done under an internal path with the additional prefix /data/. This is particularly important when using the API directly, as it means that you cannot simply migrate from v1 to v2 without potentially adjusting scripts and applications accordingly!

    Storing Multiple Key/Value Pairs at Once

    A single put command can store any number of keys:


    $ vault kv put kv/app/db pass=123 user=admin api=myapisecret
    Success! Data written to: kv/app/db
    $ vault kv put kvv2/app/db pass=123 user=admin api=myapisecret
    == Secret Path ==
    kvv2/data/app/db
    
    ======= Metadata =======
    Key                Value
    ---                -----
    created_time       2025-07-01T10:56:04.624145825Z
    custom_metadata    <nil>
    deletion_time      n/a
    destroyed          false
    version            2
    [rramge@ol9 terraform-vault-kv]$ 

    In practice, this saves commands, but don't forget: The entire content at the path is replaced. If in doubt, it is better to update keys individually to improve script readability and reduce the risk of errors - or use files as shown in the next section.

    Importing Secrets from JSON Files

    Especially in automated environments or when dealing with extensive secret structures, using files for inputting multiple key/value pairs is recommended:


    $ vault kv put kv/app/db @secrets.json

    Example content of the file secrets.json:



    {
      "pass": "123",
      "user": "admin",
      "api": "myapisecret"
    }

    Important Difference Between put and patch

    Many Vault users make a critical mistake early on: They use put to update individual values.

    This always results in all previously stored secret data being deleted in KV v1 and v2, unless it is included again in the new put command.

    Example:


    # Initial state 
    $ vault kv put kv/app/db pass=123 user=admin api=myoldapisecret
    Success! Data written to: kv/app/db
    $

    # Updating the API key, mistakenly using "put"
    $ vault kv put kv/app/db api=mynewapisecret
    Success! Data written to: kv/app/db
    $ # Result: Only "api" remains $ vault kv get kv/app/db
    === Data ===
    Key Value
    --- -----
    api mynewapisecret
    $


    The keys user and pass are therefore lost.

    vault kv put was the wrong command if you wanted to preserve the existing user and pass keys. Only patch prevents their deletion. We will cover that below. For now, remember that put is destructive:

    Important: A vault kv put command always replaces all data at the specified path. It is not a merge operation!

    Reading Secrets with get: A Look into Storage

    With vault kv get you retrieve secrets. By default, the latest version is always displayed.

    For example, this command:


    vault kv get kv/app/db

    With KV v1:


    $ vault kv get kv/app/db
    ==== Data ====
    Key Value
    --- -----
    api myapisecret
    pass 123
    user admin
    $ 

    With KV v2:


    $ vault kv get kvv2/app/db
    == Secret Path ==
    kvv2/data/app/db
    ======= Metadata =======
    Key Value
    --- -----
    created_time 2025-07-01T10:56:04.624145825Z
    custom_metadata <nil>
    deletion_time n/a
    destroyed false
    version 2
    ==== Data ====
    Key Value
    --- -----
    api myapisecret
    pass 123
    user admin
    $

    Note: The /data/ path is also visible in KV v2. If you parse this output in scripts or applications, you need to be aware of it.

    JSON Output for Scripts and Automation

    With -format=json you get machine-readable data:


    $ vault kv get -format=json kv/app/db
    {
      "request_id": "7de43863-d7c4-837f-b59c-4b5e546a1d65",
      "lease_id": "",
      "lease_duration": 2764800,
      "renewable": false,
      "data": {
        "api": "myapisecret",
        "pass": "123",
        "user": "admin"
      },
      "warnings": null,
      "mount_type": "kv"
    }
    $ 

    You can combine this with jq:


    $ vault kv get -format=json kv/app/db | jq -r '.data.api'
    myapisecret
    $ 

    Pro tip: You can permanently set the default output format to JSON using the VAULT_FORMAT environment variable:


    export VAULT_FORMAT=json

     

    Retrieving Secret Versions with -version

    The read behavior of get is fairly simple to understand, but only if you know the differences. The following rules apply:

    • A normal get command always returns the latest version.
    • For deleted secrets (KV v2), you receive metadata but no data.
    • Specific versions can be retrieved using --version=X (also only in KV v2).

    Example of retrieving a specific version of a secret:


      $ vault kv get --version=1 kvv2/app/db
      == Secret Path ==
      kvv2/data/app/db
      
      ======= Metadata =======
      Key                Value
      ---                -----
      created_time       2025-07-01T10:52:07.551872783Z
      custom_metadata    <nil>
      deletion_time      n/a
      destroyed          false
      version            1
      
      ==== Data ====
      Key     Value
      ---     -----
      pass    123
      $ vault kv get --version=2 kvv2/app/db
      == Secret Path ==
      kvv2/data/app/db
      
      ======= Metadata =======
      Key                Value
      ---                -----
      created_time       2025-07-01T10:56:04.624145825Z
      custom_metadata    <nil>
      deletion_time      n/a
      destroyed          false
      version            2
      
      ==== Data ====
      Key     Value
      ---     -----
      api     myapisecret
      pass    123
      user    admin
      $ 
      

      I've said it before, but it cannot be emphasized enough: Secret versioning is only available in KV v2. This also means that only the KV v2 Secrets Engine allows targeted access to the history of individual secrets. That can be very important for future auditing, for example to verify whether and how often secrets were actually rotated.

      Targeted Updates and Recovery of Secrets

      patch: Precisely Update the Correct Key

      Earlier in this article, we explicitly pointed out that the put command completely overwrites existing secrets and could potentially delete existing data. Please use patch to specifically update individual values within a secret.

      This does not work in KV v1 due to the lack of metadata:


      $ vault kv patch kv/app/db user=dbadmin
      KV engine mount must be version 2 for patch support
      $

      But it works in KV v2:


      $ vault kv patch kvv2/app/db user=dbadmin
      == Secret Path ==
      kvv2/data/app/db
      
      ======= Metadata =======
      Key                Value
      ---                -----
      created_time       2025-07-01T11:07:50.365256138Z
      custom_metadata    <nil>
      deletion_time      n/a
      destroyed          false
      version            3
      $ 
      

      Result: Only the user key is updated, the rest of the secret remains unchanged.

      rollback: Safely Revert Versions

      Saved a secret incorrectly? Used vault kv put by mistake? No problem in KV v2. If you accidentally overwrote data, you can use rollback in KV v2 to revert to an earlier version:


      $ vault kv get kvv2/app/db
      == Secret Path ==
      kvv2/data/app/db
      
      ======= Metadata =======
      Key                Value
      ---                -----
      created_time       2025-07-01T11:07:50.365256138Z
      custom_metadata    <nil>
      deletion_time      n/a
      destroyed          false
      version            3
      
      ==== Data ====
      Key     Value
      ---     -----
      api     myapisecret
      pass    123
      user    dbadmin
      $
      $ vault kv rollback -version=2 kvv2/app/db
      Key                Value
      ---                -----
      created_time       2025-07-01T11:10:44.341592593Z
      custom_metadata    <nil>
      deletion_time      n/a
      destroyed          false
      version            4
      $ 

      This creates a new version with the content of the specified older version. In our example:

      • Version 2 contains the original content
      • Version 3 was the faulty overwrite attempt
      • Version 4 is the result of the rollback

      Pro tip: It does not roll back the system state to when version 2 was created. Instead, it creates a new version using the contents of version 2. The version history remains intact.

      Deleting Data: delete vs. destroy

      It is essential to remember that the delete command behaves differently in KV v1 and v2.

      KV v1: Hard delete

      In the KV v1 Secrets Engine, delete permanently removes the data:


      $ vault kv delete kv/app/db
      Success! Data deleted (if it existed) at: kv/app/db
      $ 
      $ vault kv get kv/app/db
      No value found at kv/app/db
      $ 

      The data is therefore irreversibly deleted. Recovery is only possible by restoring a Vault snapshot.

      KV v2: Soft delete

      In KV v2, delete performs a soft delete:


      $ vault kv delete kvv2/app/db
      Success! Data deleted (if it existed) at: kvv2/data/app/db
      $
      $ vault kv get kvv2/app/db
      == Secret Path ==
      kvv2/data/app/db
      ======= Metadata =======
      Key Value
      --- -----
      created_time 2025-07-01T11:10:44.341592593Z
      custom_metadata <nil>
      deletion_time 2025-07-01T11:13:33.705332433Z
      destroyed false
      version 4
      $

      The data itself is no longer visible, but it is still present in the system. The metadata, however, remains. Note especially the newly added deletion_time field - it previously had the value n/a.

      Soft-deleted secrets can be reactivated using undelete or permanently deleted with destroy.

      undelete: Reactivating Deleted Versions

      If something was deleted by mistake:


      $ vault kv undelete -versions=4 kvv2/app/db
      Success! Data written to: kvv2/undelete/app/db
      $ 
      $ vault kv get kvv2/app/db
      == Secret Path ==
      kvv2/data/app/db
      
      ======= Metadata =======
      Key                Value
      ---                -----
      created_time       2025-07-01T11:10:44.341592593Z
      custom_metadata    <nil>
      deletion_time      n/a
      destroyed          false
      version            4
      
      ==== Data ====
      Key     Value
      ---     -----
      api     myapisecret
      pass    123
      user    admin
      $ 
      

      The data becomes visible again. The requirement is that destroy was not executed.

      destroy: Permanently Deleting Individual Versions

      Important: destroy is irreversible - undelete and rollback no longer work. Only use this if you are absolutely sure and do not confuse it with delete!

      After this, recovery is no longer possible:


      $ vault kv destroy -versions=2 kvv2/app/db
      Success! Data written to: kvv2/destroy/app/db
      $ 

      You can also remove multiple versions at once:


      $ vault kv destroy -versions=1,3,4 kvv2/app/db
      Success! Data written to: kvv2/destroy/app/db
      $ 

      After a destroy, the destroyed field shows the value true:


      $ vault kv get kvv2/app/db
      == Secret Path ==
      kvv2/data/app/db
      
      ======= Metadata =======
      Key                Value
      ---                -----
      created_time       2025-07-01T11:10:44.341592593Z
      custom_metadata    <nil>
      deletion_time      n/a
      destroyed          true
      version            4
      
      $ 
      

      This field exists primarily for audit purposes. A rollback or undelete is no longer possible, and the field serves as proof that a destroy was executed.

      Practical Tips for Your Daily Work and KV Strategy

      1. Use Version Control

      Regularly check which KV version you are using:


      $ vault secrets list --detailed | grep kv
      kv/           kv           kv_11a18e23           system         system     false             replicated     false        false                      map[]             n/a                                                        4a479e06-6b44-e721-bbff-5ab3df0b7134    n/a        v0.21.0+builtin          n/a               supported
      kvv2/         kv           kv_be4ef9a0           system         system     false             replicated     false        false                      map[version:2]    n/a                                                        390c2b5f-45cd-6774-ae17-3d643d951401    n/a        v0.21.0+builtin          n/a               supported
      $ 
      

      Look for version:2 in the options column (second from the right).

      2. Use Structured Paths

      Develop a consistent naming convention within the path of the secrets engine, for example:


      apps/
        └── payment-service/
            ├── prod/
            │   ├── db-credentials
            │   └── api-keys
            └── dev/
                ├── db-credentials
                └── api-keys

      3. Use patch Instead of put

      Always use patch instead of put for updating individual values to avoid data loss.

      4. JSON Output for Automated Workflows

      In scripts, always use:


      vault kv get -format=json kv/app/db | jq -r '.data.password'

      5. Implement a Rollback Strategy

      Document important versions and keep rollback procedures ready. For example:


      # Save current version
      $ CURRENT_VERSION=$(vault kv get -format=json kvv2/app/db | jq -r '.data.metadata.version')
      $ echo $CURRENT_VERSION
      5
      # In case of issues: rollback $ vault kv rollback --version=$((CURRENT_VERSION-1)) kvv2/app/db

      Avoiding Common Pitfalls

      1. The "write is not merge" Mistake

      The most common issue: accidental overwriting by using put instead of patch.

      2. Path Confusion in KV v2

      Keep in mind:

      • Different paths in KV v2 due to case sensitivity
      • KV v2 automatically adds /data/ in the internal path. The CLI abstracts this, but with direct API calls, you need to account for it.

      3. Confusing delete and destroy

      • delete = soft delete (recoverable) in KV v2, hard delete in KV v1
      • destroy = hard delete (irreversible) in KV v2

      4. No Reset of Version Numbers After delete

      Version numbers in KV v2 are sequential and never reset, even after a delete.

      Conclusion

      The Key/Value Secrets Engine is quick to explain, but full of nuances in practical use. A solid understanding of versioning, the commands, and their side effects is key to stable and secure operation.

      Those who use put with care, maintain secrets with patch, and master rollback in critical moments are in full control of the KV Engine. And anyone who avoids destroy unless absolutely necessary spares themselves a lot of trouble and keeps the path open to recover from a potential disaster.

      Rule of thumb for practice: In KV v2, almost everything is recoverable - except after a destroy.

      Make use of this safety, but still design your processes with foresight.