跳转到内容

用本地 echo 上游验证脱敏

Tokenhush 是一个本地、仅回环的 HTTP 网关:在 AI 编程工具的请求到达模型之前完成脱敏,并在响应回来时还原。这项检查无需向真实厂商发送任何内容,就能验证整个往返:你运行一个回显请求 body 的假厂商,把一个一次性网关指向它,发送一个携带假密钥的请求,然后观察密钥在哪里出现、在哪里不出现。

需要 Go 1.25+,以及 python3 和 curl。

在第一个终端里启动 echo 上游:

终端窗口
python3 - <<'PY'
import http.server, sys
class Echo(http.server.BaseHTTPRequestHandler):
def do_POST(self):
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
sys.stderr.write("upstream received: " + body.decode() + "\n"); sys.stderr.flush()
self.send_response(200); self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body))); self.end_headers()
self.wfile.write(body)
def log_message(self, *args): pass
http.server.HTTPServer(("127.0.0.1", 9999), Echo).serve_forever()
PY

它把收到的每个请求 body 都打印到 stderr。让它保持运行。

在第二个终端里创建一个一次性的 TOKENHUSH_HOME,写一个最小的 tokenhush.yaml, 把 /v1/chat/completions 路由到 echo 上游,然后启动网关:

终端窗口
export TOKENHUSH_HOME="$(mktemp -d)"
mkdir -p "$TOKENHUSH_HOME/config"
cat > "$TOKENHUSH_HOME/config/tokenhush.yaml" <<'YAML'
listen:
host: 127.0.0.1
port: 8787
upstreams:
- match: /v1/chat/completions
target: http://127.0.0.1:9999
YAML
tokenhush run

同样让它保持运行。它每次脱敏都会打印一行掩码的 stderr 日志。

在第三个终端里发送一个请求,body 里带一个假密钥:

终端窗口
curl -sS http://127.0.0.1:8787/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"echo","messages":[{"role":"user","content":"my email is [email protected]"}]}'

任何请求之前,网关会打印一段启动横幅:回环端点、实际生效的上游路由(配置项加上 内置回退),以及把工具指过来所需的两种 base URL 形式。横幅里没有任何密钥。接下来 下面三项观察合在一起,就验证了整个往返。

1. 上游终端打印出的 body 里,密钥已被替换。 该值以占位符形式离开网关,所以这是 假厂商收到的东西:

upstream received: {"model":"echo","messages":[{"role":"user","content":"my email is __PII_email_<digest>__"}]}

<digest> 是每次会话的哈希,所以具体值每次运行都不同。关键在于上游从未见到 [email protected]。

2. 网关终端打印了两行只含元数据的日志。 它改动的值按类型、长度和掩码形式报告, 绝不给出完整值;回程只报告还原了多少个占位符:

tokenhush: redacted request email (len=14) ****@example.com
tokenhush: restored response placeholders=1

3. curl 的输出里又是原始值。 响应路径在客户端看到 body 之前还原了会话占位符:

{"model":"echo","messages":[{"role":"user","content":"my email is [email protected]"}]}

上游从未见到密钥,客户端从未见到占位符。

网关仍在运行时,查询它的元数据:

终端窗口
tokenhush status --json

这份 JSON 文档包含十个冻结的键(state、addrs、port、uptime_ms、requests、 redactions、content_policy_blocks、rule_blocks、walk_skips、pack_serial)。 发送上面那一个请求之后,redactions 是 1:

"redactions": 1

格式是:

tokenhush: redacted request <type> (len=<N>) <masked>

它只写 stderr,永不落盘。多数类型的 <masked> 是 ****。对于不透明凭据类型 (api_key、high_entropy),它是经过截断的前缀与后缀,例如 sk-p…j0。private_key 命中会报告其 PEM 头类型,例如 RSA PRIVATE KEY;email 命中只报告其域名,例如 ****@example.com。掩码形式永远不会等于密钥:字面量 **** 会渲染成 [redacted]。

该日志默认开启。用 tokenhush run --log-redactions=false 可以静默它。

每一个还原了至少一个会话占位符、发回客户端的响应,都会输出一行计数日志:

tokenhush: restored response placeholders=<N>

缓冲响应输出一行给出总数;流式(SSE)响应每还原一个占位符输出一行。该行只包含 计数,只写 stderr,永不落盘。tokenhush run --log-redactions=false 会把它和脱敏 日志一起静默。

上游是模型的替身。它只收到 __PII_email_<digest>__,所以密钥在发往厂商的请求里 从未离开本机。客户端又拿回了 [email protected],说明占位符在回程上被还原。而且 出站方向永不回填:网关持有正向映射(密钥到占位符),没有反向映射,所以提示注入 试图让模型回显密钥时,无法让网关在出站路上重新填入占位符。这就是你刚刚看到的 往返:密钥离开 body、上游看到占位符、原值回到客户端。

最后更新: