🎧 Listen to this article: हिंदी · English · தமிழ் · తెలుగు · ಕನ್ನಡ · മലയാളം · ଓଡ଼ିଆ · 日本語 · 中文
🌍 Read this in your language: हिंदी · தமிழ் · తెలుగు · ಕನ್ನಡ · മലയാളം · ଓଡ଼ିଆ · 日本語 · 中文
大多数凭据泄露并不引人注目。没有黑客,也没有违规报告。有人仅仅是将一个全账户 API 密钥放在其主目录中的一个明文文件里,有一天,一次备份、一次屏幕共享、一次粘贴的命令或一次 AI 编程会话,就会悄悄地将其带到它永远不应该去的地方。
截至 2026 年 8 月,最后一个途径已经改变了安全计算的规则。现在几乎每个开发者都会运行一个代表他们读取文件和运行命令的 AI 编程助手。这种工具读取的任何内容,以及命令打印的任何内容,都会成为会话记录的一部分,在每一次交互中都会传输给模型提供商。如果一个明文密钥在会话期间被显示、被解密到屏幕上或被回显,它现在就存在于那个记录中,脱离了你的控制。这并非假设:这正是一个极其谨慎的工程师在没有做任何明显错事的情况下,泄露密钥的方式。
本文是用于修复该问题的完整、通用的运行手册。此处的实际示例是 Cloudflare 全局 API 密钥,但该技术适用于任何高权限凭据。第一部分不使用任何其他东西,除了 gpg 和 bash ,并且适用于任何 Linux 或 macOS 机器。第二部分仅适用于使用 AI 编程助手并希望在顶层有一个硬性、强制性保护机制的人。
为什么全账户密钥需要特殊对待
Cloudflare 全局 API 密钥是全账户级别的且无作用域限制的。它可以触及与该电子邮件绑定的每个账户上的每一个区域、Worker 和 DNS 记录。这产生了两个截然不同的风险:
- 静止状态。 主目录文件中的明文密钥可以被任何接触你文件系统的进程、脚本或备份读取。一次粗心的
chmod、一次范围过大的备份任务、一次借用的笔记本电脑,就可能导致其暴露。 - 在 AI 会话中。 编程助手读取的任何内容,或任何打印该密钥的命令,都会落在发送给提供商的记录中。
cat ~/.cf,echo $CLOUDFLARE_API_KEY,或一个gpg --decrypt将其转储到屏幕的命令都会立即泄露它。
修复方案分为三个部分:保持密钥 在静止状态下加密,解密它 仅在一个永远不会打印它的短暂子进程中,并且 —— 如果 AI 助手在环节中 —— 添加一个 由代码强制执行的硬拦截,而不是靠记住要小心。
开始前的注意事项:如果你使用的工具支持有作用域的 API 令牌(Token)而不是全局 API 密钥,请使用令牌。令牌可以被单独撤销,并且无法触及指定作用域之外的任何内容,因此泄露的爆炸半径只是全局密钥的一小部分。以下步骤同样适用于令牌;你只需存储一个单独的令牌值,而不是电子邮件加密钥对。
步骤 1:检索凭据并在严格权限下将其暂存
从你提供商的仪表板获取密钥。对于 Cloudflare,也就是在 My Profile,然后是 API Tokens 选项卡,然后是 Global API Key 旁边的 View。
将其写入项目目录中的一个暂存文件,从第一个字节开始就将其锁定创建:
mkdir -p ~/vault
umask 077 # new files are created 600, never world-readable
printf '%s\n' "[email protected] REPLACE_WITH_GLOBAL_API_KEY main-account" > ~/vault/.cf
chmod 600 ~/vault/.cf
这个 umask 077 很重要:它保证了明文在创建期间不会被其他用户短暂读取,这是后来的 chmod 无法撤销的。
步骤 2:使用 GPG 对称 AES-256 对其进行加密
在这里,基于密码短语的对称加密是正确的选择。只有一个人需要解密此文件,因此公钥/私钥对会增加复杂性而没有任何好处。
gpg --symmetric --cipher-algo AES256 -o ~/vault/.cf.gpg ~/vault/.cf
GPG 会提示你输入并确认密码短语。选择一个强大的、独一无二的密码并在密码管理器中记录下来,因为如果你忘记了它,就无法恢复了。从现在起,这个密码短语是文件系统访问权限和密钥之间唯一的障碍。
步骤 3:在删除任何东西之前验证加密副本
在证明密文能够逐字节解密回来之前,永远不要删除明文:
diff <(gpg --quiet --batch --decrypt ~/vault/.cf.gpg) ~/vault/.cf
没有输出意味着两者是相同的,你可以安全地继续。任何输出都意味着出了问题 —— 密码短语错误、密码器错误、文件损坏 —— 你此时绝不能删除明文。
步骤 4:安全地删除明文
shred -u ~/vault/.cf
chmod 600 ~/vault/.cf.gpg
ls -la ~/vault/.cf* # only .cf.gpg should remain
shred 在取消链接之前覆盖字节,这比 rm更强大。对其局限性要诚实:在 SSD、写时复制(copy-on-write)文件系统或任何曾经被快照或备份过的文件上,覆盖的保证会被削弱。对抗真正被破坏的凭据的唯一真正保证是轮换,而不是删除一个副本。
步骤 5:缩短密码短语缓存(可选但推荐)
默认情况下 gpg-agent 会在你输入后缓存你的密码短语十分钟或更长时间。要缩小那个窗口:
mkdir -p ~/.gnupg && chmod 700 ~/.gnupg
cat >> ~/.gnupg/gpg-agent.conf <<'EOF'
default-cache-ttl 60
max-cache-ttl 120
EOF
chmod 600 ~/.gnupg/gpg-agent.conf
gpg-connect-agent reloadagent /bye
这将缓存限制为 60 到 120 秒 —— 足够长以便脚本在你解密后立即运行,足够短以至于走开不会让保管库处于有效解锁状态。
步骤 6:创建一个解密并执行的包装器,且永远不打印该机密
这一步使得保管库可以在日常中使用,而永远不会暴露密钥。将其保存为 ~/vault/cf-run.sh:
#!/usr/bin/env bash
# cf-run.sh — decrypt .cf.gpg and exec a command with CLOUDFLARE_EMAIL /
# CLOUDFLARE_API_KEY set for that one child process only. The decrypted value
# is never printed, written to disk, or returned as this script's output.
# Usage: ./cf-run.sh npx wrangler deploy
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CREDS_FILE="$SCRIPT_DIR/.cf.gpg"
[ -f "$CREDS_FILE" ] || { echo "cf-run.sh: $CREDS_FILE not found" >&2; exit 1; }
[ "$#" -gt 0 ] || { echo "usage: cf-run.sh <command> [args...]" >&2; exit 1; }
CREDS="$(gpg --quiet --batch --decrypt "$CREDS_FILE")" || { echo "cf-run.sh: decryption failed" >&2; exit 1; }
CF_EMAIL="$(awk '{print $1}' <<<"$CREDS")"
CF_KEY="$(awk '{print $2}' <<<"$CREDS")"
unset CREDS
exec env CLOUDFLARE_EMAIL="$CF_EMAIL" CLOUDFLARE_API_KEY="$CF_KEY" "$@"
chmod +x ~/vault/cf-run.sh
为什么它能保持安全,逐行解析:
- 解密后的文本通过命令替换直接进入 shell 变量 —— 永远不会到终端或 stdout。
awk从该变量中提取这两个字段;仍然没有任何内容被打印。unset CREDS一旦被提取出部分,合并的行就会立即从内存中丢弃。exec env VAR=... "$@"用你的目标命令替换包装器的进程,仅在其自己的环境中将值传递给它。没有任何东西被写入磁盘、记录或在任何时候被回显。
步骤 7:对一切都使用包装器
再也不要进行 CLOUDFLARE_API_KEY=$(cat ...) wrangler deploy 了。将每个命令路由通过包装器,并让目标工具从其自己的环境中读取变量:
~/vault/cf-run.sh npx wrangler deploy
~/vault/cf-run.sh curl -s "https://api.cloudflare.com/client/v4/user" \
-H "X-Auth-Email: $CLOUDFLARE_EMAIL" -H "X-Auth-Key: $CLOUDFLARE_API_KEY"
步骤 8(仅限 AI 助手用户):添加硬性保护钩子
如果编程助手在你的机器上运行命令,“记住不要打印密钥”并不是一种控制措施。许多助手暴露了一个钩子(hook)系统,该系统可以在命令运行前检查它并拒绝它。这个想法是一个执行前保护机制,它阻止两件事:直接读取保管库的明文形式,以及将保管库解密到 stdout 或管道,而不是解密到被捕获的变量中。
简单来说,保护机制检查挂起的 shell 命令并:
- 阻止 一个读取命令(
cat,less,head,strings等)指向保管库的明文文件名。 - 阻止 一个对保管库的
gpg --decrypt,除非其输出被捕获在类似以下的命令替换中X="$(gpg -d ... )". - 允许 其他一切,包括合法的
cf-run.sh调用。
一个最小的 Python 保护机制返回退出码 0 以允许,返回退出码 2 以阻止,并将原因打印到标准错误输出,以便助手看到原因:
#!/usr/bin/env python3
import json, re, sys
VAULT_HINTS = ("vault/.cf",) # add every vault path you protect
READ_CMDS = r"(cat|less|more|head|tail|strings|xxd|od)"
def main() -> int:
try:
cmd = (json.load(sys.stdin).get("tool_input") or {}).get("command", "") or ""
except Exception:
return 0 # never break tooling on a parse error
# Block reading the plaintext vault directly (matches .cf but not .cf.gpg)
if re.search(rf"(^|[;&|]\s*){READ_CMDS}\s+[^\n]*vault/\.cf(\s|$)", cmd):
sys.stderr.write("BLOCKED: reading the plaintext vault would leak it. Use cf-run.sh.\n")
return 2
# Only care about decrypting a protected vault
if re.search(r"gpg\b.{0,60}?(-d\b|--decrypt\b)", cmd) and any(h in cmd for h in VAULT_HINTS):
if re.search(r"\$\(\s*gpg\b", cmd) or re.search(r"`\s*gpg\b", cmd):
return 0 # captured into a variable — safe
sys.stderr.write("BLOCKED: decrypting to stdout leaks secrets. Capture into a variable.\n")
return 2
return 0
if __name__ == "__main__":
sys.exit(main())
将其接入你助手的预工具(pre-tool)钩子配置中,然后针对诱饵文件对其进行测试 —— 永远不要使用真实的保管库,因为测试期间规则中的一个错误可能会打印出实际的机密。创建一个无害的 vault/.cf 在一个一次性目录下,确认助手拒绝读取它,并单独确认合法的 cf-run.sh 命令仍然通过。
结论
仅在静止状态下加密凭据是不够的。该值只有在以一种永远不会将其暴露出来的方式解密,并且 —— 在涉及 AI 助手的地方 —— 如果不安全的访问是由代码而不是纪律来阻止的情况下,才能保持安全。GPG 对称加密、解密并执行的包装器以及执行前保护钩子,三者结合为你提供了一个真正可用且真正难以从中泄露的保管库。它需要十五分钟来设置,并消除了一整类悄无声息、令人尴尬的暴露事件。
优点
- 便携且无依赖:核心设置只需要
gpg和bash,它们几乎存在于每一台 Unix 机器上。 - 机密在正常使用期间永远不会被打印、写入磁盘或被记录。
- 对称加密使单一操作员的模型保持简单。
- 可选的保护钩子将“小心”变成了一个强制的、代码级别的控制措施。
- 同样的模式适用于任何高权限凭据,而不仅仅是 Cloudflare。
缺点
- 忘记密码短语意味着永久丢失;没有恢复途径。
shred无法在 SSD、写时复制(copy-on-write)文件系统或先前备份过的文件上完全保证删除。- 加密能保护未来的密钥,但无法撤销过去的暴露。
- 保护钩子是针对特定助手的,并且必须与你实际的保管库路径保持同步。
警告
本文中的每个值 —— [email protected], REPLACE_WITH_GLOBAL_API_KEY, main-account,以及 ~/vault 路径 —— 都是占位符。替换为你自己的值,并且永远不要将明文凭据提交到版本控制中。在用真实密钥信任包装器和保护机制之前,请先针对诱饵文件测试它们,并在你怀疑凭据暴露的那一刻立即轮换任何凭据。风险自负;你需对自己的凭据和基础设施负责。
常见问题解答
为什么使用 GPG 对称加密而不是公钥/私钥对? 对于既是加密者又是唯一解密者的单个操作员来说,基于密码短语的对称密码器更简单且同样强大。在这种情况下,密钥对增加了密钥管理的开销,而没有任何安全性提升。
将加密的 .cf.gpg 文件存储在 git 中安全吗? 在技术上,密文是可以安全提交的,但让它离开你的机器没有任何好处。将其保留在本地,而且千万、千万不要提交明文。
我应该使用 Cloudflare API Token 还是 Global API Key? 是的,只要工具支持。有作用域的令牌可以单独撤销,并且无法触及指定作用域之外的资源,因此其泄露的危害远小于泄漏的全局 API 密钥。
解密并执行的包装器实际上防止了什么? 它确保解密后的机密只存在于一个子进程的环境中。它永远不会被打印到终端、在 shell 历史中被捕获,或被写入磁盘,这些都是常见的意外泄露途径。
机密是如何泄露给 AI 编程助手的?
助手读取的一切内容或命令打印的一切内容,都会被添加到发送给模型提供商的会话记录中。一次针对明文密钥的 cat ,或一次解密到 stdout,都会将该密钥永久置于该记录中。
如果密钥已经被泄露过一次,加密它还有用吗? 没有。加密只保护密钥不被未来暴露。如果一个密钥曾经被打印、粘贴或在记录中出现过,你必须轮换它;对旧值进行加密丝毫不能改变它已被暴露的事实。
我应该多久轮换一次高权限密钥? 根据你的风险承受能力制定轮换计划,并在任何疑似暴露的情况下立即轮换。对于全账户密钥,宁可更频繁地轮换,因为它的爆炸半径是整个账户。
我能将它调整适用于 AWS、数据库或其他凭据吗? 可以。同样的三个理念 —— 在静止状态下加密,仅将其解密到一次性子进程中,并阻止不安全的读取 —— 适用于任何机密。调整字段解析和包装器中的环境变量名称以匹配即可。
标签
#Security #GPG #Encryption #SecretsManagement #Cloudflare #APIKeys #DevOps #DevSecOps #AICoding #Linux
Kubernetes Security Checklist
Harden cluster access, workload identity, pod security, network boundaries, software supply chain, secrets, and operational monitoring.
Free. No spam — unsubscribe in one click.


Responses
Sign in to leave a response.