Agent写库前先签名:零信任写操作怎么做

很多 Agent 系统现在已经能:

改数据库
改CRM
发退款
写工单
更新配置

但真正落到生产以后,一个很难回答的问题是:

数据库里这一行,
到底是谁改的?

如果所有 Agent 都通过同一个连接池、同一个 Service Account 写入,日志里最多只能看到:

application=agent-platform

看不到:

哪个Agent
哪个Run
代表谁
为什么写
写入时参数是什么

Google 最近公开的 ADK 零信任示例里,把“每次状态变更都由具体 Agent 签名”作为第一层硬边界:生产环境可以给 Agent 分配独立 Workload Identity,并通过 Cloud KMS / HSM 保存非对称私钥;Agent 对写入 Payload 做确定性序列化和摘要,再请求 KMS 签名,数据库入口验证通过后才允许提交。

真正值得借鉴的不是某个 Cloud 产品,而是:

状态变更要从“有权限就能写”升级成“身份、内容和授权都能被证明”。

第一层:先定义“被签名的是什么”

危险做法:

只签 amount=149

这不够。

真正应该把完整业务动作纳入 Signature:

{
  "agent_id": "refund-agent",
  "run_id": "run-918",
  "subject_id": "user-82",
  "purpose": "customer-refund",
  "order_id": "order-17",
  "amount": 149.00,
  "currency": "USD",
  "idempotency_key": "refund-order-17-v1",
  "expires_at": "2026-08-27T03:10:00Z"
}

如果只签金额,不签订单号:

签名可能被重放到另一个订单

如果不签 expires_at

旧签名可能长期有效

如果不签 idempotency_key

重复执行更难识别

Payload必须Canonicalize

JSON:

{"a":1,"b":2}

和:

{"b":2,"a":1}

逻辑相同,但字节不同。

如果签名前没有统一序列化,验证端很容易失败。

可以定义:

public interface CanonicalPayloadEncoder {

    byte[] encode(Object payload);
}

规则至少包括:

字段排序
数字格式统一
时间统一UTC
Unicode Normalize
禁止浮点隐式精度变化

金额最好不要直接签:

149.0
149.00

而是转成:

minor units = 14900

我会签Minor Units

public record RefundAction(
        String agentId,
        String runId,
        String orderId,
        long amountMinor,
        String currency,
        String idempotencyKey,
        Instant expiresAt) {
}

这样:

149.00 USD

稳定表示成:

14900

不会出现浮点序列化问题。

第二层:Agent不能持有私钥

最差实现:

AGENT_PRIVATE_KEY

放在:

.env
Kubernetes Secret
Container File

模型如果获得文件读取或环境变量访问能力,私钥就可能泄漏。

更好的:

Agent只有Sign权限
没有Export权限

私钥永远留在:

KMS / HSM

Runtime 只提交摘要。

签名请求也需要授权

不能让任何 Workload 都能调用:

refund-agent-key

KMS Policy:

只有 refund-agent Workload Identity
允许 sign

也就是说:

Agent Identity
→ Dedicated Signing Key

而不是所有 Agent 共用一把 Key。

为什么每个Agent独立Key有价值

如果:

research-agent

拿不到:

refund-agent-key

即使它设法调用数据库接口,也无法生成合法退款签名。

这形成:

Capability Separation

比 Prompt 里写:

“研究Agent禁止退款”

强很多。

第三层:验证必须发生在真正的写边界

签名检查放在 Agent Tool 内部还不够。

例如:

Agent
→ refund_tool
→ verify
→ DB

如果另一个服务可以直接连 DB:

绕过

所以真正验证位置应该尽量靠近:

State Mutation Boundary

例如:

Database Ingress Service
Write API
Stored Procedure
Event Consumer

一个Write Gateway

Agent
↓
Signed Action
↓
Write Gateway
├─ Signature Verify
├─ Expiry
├─ Idempotency
├─ Policy
├─ Resource Ownership
└─ Audit
↓
Database

数据库不直接暴露给 Agent。

第四层:签名不能替代业务Policy

合法签名只能证明:

这个Agent确实发了这个请求

不能证明:

这个请求业务上合理

例如 Refund Agent 合法签了:

$10,000

仍然应该被 Policy 拒绝。

if (action.amountMinor()
        > order.refundableAmountMinor()) {

    return Decision.DENY;
}

所以签名是:

Identity + Integrity

不是:

Business Authorization

第五层:签名必须绑定Authorization Decision

可以在 Action 里继续加入:

grant_id
approval_id
policy_version

例如:

{
  "grant_id": "grant-77",
  "approval_id": "approval-19",
  "policy_version": "refund-policy-v12"
}

Gateway 验证:

签名有效
+
Grant有效
+
Approval有效
+
Policy仍允许

才提交。

第六层:签名要防Replay

攻击者拿到一份完整合法请求:

重复发10次

签名每次都有效。

所以一定需要:

idempotency_key

Gateway 保存:

create table write_idempotency (
    idempotency_key varchar(256) primary key,
    action_hash varchar(64) not null,
    status varchar(32) not null,
    result_ref varchar(512),
    created_at timestamptz not null
);

第一次:

执行

后面同 Key:

返回原结果

如果 Key 相同但 Action Hash 不同:

SECURITY ERROR

第七层:Expiry要短

一个签名动作最好只活:

30秒
2分钟
5分钟

而不是一天。

验证:

if (Instant.now()
        .isAfter(action.expiresAt())) {

    throw new ExpiredActionException();
}

长期审批可以存在更久。

真正执行签名应该短。

第八层:数据库行要保存Signature Evidence

不要验证完就丢。

例如:

create table refund_ledger (
    refund_id varchar(128) primary key,
    order_id varchar(128) not null,
    amount_minor bigint not null,
    agent_id varchar(128) not null,
    run_id varchar(128) not null,
    action_hash varchar(64) not null,
    signature_ref varchar(512) not null,
    key_version varchar(64) not null,
    created_at timestamptz not null
);

以后可以独立做:

Integrity Audit

后台Audit怎么工作

每天抽样或全量:

读取Ledger
↓
重新Canonicalize
↓
计算Hash
↓
验证历史Signature

如果某行被 SQL 直接改过:

Hash对不上

立即报警。

这就是签名真正的价值之一:

数据库本身被修改
也能发现

Key Rotation必须保存Key Version

如果 Key 每90天轮换:

旧记录

仍然需要验证。

所以签名记录不能只存:

key_id

要存:

key_version

历史 Public Key 继续保留用于 Verify。

Key Rotation流程

Create new key version
↓
New writes use v2
↓
Old v1 disable signing
↓
Keep v1 public verify
↓
After retention ends archive

不要直接删除旧 Key。

Agent-to-Agent写操作更要签名

Supervisor:

让 refund-agent 执行退款

最终签名者应该是:

refund-agent

同时保留:

parent_run
delegation_id

这样能还原:

谁提出
谁授权
谁真正写

不要让 Supervisor 用自己的 Key 替所有 Sub-Agent 签。

一个Delegated Action

public record DelegatedWriteAction(
        String agentId,
        String parentAgentId,
        String delegationId,
        String runId,
        String capability,
        String resourceId,
        String actionHash) {
}

Write Signature最适合哪些动作

非常适合:

退款
预算修改
权限修改
数据库关键状态
部署状态
合同审批状态

不一定需要给:

每条Debug日志

都签。

优先覆盖:

高价值、不可逆、合规要求高

的写操作。

最少测试这10种情况

1. Payload字段顺序变化
2. 金额被篡改
3. Order ID被篡改
4. 签名过期
5. 相同Idempotency重复提交
6. 同Key不同Payload
7. Agent使用错误Key
8. Grant撤销后执行
9. Key Rotation后验证旧记录
10. 直接SQL修改Ledger后Audit报警

签名解决不了什么

它解决不了:

模型为什么做出错误决定

也解决不了:

业务Policy写错

它能提供的是:

确定是谁
确定签了什么
确定内容没被改
确定历史可验证

这已经是生产 Agent 非常重要的一层。


Agent 有写权限以后,真正成熟的系统不能只依赖:

数据库账号有权限

而应该继续建立:

Workload Identity
+
Action Signature
+
Policy
+
Approval
+
Idempotency
+
Immutable Audit

这样即使模型被诱导、服务被误配或数据库记录被直接修改,系统仍然有硬边界和证据。

Prompt 可以告诉 Agent:

“不要乱写。”

签名和 Gateway 则能证明:

这次到底是谁写的、写了什么、是否经过授权。

这两个层次完全不是一回事。


更多企业级 AI 应用、Agent、RAG 与模型工程化内容,我会继续整理在 智元界

https://www.zyentor.com/