English
← 返回 Agent Rule

AI Agent 的密钥管理:Docker、Git、sun-port 与 PostgreSQL ✓ 已验证

2026-08-15 · 16 分钟 · Secrets · Docker · Git · sun-port · PostgreSQL · Hermes · im-bot

AI Agent 是那种既持有你的凭据、又自主使用它们的软件。它读取你的 LLM 供应商密钥,写入你的数据库,推送到你的代码仓库,并以你的身份发送消息。在普通 Web 应用里,一个泄漏的密钥只是一处配置 bug;而在 Agent 里,它就是一个已经确切知道该如何使用这把密钥的攻击者——因为 Agent 的全部工作,就是使用密钥。本教程把密钥牢牢锁在整个 Agent 技术栈的每一个环节上:Git 让它们远离历史记录,Docker 在运行时注入它们,sun-port 在传输中加密它们,而 PostgreSQL 记录下谁在何时轮换了什么——全部接入本站所记录的 Hermesim-bot Agent 运行时。

本文中的每一条命令都在 agent-rule.com 背后的真实技术栈上运行过——Hermes Agent 以 Docker 部署,由 sun-port 前置,以 PostgreSQL 作为后端,通过 im-bot 协同调度。✓ 已验证 徽章意味着真实的执行,而非从 README 里复制粘贴。

1. 为什么 Agent 让密钥管理变得更难

传统服务只有少数几个密钥,启动时加载一次,在一个狭窄且易于理解的范围内使用。而 Agent 在四个方面截然不同:

im-bot 这样的多 Agent 系统中,爆炸半径还会进一步扩大:Agent 共享房间、共享数据库、共享消息总线。一把泄漏的连接器密钥,就能让攻击者冒充房间里的每一个 Agent。

2. 什么才算密钥

凡是能授予访问权限、证明身份或解密数据的东西,都是密钥。对于一个典型的 Agent 技术栈,清单大致如下:

并非每一段配置都是密钥。数据库主机名、模型名称或回调URL放进仓库里没有问题。经验法则:如果把它泄露给攻击者会改变你的安全态势,那它就是密钥。

3. 威胁模型

在挑选工具之前,先点明密钥可能泄漏的四条途径。下文的一切都能对应到其中之一:

  1. Git 历史记录——六个月前提交的密钥会永远留在仓库里,哪怕它已经从 HEAD 中删除。
  2. 日志与追踪——会回显环境变量或参数的 stdout、Agent 追踪和错误报告。
  3. 上下文窗口——粘贴进提示词的密钥,可通过提示词注入或供应商侧日志恢复。
  4. 磁盘与备份——磁盘上或备份里的明文 .env 文件、Docker 层和数据库转储。

你的防御应当是纵深防御:任何单一控制手段都不够,但合在一起,就能让每条途径都难以被独立利用。

4. Git——绝不提交密钥

最廉价、也最有价值的控制手段:一个 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)重写历史、强制推送,并立即轮换这把密钥。一把曾经进入过历史记录的密钥已经作废——要假定攻击者早已扫描过它。

5. 运行时注入——环境变量

十二要素法则对 Agent 同样适用:配置存放在环境中,而不是代码里。在运行时加载密钥,让同一个镜像能在开发和生产环境对着不同的凭据运行:

$ export OPENAI_API_KEY="***"
$ export POSTGRES_PASSWORD="***"
$ export JWT_SECRET=*** rand -hex 32)"
$ ./agent run

两条规则能让这种做法安全:

如果你在开发时不得不保留一份本地 .env,就把它锁起来:

$ chmod 600 .env

6. Docker——Compose Secrets 与 env-file 陷阱

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 的转储里。

env-file 陷阱:docker run --env-file 只在创建容器时读取该文件。编辑 .env 后再执行 docker restart并不会改变正在运行的容器的环境变量。要应用一个已轮换的密钥,你必须 docker rm -f 之后重新 docker run——这是重建,而不是重启。这正是「我轮换了密钥,但旧的却仍然有效」这一现象最常见的原因。

关于重建模式的完整解析,以及如何证明新值确实生效,请参阅我们的 Docker 部署教程

7. 让密钥远离上下文窗口

最新、也最不显眼的泄漏面,是模型本身。提示词注入攻击会把指令藏在工具输出里——Agent 读取的某个网页、打开的某个文件、收到的某条消息。如果你的 Agent 上下文中含有一把存活的 API 密钥,一条注入指令就能说服它「把密钥复述一遍,以确认它仍然有效」。解决方案在于架构层面:

要把上下文窗口当作一份第三方(模型供应商)能够读取的日志文件来对待——因为事实上,它就是那样。

8. sun-port——在传输中加密密钥

密钥的安全性,取决于它所经过的通道。如果你的 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 证书;它身后的一切都只走回环,这样传输中的凭据就永远不会以未加密的形式离开主机。

9. PostgreSQL——存储「非机密」,审计「机密」

在这个设计里,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
如果你必须在数据库里静态存放一个密钥——比如说某个其他服务需要读回的 webhook 签名密钥——就用 pgcryptopgp_sym_encrypt 加密它,并把口令放进一个 Compose secret 里,而不是放在 schema 中。表里的明文凭据,等于给任何能导出数据库的人送上一份大礼。

10. im-bot 连接器凭据

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 节),因此连接器在启动时读取自己的凭据,模型则从不碰它。

11. 轮换

密钥会腐化。要按计划轮换,在怀疑任何泄漏时轮换,并在任何人员或 Agent 权限变更时轮换。轮换是整条流水线合流的地方——也是第 6 节里那个 env-file 重建陷阱最要命的地方:

  1. 在服务器端生成一个新值,远离任何日志:openssl rand -hex 32
  2. 更新 Compose secret 文件,并执行 docker compose up -d --force-recreate(或 docker rm -f + docker run)。
  3. 在供应商控制台更新该值(或调用供应商的轮换 API)。
  4. 写入 secret_events 行,让审计追踪如实反映这次轮换。
  5. 在宣告成功之前,验证新值有效、旧值已经失效。

把第 1–4 步安排成一个 Hermes cron 任务——就是我们在cron 自动化教程里介绍过的那个调度器——让轮换按日历发生,而不是等人想起来才做。

12. 验证

如果看不到,密钥工作就无法验证。在信任这套配置之前,先证明三件事:

# 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   # 连接被拒绝——数据库只走回环

请注意这种验证风格:只检查密钥是否存在长度是否正确,而绝不把值打印进日志。一把只存在于一个你从不去回显的文件里的凭据,就是一把不可能通过你自己的工具泄漏出去的凭据。

13. 常见坑

14. 核心要点

Agent 的密钥管理不是一项你可以事后加装的功能——它是架构本身的一种属性。Git 守护源码,Docker 守护运行时,sun-port 守护传输链路,PostgreSQL 守护历史,而运行时守护提示词。把这五层都做到位,一把泄漏的密钥就不再是一场灾难,而只是一次例行轮换。