最近做安全基线整改我把公司内部用 Nacos 做配置中心的应用全部过了一遍发现一个被忽视很久的问题数据库密码几乎是明文躺在 Nacos 的 YAML 配置里。跟几个同事聊大家的反应都是“反正内网、还有账号鉴权”可真等做完了“基于凭据管理系统实现 Nacos 服务端配置中数据库密码加密”的改造再回头看这个说法站不住脚。这篇文章把这次改造的来龙去脉、方案取舍、落地细节和踩过的坑完整写一遍给你一个可参考的实践样本。这套方案做下来核心效果有两点第一Nacos 控制台、配置导出、客户端拉取的配置内容里数据库密码不再是明文而是类似ENC(prod-order-db-pwd-001)的引用串第二真实密码全部收归到凭据管理系统由它统一做权限控制、解密审计和密钥轮换。也就是说就算有人拿到了 Nacos 的配置也只是拿到一个“凭据编号”拿不到能连数据库的东西。文章偏实战面向正在做配置中心安全治理、或者打算给 Spring Boot 应用加配置加密能力的同学。1. 项目背景与改造动机为什么不能再让明文密码躺在 Nacos 里1.1 明文密码在配置中心的三类典型风险第一类风险是配置数据被越权查看或导出。Nacos 的控制台虽然做了登录鉴权但实际使用中一个配置往往会被多个角色共享后端开发要看、测试要连库、运维排查要验证配置、数据同学可能要核对连接信息。人一多能访问配置中心的人就远远超出实际需要。只要其中有一个人账号被攻破或者内部有人把配置复制出去数据库的账号密码就等于直接暴露。第二类风险是配置内容很容易被日志、监控和排查过程“带出来”。Spring Boot 启动时会把数据源相关信息打印到控制台如果日志级别调到 DEBUGJdbcUrl、username、部分密码信息很可能跟着出现。另外数据库连接报错时驱动会输出包含连接串的异常信息万一连接串里就带着密码日志系统一收集密码又多了几个存放副本。明文密码的副本越多泄露面就越大。第三类风险是密码复用导致的横向扩散。我看过太多项目的数据库账号是“一处创建、多处复用”同一个root或同一个业务账号被三四个服务共用密码还一样。只要其中一个服务的配置泄露攻击者拿到的就是多套系统的数据库入口。就算每套系统分开账号如果 Nacos 配置里都明文写着同样的密码结果也是一样的。1.2 传统防护手段为什么不够用有人会说Nacos 不是有鉴权、有命名空间隔离吗确实有但鉴权只能解决“谁能不能进这扇门”的问题解决不了“进来的人能不能看到密码”的问题。通常一个团队的成员都有权查看该团队服务的配置这时候明文密码等于向团队内所有人默认开放。更麻烦的是Nacos 默认的账号管理体系在多人共用账号时连“到底谁看了配置”都查不出来。还有人会想到把密码放到环境变量里Nacos 里不写。这个思路部分有效但引入的新问题也不少环境变量在容器、虚拟机、本地开发环境之间容易不一致密码散落在各个部署平台的配置里没有统一变更入口轮换一次密码需要同时改很多地方漏一个就出事。最关键的是环境变量里的敏感信息同样缺少审计和权限控制本质上只是把明文搬了个位置。所以这次改造的目标就很清楚了让密码以密文或凭据引用的形式出现在 Nacos 配置中真实密码集中存放在凭据管理系统里应用启动时才向凭据系统申请解密。整个过程有权限校验、有调用审计、支持密码轮换才算真正把风险收敛住。2. 整体方案设计凭据管理系统接管密码Nacos 只存“暗号”2.1 三种加密改造思路的对比动手之前我先把市场上常见的三种方案摆在一起做了个对比避免一上来就钻代码。方案实现思路优点缺点A. 环境变量直接注入Nacos 只写${DB_PASSWORD}密码放到部署平台的环境变量里改造量小不下发到配置中心密码分散在各环境无统一审计多机配置易漂移B. Jasypt 加密 密钥放环境变量Nacos 存ENC(密文)应用启动时用环境变量里的密钥解密社区方案成熟配置改造清晰密钥仍散落在环境变量等于把明文换成密钥密码泄露风险转化为密钥泄露风险C. Jasypt 凭据管理系统托管密钥/密码Nacos 存ENC(凭据ID)真正的密码或密钥保存在凭据管理系统应用启动时调用凭据系统解密权限、审计、轮换统一收口Nacos 中无任何明文和密钥需要额外依赖凭据管理系统有集成成本我最终选了方案 C。核心判断是方案 B 虽然做到了“配置里没明文”但密钥还是躺在环境变量里一旦密钥泄露数据照旧加密等于没加而且密钥怎么轮换、谁看了密钥都难以追踪。方案 C 的价值在于把“密文存储”和“密钥管理”拆开了——密文放在 Nacos密钥/真实密码放在凭据管理系统中间加一层应用身份鉴权安全性和可运维性都上一个台阶。2.2 推荐架构与核心链路改造后的核心组件有四个Nacos 配置中心、Spring Boot 应用、凭据管理系统、目标数据库。整个启动链路大概是这样的Spring Boot 应用启动Nacos 客户端先从配置中心拉取对应dataId的配置Spring 环境变量解析过程中Jasypt 识别出形如ENC(prod-order-db-pwd-001)的值Jasypt 调用自定义的StringEncryptor实现类自定义实现类向凭据管理系统发起解密请求请求携带应用身份标识和凭据 ID凭据系统校验权限把真实数据库密码返回给应用解密后的密码注入数据源配置应用正常连库。这里面 Jasypt 起的是“解密编排”作用它本身不保管任何密钥只是负责在 Spring 解析配置属性时把ENC(...)标记的值转交给我们指定的解密器。真正的安全底座是凭据管理系统。用一个生活类比Nacos 配置相当于办公室文件柜里的一张便签上面写着“银行保险柜 3 号”而凭据管理系统就是那家银行取东西时要出示身份证、留下记录而不是任何人都能从便签上直接看到保险柜密码。顺手说一句这套链路不只适用于数据库密码。Redis 密码、消息队列密码、第三方 API 密钥凡是会被写进 Nacos 的敏感字符串都可以用相同的ENC(凭据ID)方式接管。改造一次后面要扩到别的敏感项就只是加配置、加凭据项的事。2.3 安全模型设计权限、校验和审计缺一不可很多人以为把明文换成密文就完事了其实安全模型设计才是关键。我这次在凭据管理系统侧做了三层约束缺一层都可能被绕过。最小权限原则。每个应用在凭据管理系统中注册独立的身份只能申请自己命名空间下的凭据。比如订单服务只能解密prod-order-*开头的凭据不能碰用户服务的数据库密码。这个看起来理所当然但在实际操作中发现相当多系统是“一个密钥到处用”如果不先做账号和命名空间规划后面的权限控制根本无从谈起。双重身份校验。应用调用凭据系统解密接口时不能只凭一个凭据 ID 就放行。我要求同时校验应用身份AppId 签名、来源 IP 网段、请求时间戳签名用非对称方式生成防止请求被伪造或重放。这样即使攻击者拿到 Nacos 里的ENC(prod-order-db-pwd-001)他也无法通过凭据系统的校验等于拿着一张没带身份证的便签去银行取保险柜。调用审计留痕。凭据系统的每次解密请求都记录操作者身份、请求时间、目标凭据、应用来源。出问题的时候能从审计日志反查出“谁在什么时候解密过什么”。这一步不能省后面安全团队要求提供数据访问溯源时它就是唯一的依据。3. 落地实现完整步骤Spring Boot 结合凭据管理系统的前后端改造3.1 Nacos 配置项改造把明文密码替换为凭据引用先说改造前 Nacos 里数据库配置长什么样通常是这样一段 YAMLspring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://10.20.3.9:3306/order_db?useSSLfalsecharacterEncodingutf8 username: order_rw password: OrderDb2024!这段配置里password就是数据库的真实密码任何一个能查看该配置的人都能直接连库。改造后我把它改成下面这样spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://10.20.3.9:3306/order_db?useSSLfalsecharacterEncodingutf8 username: order_rw password: ENC(prod-order-db-pwd-001)关键变化是password字段从明文变成ENC(prod-order-db-pwd-001)。这里ENC()是 Jasypt 默认的加密前缀和后缀括号里放的是一个凭据 ID不是密码本身。凭据 ID 的命名我建议按环境-应用-用途-序号来组织比如prod-order-db-pwd-001一目了然。注意凭据 ID 本身只包含小写字母、数字和短横线不要加入空格或特殊字符否则 YAML 解析和后续正则匹配都会踩坑。有人会问用户名username要不要也加密我的做法是优先加密密码用户名可以后续再纳入凭据管理。因为用户名相对不算高敏感但也不是不能加密同理会变成ENC(prod-order-db-user-001)。看你团队的安全要求如果要求高把用户名也放进凭据系统改动上没有任何额外成本。3.2 引入 Jasypt 并自定义 StringEncryptor 对接凭据管理系统配置项改造完成后下一步是让应用能够识别ENC(...)并完成解密。这里我用的还是 Jasypt 这套成熟方案但它不再持有什么机密只是一个“解密分发器”。首先是依赖引入在 Spring Boot 应用的pom.xml里增加dependency groupIdcom.github.ulisesbocchio/groupId artifactIdjasypt-spring-boot-starter/artifactId /dependency版本号我一般不写死由项目 parent 统一管理避免和 Spring Boot 版本冲突。如果你的项目用的是 Spring Boot 3.x需要确认一下对应 starter 的版本兼容性遇到问题优先升级到 3.x 兼容版本。接下来在启动类上加上注解让 Jasypt 接管所有配置属性的解密EnableEncryptableProperties SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }然后是核心部分自定义一个StringEncryptor实现类把它声明为 Spring 容器中的一个 Bean并告诉 Jasypt 使用这个 Bean。示例代码如下Component(credentialEncryptor) public class CredentialStringEncryptor implements StringEncryptor { private static final Pattern ENC_PATTERN Pattern.compile(^ENC\\((.?)\\)$); Resource private CredentialService credentialService; Override public String encrypt(String plaintext) { // 加密操作一般在管理端或发版工具中执行运行时很少调用 String credentialId credentialService.registerSecret(plaintext); return ENC( credentialId ); } Override public String decrypt(String encryptedValue) { Matcher matcher ENC_PATTERN.matcher(encryptedValue); if (!matcher.matches()) { throw new IllegalArgumentException(unsupported encrypted format: encryptedValue); } String credentialId matcher.group(1); return credentialService.fetchSecret(credentialId); } }这段代码里encrypt方法用于把明文密码注册到凭据管理系统并返回凭据 ID。但在生产环境我一般不会让应用在运行时调用encrypt而是由管理端工具或 CI/CD 流程来做避免把“写入密码”的能力暴露在业务进程里。decrypt方法则是真正的运行时路径解析出ENC()括号里的凭据 ID然后调用凭据管理系统的 SDK 或 HTTP 接口拿到真实密码。最后在application.yml里指定 Jasypt 使用这个自定义解密器jasypt: encryptor: bean: credentialEncryptor property: prefix: ENC( suffix: )这里prefix和suffix其实可以省略因为ENC()就是 Jasypt 的默认配置写出来是为了让团队同事在阅读代码时知道这套自定义标记是哪些。bean字段是关键它告诉 Jasypt 去容器里找名为credentialEncryptor的StringEncryptor实现。3.3 本地开发环境的兜底策略与强制校验开关改造过程中第一个让我头疼的问题就是本地开发环境没有凭据管理系统可用。总不能让每个开发在自己电脑上再搭一套凭据系统那成本太高了。我的处理方式是在配置文件里增加一个开关区分“强制凭据解密”和“本地兜底”两种模式。生产、预发环境的配置强制走凭据解密credential: decryption-enabled: true本地开发环境的配置则允许兜底直接解密失败时回退到开发库密码spring: config: activate: on-profile: dev credential: decryption-enabled: false在CredentialStringEncryptor.decrypt方法里加一段判断如果decryption-enabled为 false直接返回本地配置的真实密码如果为 true则必须从凭据系统获取获取失败就抛出异常、终止启动不允许带病运行。这个“fail fast”设计很重要避免生产环境凭据系统故障时应用带着错误的连接偷偷跑起来。再补充一个安全细节点生产环境的兜底开关必须彻底关闭不能留任何“本地密码兼容”。我见过有团队为了省事在生产配置里也保留了一段可用的明文密码结果 Nacos 一拉配置就等于明文还是公开了改造等于没做。建议在发布前用 CI 脚本扫描配置文件直接禁止生产环境配置中出现decryption-enabled: false或者常见密码字段。3.4 配置热更新后的密码刷新处理Nacos 支持配置动态刷新这是很多人引以为傲的功能。但在数据库密码加密这个场景下动态刷新并没有想象中那么简单。问题在于即使 Nacos 里password字段从ENC(credA)改成ENC(credB)Jasypt 也会在新配置加载时再次解密但数据源连接池里的连接还是基于旧密码建立的HikariCP 不会因为你改了配置属性就自动重建连接池。调研后我给了团队两套方案按变更频率选低频变更改密后重启应用。比如安全团队要求季度轮换直接滚动重启一批实例就行几十秒到几分钟的停机窗口在大部分内部系统可接受。高频轮换上连接池重建机制。在应用里监听 Nacos 配置刷新事件检测到数据源相关配置变化时主动关闭现有连接池再重新创建。主要逻辑是在刷新监听器里调用数据源的close()然后重新构建DataSourceBean。注意这一步要处理并发请求关闭和重建之间会有短暂的连接中断尽量选业务低峰期执行。我个人更推荐先用“低频重启”跑通全流程等团队对这套机制有信心了再去做无重启轮换。一上来就搞连接池热更新容易被各种边界问题缠住。4. 踩坑实录启动失败、热更新失效与权限问题的排查技巧4.1 我实际遇到过的几个典型问题第一个典型问题是服务启动直接报解密失败。第一次改造测试时某个应用启动日志里出现Decryption failed一开始怀疑是 Jasypt 配置问题查了一圈发现是凭据管理系统的网络策略没放行应用所在网段访问不了凭据系统 API。这个最容易踩因为本地验证时网络是通的一上测试环境就被防火墙拦了。排查顺序建议是先确认网络连通性再确认应用身份权限最后看凭据 ID 是否真的存在于目标命名空间。第二个问题是 Nacos 控制台刷新配置后应用日志显示密码确实变了但数据库连接仍然失败。这就是上面说的连接池重建问题。当时我查看连接池状态发现 HikariCP 里还是旧连接配置属性变了不等于底层连接会销毁重建。后来加了配置刷新监听器在检测到spring.datasource.password变化时主动关闭连接池才算解决。第三个问题比较隐蔽密文被日志打印出来了。有一次排查问题时同事把配置对象的toString()打到了日志里结果包含ENC(prod-order-db-pwd-001)的整段配置进了日志系统。虽然这不算泄露真实密码但凭据 ID 是绕过权限校验的第一步线索不能放任不管。后来我在日志过滤器里加了脱敏规则凡是匹配ENC(...)的内容统一替换成ENC(***)。还有一个版本兼容问题。Spring Boot 2.x 时代用jasypt-spring-boot-starter很顺但到了 Spring Boot 3.xJasypt 内部的字节码处理方式变了旧版本会出现反射相关异常。遇到这类问题不要死磕直接升级 starter 到与 Spring Boot 3 兼容的版本基本都能解决。4.2 问题速查表与调试建议我把这次改造过程中高频遇到的问题整理成一张速查表方便后续团队排查现象常见原因排查命令/手段解决思路启动报 Decryption failed网络不通、权限不足、凭据 ID 不存在查看异常码和日志中的 credentialId按 网络→权限→凭据ID 顺序确认控制台看到明文密码实例还在旧版本未走解密链路检查实例代码版本和 Jasypt 日志分批重启实例确保全部走新配置配置热更新后连不上新库连接池没有重建查看连接池 active 连接情况加 Nacos 刷新监听器主动重建 DataSource日志打印出 ENC(...)配置对象被直接序列化输出搜索日志中的 ENC 关键字增加日志脱敏过滤规则Spring Boot 3 启动异常starter 版本不兼容查看反射相关异常栈升级 jasypt starter 版本调试手段方面推荐三个快速验证方法。第一用 curl 直接拉一次 Nacos 配置确认远端配置里已经是ENC(...)而不是明文curl 的接口路径以你们 Nacos 版本为准重点是拉回来的response body里看password字段。第二把应用日志级别调整到 DEBUG观察 Jasypt 是否真正接管了ENC()标记的配置如果日志里出现Decrypting property相关记录基本可以断定解密链路已生效。第三直接调用凭据管理系统的查询接口用 Postman 或 curl 输入一个测试凭据 ID确认凭据系统本身没问题这样能把“应用问题”和“凭据系统问题”快速分开。5. 扩展实践密钥轮换、审计告警与落地推广的几点经验5.1 数据库密码轮换如何做到“无感”密码轮换是所有安全改造里最考验设计的一环。传统做法是改数据库密码再改 Nacos 配置再重启服务麻烦且容易在某一台机器上漏改。用凭据管理系统之后我设计了一套更顺滑的流程。在凭据管理系统中给同一份密码增加版本比如prod-order-db-pwd-001下同时存在v1和v2两个值。先让应用继续使用v1然后把数据库侧密码改为v2接着更新凭据系统里prod-order-db-pwd-001的生效版本。应用在下一次获取凭据时自然拿到v2。因为 Nacos 配置里写死的还是ENC(prod-order-db-pwd-001)这个凭据 ID 不变所以不用改 Nacos也不用重启只要连接池能按上一节说的方式重建轮换过程对用户基本无感。如果凭据系统不支持版本化也可以走“双写双读”的方式在 Nacos 配置中临时增加一个spring.datasource.secondary-password应用同时支持新旧两套密码连接数据库等所有连接都切到新密码后再移除旧密码。这个方案没有版本化那么优雅但兼容旧凭据系统。5.2 审计与告警解密行为要可追溯、可预警密码加密改造做完之后我把审计和告警当作“验收标准”。光把明文藏起来不叫安全得让人知道谁在解密、什么时候解密、解了多少次才叫闭环。我这里落地了三类告警规则解密频率突增告警正常情况下一个应用启动时解密几次运行期几乎不调用如果某个应用在短时间内频繁调用解密接口大概率是代码 bug 或账户被滥用。解密失败次数超阈值告警连续失败超过 5 次就触发告警说明凭据系统或应用身份可能出现异常。非工作时间大规模解密告警这个可以根据团队实际情况调整主要是防数据被批量拖走。审计日志的留存周期建议至少一年。配合 Nacos 控制台的访问日志安全团队可以完整还原一条链路谁看到了配置、谁触发了解密、解密出来的密码在哪个时刻被哪个 IP 使用。这在内部审计和外部合规检查时非常有说服力。5.3 最后一公里的落地推广建议这次改造踩完坑我个人最大的体会是技术选型反而简单真正难的是让团队愿意用、持续用。有几个经验值得分享。第一先用一个系统跑通全流程再铺开。不要一开始就追求把所有应用全部改造完。选一个配置简单、影响面小的服务作为试点把从 Nacos 改造、凭据系统注册、发布验证到轮换演练的全流程走一遍把文档沉淀下来。后面推广就是复制粘贴的事。第二尽量把加密能力封装成公共 Starter。如果团队里几十个 Spring Boot 应用每个服务都自己实现一套StringEncryptor维护成本很高也容易出现配置不一致的问题。我后来把CredentialStringEncryptor、凭据系统客户端、日志脱敏规则全部打成了一个公共组件业务方只需要引入依赖、配置两三个字段就能接入。第三不要在项目初期就把权限、审计、告警全部铺开那样步子太大容易顾此失彼。先把“Nacos 中无明文”这个核心目标落地再逐步完善权限模型和告警体系。安全建设不是一次性的它更像是补丁迭代先解决最大风险再处理次要风险。最后再提醒一件事数据源密码改造只是配置安全的一部分回头看看你的 Nacos 里还有没有别的明文敏感项比如密钥、Token、回调地址的鉴权信息。加密这条路一旦走通把其他敏感项纳入进来就只是“加一个凭据、改一行配置”的事了。希望这份实践方案能帮你少走几步弯路。