锁箱
botlockbox 是一个用于AI代理、MCP服务器和CLI工具的凭证注入HTTPS/HTTP MITM代理。它位于您的代理和外部API之间,解密 age-在内存中加密秘密,并将其透明地注入到出站请求中。调用者——包括具有根shell访问权限的代理-- 永远看不到凭据.
建筑
+-----------------------------------------------------+
| AI Agent / MCP Server / CLI Tool |
| (zero credentials, uses http_proxy=localhost:8080) |
+----------------------+------------------------------+
|
| plain request (no creds)
v
+-----------------------------------------------------+
| botlockbox proxy (localhost:8080) |
| - decrypts secrets in memory (age + memguard) |
| - validates sealed host allowlist |
| - injects Authorization / API headers |
| - verifies upstream TLS (anti-DNS-rebinding) |
| - scrubs credentials from responses |
| - writes structured audit log (JSONL) |
+----------------------+------------------------------+
|
| request + injected credentials
v
External API (GitHub, OpenAI, AWS...)安全模型
| # | 攻击向量 | 缓解 | 层 |
|---|---|---|---|
| 1 | 阅读 `/proc/ | ||
| /mem` | PR_SET_DUMPABLE=0 | OS | |
| 2 | ptrace 附上 | PR_SET_DUMPABLE=0 +独立UID | 操作系统 |
| 3 | 秘密已交换到磁盘 | mlockall + MADV_DONTDUMP | OS |
| 4 | 核心转储包含秘密 | setrlimit(RLIMIT_CORE,0) + MADV_DONTDUMP | OS |
| 5 | GC在堆中复制机密 | memguard 加密飞地 | 应用程序 |
| 6 | MITM CA密钥在磁盘上可读 | ECDSA CA在内存中短暂存在,从不写入磁盘 | App |
| 7 | 配置被篡改以添加新主机 | 启动时密封信封验证——很难 os.Exit(1) | 应用程序 |
| 8 | 运行时配置被篡改以绕过 | 每次注射在注射器中密封满负荷检查 | 应用程序 |
| 9 | DNS重新绑定过去的主机检查 | 上游TLS证书验证 | 应用程序 |
| 10 | 响应体泄漏令牌 | 响应清除程序编辑已知的凭据模式 | 应用程序 |
| 11 | 提示注入编辑配置+重启 | 配置集 0444 下一次启动时进行印后+信封验证 | 应用程序+操作系统 |
| 12 | 无声凭据泄露 | 结构化JSONL审计日志(仅限秘密名称,从不包含值) | 应用程序 |
| 13 | 交换/休眠将内存写入磁盘 | mlockall | OS |
| 14 | 强行加密blob | age X25519/scrypt——计算上不可行 | 加密 |
| 15 | 二进制替换(交换锁盒) | 操作系统文件完整性监控(单独操作问题) | 操作 |
安装
go install github.com/trodemaster/botlockbox@latest或者从源代码构建:
git clone https://github.com/trodemaster/botlockbox
cd botlockbox
make build
# binary at bin/botlockbox快速开始
1.生成年龄密钥对(一次)
age-keygen -o ~/.age/identity.txt
# Public key printed to stdout: age1xxxxxxxxxx2.写下你的配置(这里没有秘密)
cp botlockbox.yaml ~/.config/botlockbox.yaml
# edit rules as needed3.封存你的秘密
cat |"generates and stores private key in hardware"| KG
KG -->|"identity reference (AGE-PLUGIN-SE-1…)"| IDFILE["~/.botlockbox/identity.txt"]
KG -->|"public key (age1se1q…)"| SEAL
CREDS["credentials
(stdin, plaintext)"] --> SEAL["botlockbox seal
--recipient age1se1q…"]
SEAL -->|"device-bound ciphertext"| SAGE["secrets.age"]
end
subgraph runtime["Every login — launchd user-session agent"]
direction LR
LAUNCHD["launchd"] -->|"starts at login, KeepAlive: true"| SERVE["botlockbox serve
--identity identity.txt"]
IDFILE -->|"identity ref"| SERVE
SAGE -->|"ciphertext"| SERVE
SERVE -->|"decrypts silently, no Touch ID"| MEM["secrets in
memguard enclaves"]
AGENT["AI agent / MCP / CLI
no credentials"] -->|"HTTPS_PROXY=localhost:8080"| SERVE
SERVE -->|"Authorization injected from enclave"| API["External API"]
end
SAGE -.->|"useless on any other Mac"| LOCK["🔒 device-bound"]一次性设置:
# Install age-plugin-se (e.g. via Homebrew)
brew install age-plugin-se
# Generate a key bound to this Mac's Secure Enclave (no Touch ID at runtime)
age-plugin-se keygen --access-control none -o ~/.botlockbox/identity.txt
# note the "public key: age1se1q..." line
# Seal your credentials to that public key
printf 'openai_key: "sk-xxxx"\ngithub_token: "ghp_xxxx"\n' \
| botlockbox seal \
--config ~/.botlockbox/botlockbox.yaml \
--recipient age1se1q...作为launchd用户会话代理安装 (参见 contrib/com.trodemaster.botlockbox.plist 对于完整模板):
# Edit the plist to set your username and paths, then:
cp contrib/com.trodemaster.botlockbox.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.trodemaster.botlockbox.plist代理在每次登录时启动。 secrets.age 和 identity.txt 在任何其他Mac上都是无用的——没有提示的硬件绑定。
旋转秘密:
# Re-seal with the new credential value
printf 'openai_key: "sk-new"\ngithub_token: "ghp_xxxx"\n' \
| botlockbox seal \
--config ~/.botlockbox/botlockbox.yaml \
--recipient age1se1q...
# Hot-reload the running proxy (no restart, zero dropped connections)
botlockbox reload --pidfile ~/.botlockbox/botlockbox.pid______________________________________________________________________
模式2:GitHub操作自托管运行器(临时密钥)
运行者托管调用外部API的AI代理。凭据存在于GitHub Actions secrets中,从不放置在代理环境变量中。每次工作流运行都会生成一个新的临时年龄密钥,并直接通过管道传输到 botlockbox serve 通过 --identity-stdin — 私钥从不接触磁盘.
flowchart TD
subgraph gha["GitHub Actions workflow — self-hosted runner"]
direction TB
subgraph setup["Job startup (root / botlockbox user)"]
GHS["GitHub Actions secrets
OPENAI_KEY, GITHUB_TOKEN…"]
KEYGEN["age-keygen
ephemeral keypair"]
PUBKEY["public key
shell variable only"]
PRIVKEY["private key
shell variable only"]
KEYGEN --> PUBKEY
KEYGEN --> PRIVKEY
GHS --> SEAL["botlockbox seal
--recipient $PUBKEY"]
PUBKEY --> SEAL
SEAL --> SAGE["secrets.age
run-scoped"]
PRIVKEY -->|"piped via stdin"| SERVE["botlockbox serve
--identity-stdin"]
SAGE --> SERVE
SERVE -->|"decrypts, scrambles key buffer"| MEM["secrets in
memguard enclaves"]
end
subgraph run["Agent execution (unprivileged user)"]
AGENT["AI agent
uid: agent"]
AGENT -->|"HTTPS_PROXY=localhost:8080, no credentials in env"| SERVE
SERVE -->|"Authorization injected from enclave"| API["External API"]
end
end
KEYGEN -.->|"shell vars gone after step exits"| GONE["🗑 ephemeral"]
SERVE -.->|"no identity.txt on disk"| NOFILE["no key on disk"]工作流模式:
jobs:
run-agent:
runs-on: [self-hosted, linux]
steps:
- uses: actions/checkout@v4
- name: Start botlockbox (key never touches disk)
env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
run: |
# Generate a fresh ephemeral keypair for this run
IDENTITY=$(age-keygen)
PUBKEY=$(echo "$IDENTITY" | grep '# public key:' | awk '{print $NF}')
# Seal the credentials to the ephemeral public key
printf 'openai_key: "%s"\n' "$OPENAI_KEY" \
| botlockbox seal --config botlockbox.yaml --recipient "$PUBKEY"
# Pipe the private key directly to serve — never written to disk
echo "$IDENTITY" | botlockbox serve \
--config botlockbox.yaml \
--identity-stdin \
--ca-cert /tmp/botlockbox-ca.pem \
--pidfile /tmp/botlockbox.pid &
# Trust the ephemeral CA so agent tools can verify TLS
sudo cp /tmp/botlockbox-ca.pem /usr/local/share/ca-certificates/botlockbox.crt
sudo update-ca-certificates
- name: Run agent (no credentials in environment)
run: |
HTTPS_PROXY=http://127.0.0.1:8080 python agent.py这可以防止什么:
| 威胁 | 不带锁盒 | 带锁盒 |
|---|---|---|
代理人阅读 $OPENAI_KEY 来自env | ✗ 暴露 | ✓ 不在env中 |
| 代理从磁盘读取凭据 | ✗ 如果写入文件 | ✓ 从不在磁盘上 |
| 代理日志或泄漏请求正文 | ✗ 凭证可见 | ✓ 从响应中删除 |
泄漏 secrets.age 运行后的文件 | ✗ 可使用密钥解密 | ✓ 钥匙转瞬即逝,下班后就不见了 |
| 代理呼叫未列出的主机 | ✗ 不执行 | ✓ 密封的allowlist阻断注射 |
过程隔离(推荐):
为了加强隔离,请以特权用户身份运行botlockbox,并以单独的非特权用户运行代理。代理可以通过TCP访问代理,但无法读取botlockbox的内存或文件。
uid 0 / botlockbox: botlockbox serve --identity-stdin ...
uid 1001 / agent: HTTPS_PROXY=http://127.0.0.1:8080 python agent.py在Docker中,这是一个两阶段的入口点:以root身份启动代理,然后 exec su -c "python agent.py" agent.
______________________________________________________________________
模式3:Mac主机上的MCP容器(Docker Compose)
在Docker容器中运行MCP服务器,同时botlockbox提供来自Mac主机的凭据注入。集装箱装载 零凭据 --全部 Authorization 在请求到达外部API之前,botlockbox会注入标头。
这适用于任何遵守标准HTTP代理环境变量的MCP服务器。无需更改MCP服务器映像。
flowchart TD
subgraph mac["Mac Host"]
SE["Secure Enclave
(age-plugin-se)"]
BLB["botlockbox
0.0.0.0:8080
(launchd agent)"]
CAPEM["~/.botlockbox/ca.pem"]
SE -->|"decrypts silently"| BLB
BLB -->|"--ca-cert"| CAPEM
end
subgraph docker["Docker Desktop (bridge network)"]
MCP1["MCP server A
(Python)"]
MCP2["MCP server B
(Node.js)"]
MCP1 & MCP2 -->|"volume mount :ro"| VOL["/etc/botlockbox/ca.pem"]
end
CAPEM -->|"bind mount"| VOL
MCP1 -->|"HTTPS_PROXY=host.docker.internal:8080"| BLB
MCP2 -->|"HTTPS_PROXY=host.docker.internal:8080"| BLB
BLB -->|"Authorization injected from enclave"| API["External APIs
OpenAI, GitHub…"]先决条件:
- 在Mac主机上运行的botlockbox(模式1-launchd+Secure Enclave)。
listen: "0.0.0.0:8080"在botlockbox.yaml--默认值127.0.0.1无法从Docker的网桥网络访问。- CA证书已写入
~/.botlockbox/ca.pem通过--ca-cert ~/.botlockbox/ca.pem在洗衣店。
Docker编写配置 (完整模板在 contrib/docker-compose.example.yml):
x-botlockbox-proxy: &botlockbox-proxy
HTTP_PROXY: "http://host.docker.internal:8080"
HTTPS_PROXY: "http://host.docker.internal:8080"
http_proxy: "http://host.docker.internal:8080"
https_proxy: "http://host.docker.internal:8080"
NO_PROXY: "localhost,127.0.0.1,*.local"
x-botlockbox-ca: &botlockbox-ca
REQUESTS_CA_BUNDLE: /etc/botlockbox/ca.pem # Python (requests, httpx, boto3, openai-sdk)
NODE_EXTRA_CA_CERTS: /etc/botlockbox/ca.pem # Node.js
SSL_CERT_FILE: /etc/botlockbox/ca.pem # Go stdlib, Ruby net/http
CURL_CA_BUNDLE: /etc/botlockbox/ca.pem # curl / libcurl
services:
mcp-server:
image: your-mcp-server-image:latest
environment:
<<: [*botlockbox-proxy, *botlockbox-ca]
# No API keys here — botlockbox injects them.
volumes:
- "${HOME}/.botlockbox/ca.pem:/etc/botlockbox/ca.pem:ro"
extra_hosts:
- "host.docker.internal:host-gateway" # needed on Linux Docker; harmless on Mac它是如何工作的:
- Docker桌面解析
host.docker.internal自动连接到Mac主机IP。 HTTPS_PROXY导致容器的HTTP库通过botlockbox隧道传输所有HTTPS。- botlockbox使用其临时CA对TLS会话进行MITM,从Secure Enclave注入凭据,并转发请求。
- CA证书从Mac主机以只读方式挂载;每种语言运行库都通过自己的env-var信任它——无需更改代码,也无需重建映像。
MCP服务器的视角: 它对以下对象进行正常的HTTPS调用 api.openai.com。响应到达,但凭据从未出现在其环境中。
______________________________________________________________________
CLI参考
botlockbox seal
从stdin读取明文机密,将其绑定到从配置导出的主机分配列表,并写入 age-加密信封 secrets_file。还将配置设置为只读(0444)以防止后密封篡改。
botlockbox seal --config
(--identity
| --recipient
)| 标志 | 默认值 | 描述 |
|---|---|---|
--config | botlockbox.yaml | 通往 botlockbox.yaml |
--identity | -- | 年龄X25519身份文件的路径;从密钥中导出收件人。相互排斥 --recipient. |
--recipient | -- | 年龄公钥字符串(age1… 或 age1se1…).将其用于插件密钥,例如 age-plugin-se.相互排斥 --identity. |
正是其中之一 --identity 或 --recipient 是必需的。
Stdin格式 --YAML键/值对:
github_token: "ghp_xxxxxxxxxxxxxxxxxxxx"
openai_key: "sk-xxxxxxxxxxxxxxxxxxxx"引用的每个秘密名称 {{secrets.NAME}} 配置中的模板必须存在于stdin上。丢失秘密是一个严重的错误。
重新密封 --奔跑 seal 每当您旋转机密或向配置中添加新主机时,都会再次出现这种情况。上一个 secrets.age 被原子覆盖。
______________________________________________________________________
botlockbox serve
解密密封的信封,根据实时配置进行验证,将机密加载到锁定的内存中,并启动MITM代理。
botlockbox serve --config
(--identity
| --identity-stdin) [flags]| 标志 | 默认值 | 描述 |
|---|---|---|
--config | botlockbox.yaml | 通往 botlockbox.yaml |
--identity | -- | 年龄标识文件的路径。相互排斥 --identity-stdin. |
--identity-stdin | false | 从stdin读取年龄标识;密钥永远不会写入磁盘。相互排斥 --identity. |
--pidfile | -- | 在此处写入代理PID;被...使用 botlockbox reload. |
--ca-cert | -- | 在此处编写临时MITM CA公共证书PEM,以便客户端可以信任它 |
正是其中之一 --identity 或 --identity-stdin 是必需的。
启动顺序:
- 加载并解析
botlockbox.yaml - 解密
secrets_file使用年龄身份 - 根据实时配置验证密封的信封——配置中存在的任何在密封时未提交的秘密或主机都会立即导致
os.Exit(1) - 将每个秘密加载到
memguard加密飞地;立即对明文字节进行加密 - 应用OS硬化(
PR_SET_DUMPABLE=0,mlockall,RLIMIT_CORE=0在Linux上) - 在内存中生成临时ECDSA P-256 MITM CA(24小时寿命,从不写入磁盘)
- 如果需要,写入CA证书PEM和PID文件
- 开始接受连接
______________________________________________________________________
botlockbox reload
向跑步者发送SIGHUP serve 进程,触发实时秘密重新加载。如果重新加载因任何原因失败,代理将继续使用旧密钥。
botlockbox reload --pidfile
| 标志 | 默认值 | 描述 |
|---|---|---|
--pidfile | _(必填)_ | 写入PID文件的路径 botlockbox serve. |
______________________________________________________________________
配置参考
| 字段 | 类型 | 默认值 | 描述 |
|---|---|---|---|
listen | 字符串 | 127.0.0.1:8080 | 代理监听地址 |
secrets_file | 字符串 | ~/.botlockbox/secrets.age | 通往年龄加密秘密的路径 |
verbose bool的。 false | 记录每个代理请求 | ||
rules | list | -- | 凭证注入规则 |
rules[].name | string | -- | 人类可读的规则名称(出现在审核日志中) |
rules[].match.hosts | 列表 | -- | 主机全局模式(*.example.com 支持) |
rules[].match.path_prefixes | list | -- | 可选URL路径前缀筛选器 |
rules[].inject.headers | map | -- | 请求注入标头;支持 {{secrets.NAME}} |
rules[].inject.query_params | map | -- | 查询要注入的参数;支持 {{secrets.NAME}} |
机密文件格式
通过stdin提供给 botlockbox seal 只有-- 从未以明文形式写入磁盘:
github_token: "ghp_xxxxxxxxxxxxxxxxxxxx"
openai_key: "sk-xxxxxxxxxxxxxxxxxxxx"
aws_session_token: "IQoJb3..."写入磁盘的唯一工件是 secrets.age --不透明的 age-加密blob。
部署态势
+---------------------------------------------+
| Container / VM |
| |
| uid 1001: botlockbox serve |
| - secrets.age (0600, owned by 1001) |
| - identity.txt (0600, owned by 1001) |
| - botlockbox.yaml (0444, read-only) |
| - PR_SET_DUMPABLE=0 |
| - mlockall |
| - RLIMIT_CORE=0 |
| |
| uid 1002: AI agent / MCP server |
| - http_proxy=http://127.0.0.1:8080 |
| - NO access to uid 1001 files |
| - NO ptrace capability on uid 1001 |
| |
| Egress firewall: only allowlisted hosts |
+---------------------------------------------+建筑
make build # compile to bin/botlockbox
make install # go install
make test # go test -race ./...
make lint # go vet ./...
make tidy # go mod tidy许可证
麻省理工学院
