在静止状态下加密高权限 API 密钥:一个将机密排除在 AI 记录之外的 GPG 保管库

在静止状态下加密高权限 API 密钥:一个将机密排除在 AI 记录之外的 GPG 保管库

一个便携的、无依赖的运行手册,用于使用 GPG、解密并执行包装器以及在工具层面防止意外泄漏的硬性保护机制来锁定 Cloudflare 全局 API 密钥(或任何全账户级别的凭据)。

大多数凭据泄露并不引人注目。没有黑客,也没有违规报告。有人仅仅是将一个全账户 API 密钥放在其主目录中的一个明文文件里,有一天,一次备份、一次屏幕共享、一次粘贴的命令或一次 AI 编程会话,就会悄悄地将其带到它永远不应该去的地方。

截至 2026 年 8 月,最后一个途径已经改变了安全计算的规则。现在几乎每个开发者都会运行一个代表他们读取文件和运行命令的 AI 编程助手。这种工具读取的任何内容,以及命令打印的任何内容,都会成为会话记录的一部分,在每一次交互中都会传输给模型提供商。如果一个明文密钥在会话期间被显示、被解密到屏幕上或被回显,它现在就存在于那个记录中,脱离了你的控制。这并非假设:这正是一个极其谨慎的工程师在没有做任何明显错事的情况下,泄露密钥的方式。

本文是用于修复该问题的完整、通用的运行手册。此处的实际示例是 Cloudflare 全局 API 密钥,但该技术适用于任何高权限凭据。第一部分不使用任何其他东西,除了 gpgbash ,并且适用于任何 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)文件系统或任何曾经被快照或备份过的文件上,覆盖的保证会被削弱。对抗真正被破坏的凭据的唯一真正保证是轮换,而不是删除一个副本。

默认情况下 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 对称加密、解密并执行的包装器以及执行前保护钩子,三者结合为你提供了一个真正可用且真正难以从中泄露的保管库。它需要十五分钟来设置,并消除了一整类悄无声息、令人尴尬的暴露事件。

优点

  • 便携且无依赖:核心设置只需要 gpgbash,它们几乎存在于每一台 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

Free field guide

Kubernetes Security Checklist

Harden cluster access, workload identity, pod security, network boundaries, software supply chain, secrets, and operational monitoring.