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

      HashiCorp Vault 深入解析 – 第 2b 部分:Key/Value Secrets Engine 的实际操作

      Key/Value Secrets Engine 是几乎所有 Vault 实施中的核心组件。它为静态 secrets 的安全存储提供基础,并且在实际应用中,其使用频率远高于许多动态引擎。

      在第 2a 部分的理论介绍之后,本文将聚焦于 KV Engine 的实际操作。我们将演示如何写入、读取、更新和删除 secrets,并结合实际案例分析 KV 版本 1 与版本 2 之间的差异。本文重点在于与生产环境相关的命令、常见陷阱以及日常运维中的具体建议,因此我将这些知识以教程与速查表相结合的形式呈现给您。

      vault kv 命令:您日常工作的工具箱

      与 KV Engine 的交互是通过 vault kv 命令完成的。该命令在内部对不同版本进行了抽象,但在输出结果中会清楚地显示使用的是 v1 还是 v2 引擎。基本的子命令可以分为两个类别:

      基本操作(适用于 KV v1 和 v2):

      • put:写入或更新 secrets
      • get:读取 secrets
      • delete:删除 secrets(不一定是永久删除)
      • list:列出所有可用路径

      扩展操作(仅适用于 KV v2):

      • patch:更新单个键而不覆盖其他键
      • rollback:回滚到特定版本
      • undelete:恢复已删除的版本
      • destroy:永久销毁指定版本

      这些扩展命令仅在 KV v2 中可用,因为它们依赖于内部存储的元数据。而旧的 v1 版本尚不支持元数据功能。

      使用 put 写入 Secrets:简单但暗藏陷阱

      vault kv put 写入命令是向 KV Engine 写入 secrets 的核心方法:


      vault kv put <路径> <键>=<值> [<键>=<值>...]

      一个简单示例,我们首先启用 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/

      接下来的命令将在路径 kv/app/db 下存储一个 key 为 pass、值为 123 的 secret。根据所用版本的不同,输出结果也有所差异:

      KV 版本 1:


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

      KV 版本 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
      $ 

      版本 2 会返回元数据,例如创建时间和当前版本号。

      重要提示:在 KV v2 中,数据是存储在一个内部路径下,并带有额外前缀 /data/。这在使用 API 进行操作时尤其需要注意,因为这意味着您无法简单地从 v1 迁移到 v2,而不对脚本和应用程序进行必要的调整!

      同时写入多个 Key/Value 对

      单个 put 命令可以写入任意数量的键:


      $ 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]$ 

      在实际操作中,这种方式可以减少命令数量,但请不要忘记:路径下的所有内容都会被替换。为避免出错并提高脚本的可读性,在不确定的情况下,建议分步骤操作 - 或者使用下一节介绍的文件方式。

      从 JSON 文件导入 Secrets

      尤其在自动化环境或复杂的 secret 结构中,推荐使用文件导入多个 Key/Value 对:


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

      secrets.json 文件示例内容:


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

      put 与 patch 的重要区别

      许多 Vault 用户在早期都会犯一个严重错误:他们使用 put 来更新单个值。

      这在 KV v1 和 v2 中都会导致已存在的数据被删除,只保留 put 命令中提供的字段。

      示例:


      # 初始状态
      $ vault kv put kv/app/db pass=123 user=admin api=myoldapisecret
      Success! Data written to: kv/app/db
      $
      # 更新 API 密钥(错误地使用了 "put") $ vault kv put kv/app/db pass=123 user=admin api=myoldapisecret
      Success! Data written to: kv/app/db
      $
      # 结果:只剩下 "api" $ vault kv get kv/app/db
      === Data ===
      Key Value
      --- -----
      api mynewapisecret
      $


      userpass 两个键因此被删除。

      在希望保留现有 userpass 键的场景下,vault kv put 显然不是正确的命令。只有 patch 命令可以避免删除未变更的键。我们将在下文进一步介绍 patch。现在请记住,put 是具有破坏性的:

      重要 vault kv put 命令始终会替换指定路径下的全部数据。它不是合并操作!

      使用 get 读取 Secrets:窥视存储内容

      通过 vault kv get 命令可以读取 secrets。默认情况下总是显示最新版本。

      例如,以下命令:


      vault kv get kv/app/db

      对于 KV v1:


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

      对于 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
      $

      提示:在 KV v2 中,路径中同样会出现 /data/。如果您打算在脚本或应用中解析该输出,请务必考虑到这一点。

      用于脚本和自动化的 JSON 输出

      使用 -format=json 可以获取机器可读的数据格式:


      $ 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"
      }
      $ 
      

      您可以将其与 jq 工具结合使用:


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

      专业提示:可以通过设置环境变量 VAULT_FORMAT 将默认输出格式永久设置为 JSON:


      export VAULT_FORMAT=json

       

      使用 -version 读取指定版本的 Secrets

      get 的读取行为相对简单,但前提是理解其版本处理机制。规则如下:

      • 普通的 get 命令总是返回最新版本
      • 对于被删除的 secrets(KV v2),您只能获取元数据,无法获取数据内容。
      • KV v2 支持通过 --version=X 参数获取指定版本的 secrets。

      示例:读取某个 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
        $ 

        我已经多次强调,但这个事实再怎么强调也不为过:Secrets 的版本控制仅在 KV v2 中可用。 这也意味着,只有 KV v2 Secrets Engine 才支持查看单个 Secret 的历史记录。这在后续审计中可能非常关键,例如用于确认 secrets 是否确实进行了轮换,以及轮换的频率。

        Secrets 的精确更新与恢复

        patch:精确更新指定键

        本文前文已明确指出,put 命令会完全覆盖已有 secrets,并可能导致原有数据被删除。请务必使用 patch 来单独修改某个键的值,而不会影响其他数据。

        由于 KV v1 缺乏元数据支持,因此无法使用 patch:


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

        而 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
        $ 

        结果:仅 user 键被更新,其他 secret 数据保持不变。

        rollback:安全地回滚版本

        不小心保存了错误的 secret?误用了 vault kv put?在 KV v2 中没关系。如果您误操作了数据,可以使用 rollback 回滚至旧版本:


        $ 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
        $ 

        此操作会根据指定旧版本的数据创建一个新版本。在本示例中:

        • 版本 2 包含最初的数据内容
        • 版本 3 是错误的覆盖操作
        • 版本 4 是根据版本 2 的内容生成的回滚版本

        专业提示:这并不会将 Secret 回滚至版本 2 创建时的系统状态,而是基于版本 2 的数据生成一个新版本。历史记录将完整保留。

        删除数据:deletedestroy

        请务必牢记:delete 命令在 KV v1 和 v2 中的行为截然不同。

        KV v1:彻底的 delete

        在使用 KV v1 Secrets Engine 的情况下,delete 命令会永久删除数据:


        $ 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
        $

        数据已被彻底删除,无法恢复。若需恢复,只能通过恢复 Vault 快照来实现。

        KV v2:软 delete

        在 KV v2 中,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
        $

        虽然数据本身不再可见,但仍然保存在系统中。元数据依然存在,特别注意新增的 deletion_time 字段,以前该字段为 n/a

        已软删除的 Secrets 可通过 undelete 恢复,或通过 destroy 永久删除。

        undelete:恢复已删除的版本

        如果误删了某个 Secret:


        $ 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
        $ 

        数据再次变得可见,前提是尚未执行过 destroy

        destroy:永久删除指定版本

        重要提示: destroy 是不可逆操作 - undeleterollback 都无法再使用。因此仅在完全确认的情况下执行,并且绝不可与 delete 混淆

        一旦执行 destroy,将无法恢复数据:


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

        您也可以同时销毁多个版本:


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

        执行 destroy 后,destroyed 字段将显示为 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
        
        $ 

        该字段主要用于审计目的。此时无法再执行 rollbackundelete,它是 destroy 操作已执行的明确标志。

        日常实践建议与 KV 策略

        1. 使用版本控制

        定期检查您所使用的 KV 版本:


        $ 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
        $ 
        

        在“Options”列(倒数第二列)中查找 version:2

        2. 使用结构化路径

        在 Secret Engine 路径中制定一致的命名规范,例如:


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

        3. 始终使用 patch 替代 put

        当需要更新单个键值时,请始终使用 patch 而非 put,以避免不必要的数据丢失。

        4. 在自动化流程中使用 JSON 输出

        在脚本中统一采用如下方式:


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

        5. 制定回滚策略

        记录关键版本并准备好回滚流程。例如:


        # 记录当前版本
        $ CURRENT_VERSION=$(vault kv get -format=json kvv2/app/db | jq -r '.data.metadata.version')
        $ echo $CURRENT_VERSION
        5
        
        
        # 若出现问题:执行回滚
        $ vault kv rollback --version=$((CURRENT_VERSION-1)) kvv2/app/db

        避免常见错误来源

        1. “write is not merge” 错误

        最常见的问题:不小心使用 put 覆盖数据,而不是使用 patch

        2. KV v2 中的路径混淆

        请时刻牢记:

        • KV v2 中路径区分大小写,容易造成混淆
        • KV v2 在内部路径中自动插入 /data/。虽然 CLI 会做抽象处理,但在直接调用 API 时必须自行处理该前缀。

        3. 混淆 delete 与 destroy

        • delete = KV v2 中的软删除(可恢复),KV v1 中为硬删除
        • destroy = KV v2 中的硬删除(不可恢复)

        4. delete 后版本号不会重置

        KV v2 中的版本号是递增的,即使执行了 delete 也不会重置或回滚。

        结语

        Key/Value Secrets Engine 原理简洁,但在实际使用中充满细节。对版本机制、各类命令及其副作用的深刻理解,是实现稳定安全运行的关键。

        在实际操作中,谁能谨慎使用 put,精确使用 patch 进行维护,并在必要时掌握 rollback,谁就能真正驾驭 KV Engine 这项工具。而能在非必要时避免使用 destroy 的人,将在误操作发生时保留恢复的余地,避免陷入万劫不复之境。

        实践口诀:在 KV v2 中几乎所有内容都可以恢复 - 除了执行了 destroy 之后。

        充分利用这一安全机制,同时也应在流程设计上做到未雨绸缪。