bigywater.tech
微服务 · 58

敏感配置:加密存储与密钥轮换

数据库密码放在配置中心里安全吗?改了密码旧密码还会留在哪?信封加密到底省了什么?密钥怎么轮换才不会让数据读不出来?密钥给进程用环境变量还是文件?这篇用 Nacos 3.2.4 和几十行 AES-GCM 代码逐项量出来。

6 次
改成新密码、删除配置之后,旧密码标记仍在 Nacos 数据目录(derby-data/log/log1.dat)里出现的次数
104,857,600 → 3,200
100 条 1 MiB 数据轮换主密钥时,单密钥要重新加密的字节数,与每条一个 DEK 只重新包的字节数
400 / 1000
双密钥轮换顺序做错时读不出来的记录数(写入方先切;重新加密之前忘掉旧密钥)

配置中心里的明文

实测 · 本机执行输出
  取回:plain-db → 'password=MARKA_7f3a91';cipher-aes-db → 'password=MARKB_9c1d44'
  发行版 plugins 目录里名字带 encrypt / cipher / kms 的文件:没有
  时间点    | 搜索的标记        | 数据目录(data/)                            | 日志目录(logs/)
  -------+--------------+----------------------------------------+------------
  发布之后   | MARKA_7f3a91 | 2 个文件 / 4 次(如 derby-data/log/log1.dat) | 0 个文件 / 0 次
  发布之后   | MARKB_9c1d44 | 2 个文件 / 4 次(如 derby-data/log/log1.dat) | 0 个文件 / 0 次
  改成新值之后 | MARKA_7f3a91 | 1 个文件 / 6 次(如 derby-data/log/log1.dat) | 0 个文件 / 0 次
  改成新值之后 | NEWA_55e2c8  | 2 个文件 / 3 次(如 derby-data/log/log1.dat) | 0 个文件 / 0 次
  改成新值之后 | MARKB_9c1d44 | 1 个文件 / 6 次(如 derby-data/log/log1.dat) | 0 个文件 / 0 次
  改成新值之后 | NEWB_0b7a3f  | 2 个文件 / 3 次(如 derby-data/log/log1.dat) | 0 个文件 / 0 次
  删除配置之后 | MARKA_7f3a91 | 1 个文件 / 6 次(如 derby-data/log/log1.dat) | 0 个文件 / 0 次
  删除配置之后 | NEWA_55e2c8  | 1 个文件 / 3 次(如 derby-data/log/log1.dat) | 0 个文件 / 0 次
  删除配置之后 | MARKB_9c1d44 | 1 个文件 / 6 次(如 derby-data/log/log1.dat) | 0 个文件 / 0 次
  删除配置之后 | NEWB_0b7a3f  | 1 个文件 / 3 次(如 derby-data/log/log1.dat) | 0 个文件 / 0 次
  改成新值之后,plain-db 的历史:[('U', 'password=MARKA_7f3a91'), ('I', 'password=MARKA_7f3a91')];cipher-aes-db 的历史:[('U', 'password=MARKB_9c1d44'), ('I', 'password=MARKB_9c1d44')]
  删除之后取 plain-db:(HTTP 404)
  搜索的是文件里的原始字节,发布之后约 1 s 就搜了,当时标记已经在 derby-data/log/ 下的文件里;发行版里没有加密插件时,cipher- 前缀的 dataId 和普通 dataId 一样是明文存的

没有加密插件时,密码以明文存在数据目录和历史里;改密码、删配置都不会让旧密码消失。

信封加密与轮换

实测 · 本机执行输出
  数据                | 方案                  | 轮换主密钥要处理的记录数 | 要解密再加密的字节数(DEK 按 32 字节算) | 中位用时(ms)
  ------------------+---------------------+--------------+--------------------------+---------
  (a)10000 条 × 64 B | A 单密钥(全部重新加密)       | 10000        | 640,000                  | 17.5    
  (a)10000 条 × 64 B | B 每条一个 DEK(重新包 DEK) | 10000        | 320,000                  | 17.2    
  (a)10000 条 × 64 B | C 每 20 条共用一个 DEK    | 500          | 16,000                   | 0.84    
  (b)100 条 × 1 MiB  | A 单密钥(全部重新加密)       | 100          | 104,857,600              | 33.7    
  (b)100 条 × 1 MiB  | B 每条一个 DEK(重新包 DEK) | 100          | 3,200                    | 0.2     
  (b)100 条 × 1 MiB  | C 每 20 条共用一个 DEK    | 5            | 160                      | 0.01    
  单密钥轮换要把所有数据读出来再写回去;信封加密轮换主密钥只处理 DEK,与数据本身多大无关。轮换 DEK 本身(也就是换掉保护数据的那把密钥)仍然要重新加密对应的数据

轮换主密钥只需重新包数据密钥,和数据多大无关;轮换数据密钥仍要重新加密数据。

轮换的顺序

实测 · 本机执行输出
  正确顺序:
  阶段                    | K1 记录数 | K2 记录数 | 读取方认识的密钥     | 读不出来的记录数
  ----------------------+--------+--------+--------------+---------
  开始(都是 K1)             | 1000   | 0      | ['K1']       | 0       
  读取方认识 K1 和 K2         | 1000   | 0      | ['K1', 'K2'] | 0       
  写入方改用 K2,又写了 400 条新记录 | 1000   | 400    | ['K1', 'K2'] | 0       
  后台把 K1 记录重新加密成 K2     | 0      | 1400   | ['K1', 'K2'] | 0       
  读取方不再认识 K1            | 0      | 1400   | ['K2']       | 0       
  出错一:写入方先改用 K2:
  阶段                              | K1 记录数 | K2 记录数 | 读取方认识的密钥     | 读不出来的记录数
  --------------------------------+--------+--------+--------------+---------
  开始(都是 K1)                       | 1000   | 0      | ['K1']       | 0       
  写入方先改用 K2(读取方还只认识 K1),又写了 400 条 | 1000   | 400    | ['K1']       | 400     
  读取方后来也认识了 K2                    | 1000   | 400    | ['K1', 'K2'] | 0       
  出错二:重新加密之前忘掉 K1:
  阶段                | K1 记录数 | K2 记录数 | 读取方认识的密钥 | 读不出来的记录数
  ------------------+--------+--------+----------+---------
  开始(都是 K1)         | 1000   | 0      | ['K1']   | 0       
  还没重新加密,读取方就忘掉了 K1 | 1000   | 400    | ['K2']   | 1000    

先让读取方认识新密钥,再切写入,再重新加密旧数据,最后才让读取方忘掉旧密钥。

密钥怎么交给进程

实测 · 本机执行输出
  标记(交给进程的方式)       | ps 的输出         | 结果 
  ------------------+----------------+----
  ARGSECRET_a93f10  | ps -o command= | 能搜到
  ENVSECRET_6be2a8  | ps -o command= | 搜不到
  FILESECRET_d41c7e | ps -o command= | 搜不到
  ARGSECRET_a93f10  | ps eww         | 能搜到
  ENVSECRET_6be2a8  | ps eww         | 能搜到
  FILESECRET_d41c7e | ps eww         | 搜不到
  文件权限:0o600。命令行参数用普通的 ps 就能看到,环境变量在 ps eww 里能看到(这里都是同一个用户,别的用户能不能看到我没有测);文件里的内容 ps 看不到,由文件权限保护
ERRORS {}

命令行参数和环境变量在 ps 里能看到,文件靠权限保护。

分步演示

敏感配置
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Go / nginx / Envoy / etcd / Consul / Nacos / OpenTelemetry / Prometheus(按各篇注明用到哪些及版本);所有服务都是本机自己起的进程,端口 9150 以上,只监听 127.0.0.1,不连任何外部服务,不碰本机已有的实例;故障用 kill -9 进程、让连接断开或在代码里注入延迟与错误来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。Nacos 3.2.4(单机,Java 17,没有加密插件);信封加密是我用 Python cryptography 50.0.2 的 AES-256-GCM 写的几十行,不是任何 KMS 或 Vault 的实现;用时是 3 次取中位数,含 Python 循环开销;双密钥轮换是内存里的模拟(真实的 AES-GCM 加解密);ps 的实验在 macOS、同一用户下做
    • 没有验证:Nacos 的 cipher- 前缀与 KMS 加密插件(我印象里 cipher- 前缀会触发加密插件,这个发行版里没有插件,没有测加了插件之后的行为);Vault / 云 KMS / sops 等真实的密钥管理;K8s Secret 的存储与加密(见 K8s 系列);别的用户能不能用 ps 看到环境变量或命令行参数;Derby 日志文件什么时候被清理;磁盘加密与备份里的残留;密钥泄漏后的撤销流程
    京ICP备2026067502号-1京公网安备11010502063487号