密码安全实践:从明文存储到凭据隔离

密码安全实践:从明文存储到凭据隔离

一、从一个真实的疏忽说起

前阵子我在整理一个项目的接口说明文档,准备归档时才发现——那份文档里明明白白写着接口的鉴权 Token,是完整可用的原值。

文档本身没问题,写得挺规范:鉴权方式、请求示例、字段说明一应俱全。问题在于,为了让"示例"足够真实,Token 被原样贴了进去。而这份文档当时正躺在交流文件里,随时可能被转发出去。

发现之后当然立刻处理了。但这件事让我意识到一个更普遍的问题:绝大多数人并不是"不在乎安全",而是根本没意识到自己已经把凭据暴露出去了。

贴 Token 的时候想的是"方便对方调用",写密码的时候想的是"先跑通再说"。这些想法本身没错,出问题的是没有人在后面把它们收回来

这篇文章想聊的就是这件事——密码和凭据到底该怎么管,分个人和开发两个场景来说,都是能立刻上手的做法。

二、先分清三个层次

"密码安全"这个词太大了,容易一谈就变成泛泛而谈。实际上它至少分三个层次,每个层次的问题和对策都不一样。

密码安全的三个层次:个人账号密码、开发中的凭据、服务间凭据

层次典型对象核心风险
个人账号密码邮箱、社交、网银、各种网站撞库、钓鱼、复用导致一破全破
开发中的凭据API Key、数据库密码、Token写进代码、提交进版本库、贴进文档
服务间凭据多服务/多账户共享的密钥权限过大、无法追溯、一泄漏影响全局

很多事故的根源,是把这三个层次当成一件事来处理。 比如用管理个人密码的思路去管理服务器密钥——记在一个备忘录里,谁要就发给谁。个人场景这么做最多丢个账号,服务场景这么做可能是整个系统失守。

三、六个最常见的错误

3.1 明文写在文档、代码或聊天记录里

这是最常见也最容易被忽视的。表现包括:写死在代码常量里、贴在接口文档的示例中、留在聊天的历史记录里、存在桌面的 txt 文件里。

共同点是——它们都脱离了你的控制范围。文档会被转发,代码会进版本库,聊天记录会同步到云端,桌面文件会被同步盘带走。

3.2 多系统复用同一个密码

很多人觉得"我密码设得够复杂,复用没关系"。但问题是:你保证自己不被拖库,保证不了每一个你注册过的网站都不被拖库。

任何一个你用过的小网站出事,攻击者就拿这份账号密码去试你的邮箱、网银、社交账号——这就是"撞库"。复用的代价不是多丢一个账号,而是一次全丢。

3.3 弱密码加规律化

用生日、手机号、姓名拼音,或者"某某网站123"这种带规律变体的密码,在自动化工具面前基本等于没有。

还有一个反直觉的点:强制的大小写+数字+符号组合,反而不如一个很长的纯小写短语安全。 复杂度规则会逼着人写出 Abc123! 这种既难记又容易猜的密码。

3.4 提交进了版本库

这是开发场景里最危险的一种。因为版本库有历史记录——你把密钥从代码里删掉,历史提交里还原样留着。

很多人删掉之后以为没事了,实际上只要有人 clone 过,或者仓库曾经公开过,那个密钥就等于已经泄露了。

3.5 权限给得太大

为了方便,经常出现"一个 Token 管所有"、"给个管理员权限省得来回问"的情况。

结果是一旦这把钥匙丢了,丢的不是一个房间,是整栋楼。

3.6 长期不更换

密钥设好之后就再也没动过,几年不换。时间越长,暴露面越大——某次截图、某次日志、某次临时的共享,都可能已经把它带出去了,而你永远不知道。

四、个人层面:三件立刻能做的事

① 装一个密码管理器。

这是投入产出比最高的一件事。装好之后你只需要记住一个主密码,其余全部交给它生成和填写。带来的直接好处是:你终于可以用上几十位随机密码,并且每个网站都不一样。

3.2 和 3.3 两个问题,会因为这一个动作同时消失。

② 关键账号开启两步验证(2FA)。

尤其是邮箱——邮箱是所有账号的"总钥匙",因为几乎所有网站的密码重置都走邮箱。邮箱被攻破,等于其他账号也保不住。

优先开启顺序:邮箱 → 网银支付 → 社交账号 → 云盘。能用认证器 App 或硬件密钥的,不要用短信验证码(短信可被拦截和补卡攻击)。

③ 定期检查密码是否已泄露。

有一些公开的泄露查询服务,输入邮箱就能查这个账号是否出现在已知的数据泄露事件中。查到了就立刻改,并且改掉所有用了同一个密码的地方

五、开发层面:凭据管理的正确姿势

这一节是这篇文章的重点,也是最容易出大事故的地方。

凭据存放的错误做法与正确做法对照

5.1 放哪里:配置文件 + 文件权限

基本做法是把凭据从代码里拿出来,放进独立的配置文件或环境变量文件:

# .env
DB_PASSWORD=xxxxxxxx
API_TOKEN=xxxxxxxx

然后立刻把它加进 .gitignore

# .gitignore
.env
*.key
*credentials*
config.local.*

再加一道保险——把文件权限收紧

chmod 600 .env      # 只有属主可读写
ls -l .env          # 确认权限是 -rw-------

这一步很关键但常被跳过。默认权限是 644(同机其他用户可读),在多用户环境下等于没保护。

5.2 已经提交了怎么办

这是很多人踩过的坑,处理顺序不能错:

  1. 第一件事是立刻轮换密钥,不是删历史。 只要提交过,就要假定它已经泄露。先去服务商后台把密钥作废、重新生成,这一步做完再谈其他
  2. 然后才清理版本库历史——改写历史、强制推送,把那个文件从所有提交中抹掉
  3. 通知协作者重新拉取——历史改写后,其他人的本地仓库会冲突,需要说明清楚
  4. 之后检查有没有被滥用——查服务商后台的调用记录、账单异常

顺序反了是最危险的:先花时间清理历史,密钥还挂在那里可用,攻击者该拿的早就拿到了。

5.3 权限怎么给:最小权限

几条实用原则:

  • 按用途拆——读的密钥不要给写权限,测试环境的密钥绝不用在生产
  • 按服务拆——每个服务一份独立凭据,出问题能定位到具体哪一个
  • 设过期时间——能用临时凭据的就不用永久凭据
  • 留审计记录——谁在什么时候用了它,要能查

5.4 别把凭据贴进任何"给人看"的东西里

回到开头那个故事。接口文档、Readme、演示代码、给同事的说明——这些都会流动出去

写文档时用占位符就好:

X-Api-Token: <你的Token>

需要真的给对方用,就通过单独的、可控的渠道传递,而不是写在一个会被转发的文件里。

六、多服务场景:凭据隔离

这一节是前几年不太会遇到、但现在越来越常见的情况——同时运行着多个助手、机器人或自动化服务

典型的做法是把技能、脚本、配置集中放在一个共享目录里,方便统一管理。这本身没问题,但有一条硬规矩:

含凭据的文件,一律不要放进共享目录。

原因很直白:共享目录里的东西,所有服务都能读到。你放的是自己用的邮箱密码,结果所有实例都拿到了它——包括那些本来不需要、也从来没打算给它这个权限的。

正确的做法是按归属拆开

位置放什么可见范围
共享目录通用能力、无凭据的公共脚本所有服务可见
各自独立目录含凭据的配置、业务专属脚本仅该服务可见

这里的关键认知是:权限隔离应该靠结构来保证,而不是靠自觉。

"大家注意不要读别人的配置"——这种约定在压力下、在赶进度的时候,一定会被打破。但如果物理上就放在不同目录里、权限上就读不到,那就没有打破的可能。

七、一份可执行的检查清单

检查项标准
版本库里有没有凭据搜一遍仓库历史,确认没有密钥、密码、Token
.gitignore 是否覆盖至少包含 .env、*.key、含 credentials 的文件
凭据文件权限-rw-------(600),不是 644
有没有文档里贴了真 Token全部替换为占位符
是否一码多用每个服务、每个环境独立凭据
轮换周期有明确周期,且泄露后立即轮换
关键账号 2FA邮箱、支付、云平台全部开启
个人密码是否复用用密码管理器,全部唯一
泄露应急流程第一动作是轮换,不是删历史

八、小结

密码安全这件事,难点从来不在技术,而在习惯

没有哪种方案能一劳永逸。加密再强的系统,也拦不住把密钥贴进文档里这个动作。反过来,只要守住几条基本纪律,就能挡住绝大多数真实的攻击:

  • 不写死——凭据永远不写进代码、文档和聊天记录
  • 不复用——每个账号、每个服务、每个环境都用独立的
  • 不上库——确认版本库里没有凭据,历史里也没有
  • 不给大——最小权限,按用途拆分
  • 不失察——定期轮换,泄露了立刻换(而不是先删历史)

安全不是一次性的配置,而是一套持续的动作。真正重要的不是"我配得多严密",而是"当有一天我意识到出问题时,我知道第一个动作该做什么"

而对于大多数人来说,只要做到两件事就已经超过了平均水平:用一个密码管理器,把关键账号的两步验证打开。