AI Agent 是那种既持有你的凭据、又自主使用它们的软件。它读取你的 LLM 供应商密钥,写入你的数据库,推送到你的代码仓库,并以你的身份发送消息。在普通 Web 应用里,一个泄漏的密钥只是一处配置 bug;而在 Agent 里,它就是一个已经确切知道该如何使用这把密钥的攻击者——因为 Agent 的全部工作,就是使用密钥。本教程把密钥牢牢锁在整个 Agent 技术栈的每一个环节上:Git 让它们远离历史记录,Docker 在运行时注入它们,sun-port 在传输中加密它们,而 PostgreSQL 记录下谁在何时轮换了什么——全部接入本站所记录的 Hermes 与 im-bot Agent 运行时。
传统服务只有少数几个密钥,启动时加载一次,在一个狭窄且易于理解的范围内使用。而 Agent 在四个方面截然不同:
在 im-bot 这样的多 Agent 系统中,爆炸半径还会进一步扩大:Agent 共享房间、共享数据库、共享消息总线。一把泄漏的连接器密钥,就能让攻击者冒充房间里的每一个 Agent。
凡是能授予访问权限、证明身份或解密数据的东西,都是密钥。对于一个典型的 Agent 技术栈,清单大致如下:
并非每一段配置都是密钥。数据库主机名、模型名称或回调URL放进仓库里没有问题。经验法则:如果把它泄露给攻击者会改变你的安全态势,那它就是密钥。
在挑选工具之前,先点明密钥可能泄漏的四条途径。下文的一切都能对应到其中之一:
HEAD 中删除。.env 文件、Docker 层和数据库转储。你的防御应当是纵深防御:任何单一控制手段都不够,但合在一起,就能让每条途径都难以被独立利用。
最廉价、也最有价值的控制手段:一个 pre-commit 钩子,从源头上拒绝任何密钥进入历史记录。gitleaks 会对暂存的变更做基于熵和基于模式的扫描,匹配 100 多种供应商签名:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.4
hooks:
- id: gitleaks
$ pip install pre-commit
$ pre-commit install
$ git add . && git commit -m "add agent loop"
gitleaks.............................................................Failed
- hook id: gitleaks
- exit code: 1
Finding: generic-api-key
Secret: sk-abc123...
File: .env
Commit: (current)
再叠加一层 .gitignore,让凭据文件连被意外暂存的机会都没有:
# .gitignore
.env
.env.*
!.env.example
secrets/
*.pem
*.key
这个 .env.example 例外很重要——提交的是带占位符值的模板,绝不是真实值。
git filter-repo(或 BFG)重写历史、强制推送,并立即轮换这把密钥。一把曾经进入过历史记录的密钥已经作废——要假定攻击者早已扫描过它。十二要素法则对 Agent 同样适用:配置存放在环境中,而不是代码里。在运行时加载密钥,让同一个镜像能在开发和生产环境对着不同的凭据运行:
$ export OPENAI_API_KEY="***"
$ export POSTGRES_PASSWORD="***"
$ export JWT_SECRET=*** rand -hex 32)"
$ ./agent run
两条规则能让这种做法安全:
Dockerfile 或镜像层里都不应出现任何密钥。如果你在开发时不得不保留一份本地 .env,就把它锁起来:
$ chmod 600 .env
Docker 有两种值得使用的机制,以及一个几乎人人都会踩的坑。处理真实密钥的正确工具是 Compose secrets——其值以文件形式挂载进容器,因此绝不会出现在 docker inspect 或进程列表里:
# docker-compose.yml
services:
agent:
image: ghcr.io/example/agent:latest
secrets:
- openai_api_key
- pg_password
environment:
OPENAI_API_KEY_FILE: /run/secrets/openai_api_key
POSTGRES_PASSWORD_FILE: /run/secrets/pg_password
db:
image: postgres:16
secrets:
- pg_password
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/pg_password
secrets:
openai_api_key:
file: ./secrets/openai_api_key.txt
pg_password:
file: ./secrets/pg_password.txt
你的应用从文件而非环境变量中读取密钥,这样明文就永远不会出现在 shell 或 docker inspect 的转储里。
docker run --env-file 只在创建容器时读取该文件。编辑 .env 后再执行 docker restart,并不会改变正在运行的容器的环境变量。要应用一个已轮换的密钥,你必须 docker rm -f 之后重新 docker run——这是重建,而不是重启。这正是「我轮换了密钥,但旧的却仍然有效」这一现象最常见的原因。关于重建模式的完整解析,以及如何证明新值确实生效,请参阅我们的 Docker 部署教程。
最新、也最不显眼的泄漏面,是模型本身。提示词注入攻击会把指令藏在工具输出里——Agent 读取的某个网页、打开的某个文件、收到的某条消息。如果你的 Agent 上下文中含有一把存活的 API 密钥,一条注入指令就能说服它「把密钥复述一遍,以确认它仍然有效」。解决方案在于架构层面:
***,而运行时值仍然完好。要把上下文窗口当作一份第三方(模型供应商)能够读取的日志文件来对待——因为事实上,它就是那样。
密钥的安全性,取决于它所经过的通道。如果你的 Agent 通过明文 HTTP POST 一个凭据,路径上的任何人都能读到它。sun-port——我们早先介绍过的那个基于 Cloudflare Pingora 的反向代理——在边缘终结 TLS,这样内部服务就只需要对着 localhost 讲明文:
# 内部服务只绑定回环地址——绝不暴露到公网
$ ss -tlnp | grep -E '5432|8000'
LISTEN 0 128 127.0.0.1:5432 0.0.0.0:* (postgres)
LISTEN 0 128 127.0.0.1:8000 0.0.0.0:* (agent-api)
规则就是:把一切都绑定到 127.0.0.1,除了 sun-port 什么都不暴露。代理持有唯一一张面向公网的 TLS 证书;它身后的一切都只走回环,这样传输中的凭据就永远不会以未加密的形式离开主机。
在这个设计里,PostgreSQL 的职责并不是存放原始密钥——对于那些启动时需要注入的凭据,数据库是个糟糕的存放处。相反,要用它来做密钥管理系统真正需要的两件事:配置指针和审计追踪。
存储密钥在哪里以及它的生命周期,而非密钥本身:
-- secrets.sql — 指针与轮换审计,绝不存放原始值
CREATE TABLE IF NOT EXISTS secret_registry (
name TEXT PRIMARY KEY, -- 'openai_api_key', 'pg_password'
kind TEXT NOT NULL, -- 'env' | 'compose-secret' | 'vault'
location TEXT NOT NULL, -- '/run/secrets/openai_api_key'
owner TEXT NOT NULL, -- 'agent', 'im-bot', 'infra'
rotated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ
);
CREATE TABLE IF NOT EXISTS secret_events (
id BIGSERIAL PRIMARY KEY,
secret_name TEXT NOT NULL REFERENCES secret_registry(name),
event TEXT NOT NULL, -- 'created' | 'rotated' | 'revoked'
actor TEXT, -- 是哪个 Agent 或操作员执行的
at TIMESTAMPTZ NOT NULL DEFAULT now()
);
现在,你就能用一条查询来回答「是谁在何时轮换了数据库密码?」,而不必在 Slack 里做一场考古挖掘:
$ sudo -u postgres psql -d agent_rule -c \
"SELECT secret_name, event, actor, at FROM secret_events ORDER BY at DESC LIMIT 10;"
secret_name | event | actor | at
-----------------+---------+----------+---------------------
pg_password | rotated | hermes | 2026-08-15 09:00:00
openai_api_key | created | susu | 2026-08-14 18:12:00
pgcrypto 的 pgp_sym_encrypt 加密它,并把口令放进一个 Compose secret 里,而不是放在 schema 中。表里的明文凭据,等于给任何能导出数据库的人送上一份大礼。im-bot 通过连接器接入 Telegram、Discord、Slack 等外部平台,而每个连接器都携带各自的机器人令牌或 API 密钥。同样的纪律在这里照样适用,只是多了一条专属于多 Agent 系统的规则:凭据按连接器限定范围,而不是按房间。如果同一个房间里的两个 Agent 共享一个 Telegram 机器人,它们就共享同一把令牌;不要为每个 Agent 铸造实际上可以互换的令牌,也不要把连接器令牌粘贴进房间的共享上下文里——那里每个 Agent(以及任何提示词注入)都能读到它。
# im-bot 连接器配置 — 令牌通过环境变量/文件注入,绝不内联在房间里
connectors:
telegram:
enabled: true
token_file: /run/secrets/telegram_bot_token
discord:
enabled: true
token_file: /run/secrets/discord_bot_token
令牌文件由 Compose secrets 挂载(见第 6 节),因此连接器在启动时读取自己的凭据,模型则从不碰它。
密钥会腐化。要按计划轮换,在怀疑任何泄漏时轮换,并在任何人员或 Agent 权限变更时轮换。轮换是整条流水线合流的地方——也是第 6 节里那个 env-file 重建陷阱最要命的地方:
openssl rand -hex 32。docker compose up -d --force-recreate(或 docker rm -f + docker run)。secret_events 行,让审计追踪如实反映这次轮换。把第 1–4 步安排成一个 Hermes cron 任务——就是我们在cron 自动化教程里介绍过的那个调度器——让轮换按日历发生,而不是等人想起来才做。
如果看不到,密钥工作就无法验证。在信任这套配置之前,先证明三件事:
# 1. 历史记录是干净的——gitleaks 在所有提交中都一无所获
$ gitleaks detect --source . --report-format json --report-path /dev/null
0 leaks found
# 2. 运行中的容器拿到了新值(重建生效了),并且没有打印出来
$ docker exec agent sh -c 'echo ${OPENAI_API_KEY_FILE:+set}'
set
$ docker exec agent sh -c 'cat /run/secrets/openai_api_key | wc -c'
52
# 3. 从外部无法通过明文访问到任何秘密
$ curl -s -o /dev/null -w '%{http_code}' http://your-host:5432
000 # 连接被拒绝——数据库只走回环
请注意这种验证风格:只检查密钥是否存在、长度是否正确,而绝不把值打印进日志。一把只存在于一个你从不去回显的文件里的凭据,就是一把不可能通过你自己的工具泄漏出去的凭据。
docker restart 不会重新加载 --env-file——这是排名第一的轮换失败原因。要重建,不要重启。COPY 进 Dockerfile 或被某个 RUN 步骤烧进去的密钥,会永远留在镜像里。只用 Compose secrets 或运行时环境变量。.env——包含 .env 的数据库转储或服务器快照会把一切都泄露出去。把它排除在备份之外,或对备份加密。COPY 进镜像,绝不粘贴进 docker inspect。Agent 的密钥管理不是一项你可以事后加装的功能——它是架构本身的一种属性。Git 守护源码,Docker 守护运行时,sun-port 守护传输链路,PostgreSQL 守护历史,而运行时守护提示词。把这五层都做到位,一把泄漏的密钥就不再是一场灾难,而只是一次例行轮换。