密钥别再写进配置文件了:一次 macOS 钥匙串集中管理实录

SailTrack
2026-09-17
点 赞
0
热 度
2
评 论
1
  1. 首页
  2. 密钥别再写进配置文件了:一次 macOS 钥匙串集中管理实录

写在前面

整理开发环境的时候,我发现自己的 ~/.zshrc 里躺着好几行这样的东西:

export OPENAI_API_KEY=sk-xxxxxxxx
export HALO_TOKEN=pat_xxxxxxxx
export TAVILY_API_KEY=tvly-xxxxxxxx

平时用着确实方便,echo $OPENAI_API_KEY 随手就能拿到。但每次想备份一下 dotfiles,或者要把终端截图发出去,我都得先瞪大眼睛检查一遍,生怕哪个密钥跟着一起漏出去。

于是我花了个晚上,把这些密钥从明文配置里搬进了 macOS 的钥匙串(Keychain)。这篇记录一下过程,中间还踩了个特别反直觉的坑。

一、明文密钥到底危险在哪

先说清楚问题出在哪。~/.zshrc 就是个纯文本文件,这意味着:

  • 备份即泄露:dotfiles 仓库、Time Machine 备份、同步网盘,全都会把这些密钥一起带上。

  • 截图即泄露:调试时 cat ~/.zshrc,一屏截图发出去就完了。

  • 权限形同虚设:我顺手检查了一下,好几个 .env 文件的权限是 -rw-r--r--,也就是同一台机器上任何用户都能读

看到那个 644 权限的时候我才意识到,"反正是本地文件所以无所谓"这个想法其实站不住脚。

二、思路:钥匙串做真源,环境变量做接口

macOS 自带 security 命令,可以直接读写钥匙串。我的整体思路是分两层:

职责

钥匙串

真正存密钥,加密

环境变量

shell 启动时从钥匙串读出并注入

这样配置文件里就再也不出现密钥了,只留一句 source

读取函数很简单:

_zc_kc_get() {
  local _v
  _v=$(security find-generic-password -s "$1" -a "$2" -w 2>/dev/null) || return 1
  [[ -n "$_v" ]] || return 1
  print -r -- "$_v"
}

再把"环境变量名 → 钥匙串条目"做成一张映射表,启动时循环注入:

typeset -A _ZC_SECRETS=(
  OPENAI_API_KEY  zcode.openai
  HALO_TOKEN      zcode.halo.pat
)

for _name in ${(k)_ZC_SECRETS}; do
  [[ -n "${(P)_name}" ]] && continue          # 已有值就不覆盖
  if _val=$(_zc_kc_get "${_ZC_SECRETS[$_name]}" default); then
    export "$_name=$_val"
  fi
done

~/.zshrc 里只剩下:

[[ -f "$HOME/.secrets/load.zsh" ]] && source "$HOME/.secrets/load.zsh"

三、第一个坑:security -w 会把密钥暴露给 ps

写写入逻辑的时候,我本来打算这么写:

# 错误示范
security add-generic-password -s "zcode.openai" -a default -w "$VALUE" -U

看起来没问题吧?-w 后面跟上密钥值,一把梭。

结果翻了文档才反应过来:-w "$VALUE" 里的值会出现在进程的命令行参数(argv)里。也就是说,在这个瞬间,同机器上的任何进程执行一次 ps 都能读到它。

我做了个对照实验——同一把密钥、同样的调用频率,两种写法各跑一轮,用 ps 高频抓取:

写入方式

密钥出现在 argv 的次数

-w "$VALUE"

24 次

security -i(命令走 stdin)

0 次

差别很明显。

所以正确写法是让命令从标准输入进去,而不是走参数:

# 正确做法
security -i <<EOF
add-generic-password -s zcode.openai -a default -w "$VALUE" -U
EOF

为什么会这样? 因为 -w 的语义就是"把密码当参数传进去",而进程参数是公开的(ps 随时可查);换成 -i 交互模式后,整条命令从 stdin 读入,密钥始终没有落到 argv 上。

一个小细节:值里如果含双引号,得先转义,否则解析会出错。

四、第二个坑:Finder 启动的应用读不到环境变量

集中管理做得很顺,我一度想把这套用到所有配置上。然后就踩了第二个坑。

macOS 的应用从 Finder 或 Dock 启动时,不会继承 shell 的环境变量

这意味着:那些用 ${VAR} 引用环境变量的应用配置,从终端启动它一切正常,从 Finder 双击就挂——因为那一刻变量根本不存在。

所以最后我的方案是分两层:

  • 终端里跑的工具(CLI、脚本):走环境变量注入,完全集中化。

  • GUI 应用的自有配置:密钥只能留在它自己的配置文件里,但把权限收紧到 600,并且不放进任何同步目录。

不能为了"统一"而牺牲可用性,这点想明白了就不纠结了。

五、最终效果

现在 ~/.zshrc 里已经没有任何明文密钥了。验证方式是带超时地读变量长度,而不是打印值:

echo ${#OPENAI_API_KEY}    # 输出长度,不泄露内容

密钥全部躺在钥匙串里,条目名统一带 zcode. 前缀,用一个小脚本管理:

secret put <名字>    # 交互式录入,不回显
secret ls            # 列出所有条目名(不显示值)
secret rm <名字>     # 删除

顺手还写了个审计脚本,扫描常见位置有没有漏网的明文。它的输出只显示前 4 位和长度,这样连审计日志本身都不会泄露密钥。

跑完之后,扫描结果从"发现 4 处明文残留"变成了"未发现明文残留"。

六、小结

几点体会:

  1. "本地文件所以无所谓"是个错觉。 644 权限加上各种备份链路,明文密钥的暴露面比想象中大得多。

  2. 密钥不进 argv 是个很容易被忽略的细节。 security -w 这个坑,不看文档根本想不到。

  3. 统一要服从可用性。 GUI 应用读不到环境变量这件事,让我放弃了"全部集中化"的执念——分层比强行统一更靠谱。

如果你也有一堆密钥躺在 .zshrc 里,不妨也花个晚上收拾一下。搬完之后,至少截图发出去的时候不用先检查一遍了。


让我们忠于理想,让我们面对显示

SailTrack

entp 辩论家

站长

不具版权性
不具时效性

文章内容不具时效性。若文章内容有错误之处,请您批评指正。

目录

欢迎来到SailTrack的站点,为您导航全站动态

42 文章数
11 分类数
5 评论数
53标签数