第 12 章:权限、安全与审计:信任边界
12.1 工程问题的本质:多层信任与纵深防御
智能体系统存在多个信任环节:用户身份是否可信、角色能否做此操作、属性是否允许、输入是否被注入、输出是否泄露、操作是否留痕。单一防线失效即可造成损失——因此信任必须分层,任一层的突破不能直接导致系统失守。
设计原理: 纵深防御(defense in depth)在智能体系统的具体形态是:认证 → 授权(RBAC/ABAC)→ 职责分离(SOD)→ 输入输出防线 → 操作级审核链。每一层只回答一个问题,层间不互相依赖——授权层不能因为"认证过了"就放行,审核链不能因为"规则过了"就跳过确认。
12.2 访问控制:角色与属性的双重判定
权限模型的原则:RBAC 决定"能不能做",ABAC 决定"在当前属性下能不能做",两者均通过方可放行。
原理: RBAC(角色访问控制)将权限管理从"逐人授权"抽象为"角色-权限"两层,降低管理复杂度;但角色粒度无法表达情境——同样的角色,金额不同、订单状态不同、所属部门不同,权限应不同。ABAC 补上这一维度:部门属性(销售部不可排产)、金额范围(超 50 万需 admin)、订单状态(已发货不可改)。RBAC 管角色,ABAC 管情境,二者并用是生产系统的标准形态[17]。
角色审批能力矩阵与权限矩阵可训练(存配置),但有一个不可训练项:can_block_override 恒为 False——所有角色(含 admin)均不可绕过硬规则。这是第 6 章第一条约束的权限层落地:权限是授予操作的,不是授予规则的。
12.3 SOD:职责分离的防舞弊原理
SOD 定义不相容权限对:同一用户不可同时拥有"执行"与"审批"同一类操作的能力(接单 vs 审批订单、报工 vs 工资确认、采购 vs 付款审批)。
原理: SOD 针对的不是外部攻击,而是内部舞弊。舞弊成立的充要条件是"一个人能完成闭环"——执行、确认、审批全在自己手里。SOD 通过切断闭环中的关键链路,使任何单点操作都必须引入第二人[25]。因此 SOD 规则是安全铁律:不可被训练修改(防止通过训练给自己开绿灯)、不可被 admin 绕过(防止特权者重建闭环)。
12.4 输入输出防线:四道关卡
智能体与模型交互的防线:提示注入检测(输入侧,阻断"忽略以上指令"类攻击与 SQL 注入)、输出敏感信息过滤(输出侧,致命模式阻断、普通模式打码)、权限与数据访问控制(角色层级 + 数据归属)、操作频率限制(防滥用)。
原理: 输入侧与输出侧防线的不对称性值得注意:输入侧拦截的是"攻击意图",输出侧拦截的是"泄露事实"。前者失败会执行错误操作,后者失败会泄露敏感信息——两者都不可由模型自纠,必须由确定性模式检测兜底。频率限制则把"合法但异常"的滥用(批量拉取、高频探测)挡在资源层之外。
12.5 七层审核链:操作级治理与可证明性
关键操作须经过七层审核方可落库:身份核验 → 权限校验 → 规则校验 → 操作确认 → AI 合规 → 哈希封签 → 归档。任一层 block 即中止。
原理: 审核链不是七个独立的检查,而是一条可证明的链。第 6 层哈希封签使整条链可验证:每条记录包含前一条记录的哈希,任一层被篡改都会破坏后续全部哈希一致性。这带来两个能力:追溯(操作全貌可回放)与证明(操作未被篡改可验证)。可证明性是合规场景的底线——审计不能只"记录了",还必须"证明没被改过"[28]。
操作确认层体现人机协同的粒度:高风险操作(删除、取消、状态变更、额度调整)强制确认;确认词分级(强确认直接触发、弱确认仅独立短回复触发);否定词守卫防止"不要确认"被当作"确认"。
12.6 威胁建模与防御纵深的设计校验
12.6.1 威胁建模:STRIDE 逐类映射
专家级安全设计以威胁建模(threat modeling)为起点:系统性枚举"系统可能被怎样攻击",再逐类映射防线,而非凭感觉堆安全功能。以 STRIDE 分类法逐类核对本框架的防线覆盖[18],见表 12-1:
表 12-1 STRIDE 威胁建模与框架防线
| 威胁类别 | 含义 | 框架防线 | 章节 |
|---|---|---|---|
| Spoofing(伪造身份) | 冒充他人 | 认证(JWT 校验)+ 身份核验层 | 11 / 12.5 |
| Tampering(篡改数据) | 修改审计/规则数据 | 链式哈希防篡改 + 配置审批 | 12.5 / 9 |
| Repudiation(否认操作) | 抵赖做过的事 | 签字留痕 + 操作日志 + 哈希链 | 12.5 / 8 |
| Information Disclosure(泄露) | 敏感信息外泄 | 输出脱敏 + 上下文隔离 + 权限裁剪 | 12.4 / 9.2 |
| Denial of Service(拒绝服务) | 资源耗尽 | 频率限制 + 降级体系 | 12.4 / 8.5 |
| Elevation of Privilege(提权) | 越权操作 | RBAC/ABAC + SOD + 工具级门禁 | 12.2 / 12.3 |
原理: 威胁建模的价值在于"完整性检验"——每类威胁都有明确防线,没有"没想到的攻击面"[19]。本框架的纵深防御正是对 STRIDE 六类的系统覆盖:不是单点防护,而是每类威胁都有"检测 + 阻断 + 留痕"三层响应。
12.6.2 fail-open 与 fail-closed 的决策框架
安全组件失效时的默认行为(放行或阻断)不能拍脑袋,决策框架基于两个问题:失效影响面与错误成本方向。
表 12-2 fail-closed 与 fail-open 的取舍判据
| 判断维度 | 倾向 fail-closed(阻断) | 倾向 fail-open(放行) |
|---|---|---|
| 被保护对象的错误成本 | 假放行成本高(违规落库) | 假阻断成本高(业务中断) |
| 失效可观测性 | 低(静默失败不易发现) | 高(可监控、可补偿) |
| 检测组件角色 | 核心防线(规则、认证) | 辅助护栏(注入检测) |
| 示例 | 规则引擎异常 → block;认证失败 → 拒绝 | 注入检测异常 → 放行并记日志 |
原理: 决策框架的统一表述:失败默认取"错误成本更高的一侧"的对立面。规则引擎假放行成本高 → 失效默认阻断(fail-closed);注入检测若假阻断则拖垮全部业务 → 失效降级放行(fail-open)。任何安全组件的默认行为都应能从错误成本表推导,而非凭经验选择。
12.6.3 认证令牌的工程考量
JWT 认证的两个常被忽视的工程点:
- 密钥治理:弱密钥在生产环境是绕过认证的直通车——框架在非调试模式对弱密钥 fail-closed(登录入口拒绝),防止"为了测试方便"的弱密钥泄漏到生产;
- 撤销与轮换:JWT 无状态(服务端不保存),撤销需黑名单或短时效;框架以短 TTL + 登录重新签发控制撤销窗口,密钥轮换须纳入运维流程。
原理: 认证的工程本质是"密钥与时效的治理"。无状态令牌把"撤销谁"的问题转移为"令牌多久过期",因此 TTL、黑名单、密钥轮换必须成为认证设计的显式组成部分,而非事后补丁。
12.6.4 工具注册与治理(Tool Registry)
智能体驱动业务动作最终落在"调用某个工具/数据动作"(SQL 执行、业务接口、下单、审批推进)。若工具散落各处、各自为战,则"某次 Run 到底调了哪个版本的 SQL、参数里有没有带上 tenant_id"无从回答——审计与回放随之失效。
框架引入工具注册表:所有可被智能体调用的工具统一以 (name, version) 注册,持有 ToolSpec(描述 / 参数 schema / handler);调用前强制按 schema 校验参数,支持版本 pin 与灰度。
设计原理: 工具治理与审批链同一主题——可证明的操作。把工具收敛为"具名、具版本、具 schema"的登记资源后,审计可从一次调用"它想干什么"变"它是什么"(哪个版本)、"参数是否合法"都可由注册表回答;版本 pin 让历史 Run 可复现,避免"工具改了、平台救一次"。这与 12.5 哈希封签一脉相承:操作链可回放、可证明。
失败防范: 注册表是准入门槛而非文档:未注册工具 fail-closed 拒绝调用(不静默放行);schema 校验通过才进入 handler,参数不合法直接 block;版本漂移(运行时工具与其注册版本不一致)显式检测并告警,杜绝"改了 handler 但没改注册表"的静默偏差。
落地示意(框架边界之外,方法论指引):本框架当前尚未内置注册表,以下为落地形态示意,非既有 API。一张登记表即足够起步:
tool_id(PK) /name/version/schema(参数 JSON) /audit_required(是否须经审批链) /status。调用侧每次 Run 把(tool_id, version, params, run_id)一并写审计,审计即可回答"这次操作调用的是哪版工具、参数是否带齐tenant_id"。新增工具走name+version注册、老版本灰度观察后再切默认,与第 13 章审批链同构——工具本身也是"可训练、可审批"的治理对象。
12.7 失败模式与权衡
表 12-3 权限与安全的失败模式与权衡
| 失败形态 | 根因 | 设计应对 |
|---|---|---|
| 权限静默失效 | 配置列名不匹配、加载被吞 | 双键兼容读取 + 加载日志 |
| 单人完成舞弊闭环 | 执行与审批同人 | SOD 铁律 + 流程 Gate 校验 |
| 审计记录被篡改 | 无防篡改机制 | 链式哈希 + 完整性验证 |
| 安全检测拖垮业务 | 检测异常直接报错 | 安全检测异常降级不阻断(可用性优先) |
权衡: 安全与可用性的平衡点:输入侧注入检测异常时降级不阻断业务——攻击检测是"尽力而为"的护栏,业务可用性是底线;而规则引擎异常时默认阻断——业务正确性是底线。两处默认值相反,因为两者的失败成本方向相反。
12.8 实现剖析:RBAC+ABAC 双判定、SOD 与输入防线
本节从 runtime/permission.py(755 行)、runtime/sod.py(120 行)与 runtime/agent_security.py(624 行)各摘一段,印证 12.2 节的双层访问控制、12.3 节的职责分离与 12.4 节的输入防线。
12.8.1 双层判定:RBAC 先问"有没有",ABAC 再问"准不准"
def check_permission(self, user, action, resource=None):
"""权限校验主入口(RBAC + ABAC)"""
resource = resource or {}
role = self._get_user_role(user)
# 第 1 层:RBAC —— 角色是否拥有该 action 的权限
if not self._check_rbac(role, action):
if self._check_temp_override(user, action): # TG-04:临时授权兜底
return True
return False
# SOD 违规仅记录标记(审计追踪),不阻断已有操作
role_perms = self.role_permissions.get(role, {})
for _v in self.sod_checker.check_role_sod(self._get_sod_perms(role_perms)):
_logger.warning("SOD 违规标记 | role=%s | %s: %s + %s 不相容",
role, _v.get("sod_id"), _v.get("permission_a"),
_v.get("permission_b"))
if role == ROLE_ADMIN: # admin 绕过 ABAC 属性约束
return True
# 第 2 层:ABAC —— 在当前资源属性下是否允许
if not self._check_department_constraint(user, action, resource):
return self._check_temp_override(user, action) # 跨部门查询兜底
if not self._check_amount_constraint(user, action, resource):
return False
if not self._check_order_status_constraint(user, action, resource):
return False
return True
这段代码印证 12.2 节:RBAC 与 ABAC 是"串联"而非"并联"的——先过角色闸门(粗粒度:这个角色能不能做这类操作),再过属性闸门(细粒度:在当前部门、金额、订单状态下能不能做)。两者是"与"关系,任一不过即拒。这种分层让权限模型既能用角色做粗管理,又能用属性做细约束,而无需为每种组合定义新角色。
12.8.2 SOD:把"不相容权限对"变成可检查的规则表
class SODChecker:
# ★ 内置 SOD 规则定义(与外部 sod_conflict_rules 表对应)
SOD_RULES = {
"SOD-001": {"perm_a": "can_modify_order", "perm_b": "can_approve_order",
"desc": "销售接单+订单审批不相容"},
"SOD-002": {"perm_a": "can_create_purchase", "perm_b": "can_approve_payment",
"desc": "采购下单+付款审批不相容"},
"SOD-005": {"perm_a": "can_upload_training", "perm_b": "can_approve_training",
"desc": "训练数据上传+审批不相容"},
# ... SOD-003/004/006 略
}
def check_user_sod(self, user_permissions):
"""检查用户权限集是否违反 SOD 规则"""
violations = []
for sod_id, rule in self._rules.items():
has_a = user_permissions.get(rule["perm_a"], False)
has_b = user_permissions.get(rule["perm_b"], False)
if has_a and has_b: # ★ 同时持有 → 违规
violations.append({
"sod_id": sod_id,
"permission_a": rule["perm_a"],
"permission_b": rule["perm_b"],
"description": rule["desc"],
})
return violations
这段代码印证 12.3 节:SOD 的实现形态是"权限对的黑名单"。防舞弊的核心不是"某个人可信不可信",而是"同一主体不应同时握有执行权与审批权"——只要把不相容权限对枚举成规则表,校验就退化为一次集合判断。
规则表可外置到数据库(sod_conflict_rules),意味着内控口径本身可治理;admin 命中全部规则属预期(用于审计标记),说明这一层是"标记 + 审计"而非"阻断"——真正的阻断由链式 Gate 的 SOD 隔离校验完成(第 13 章)。
12.8.3 输入防线:归一化之后再做字面匹配
def check_injection(self, text):
"""检测 prompt / SQL 注入"""
if not text or not isinstance(text, str):
return SecurityResult(result="pass", reason="输入为空,跳过注入检测")
# ★ 空白归一化:防 "delete from" / "drop table" 等多空格变体绕过
text_norm = " ".join(text.lower().split())
matched = []
for pattern in self.prompt_injection_patterns: # 提示词注入模式
if pattern.lower() in text_norm:
matched.append(pattern)
for pattern in self.sql_injection_patterns: # SQL 注入模式
if pattern.lower() in text_norm:
matched.append(pattern)
if matched:
return SecurityResult(
result="blocked",
reason=f"检测到提示词注入攻击:命中 {len(matched)} 个模式",
details={"matched_patterns": matched, "defense": "prompt_injection"})
return SecurityResult(result="pass", reason="未检测到提示词注入")
这段代码印证 12.4 节:输入防线的第一道关卡是"检测",第二道是"净化"。检测(check_injection)在归一化空白后按模式字面匹配,避免攻击者用多空格变体绕过;净化(sanitize_input)则移除命中关键词、把单引号双写转义、剔除控制字符。
二者配合"权限校验"与"输出过滤"构成四道关卡——任何一道 blocked,请求都不会进入生成环节。这也解释了 12.4 节为何强调"防线在生成之前":把攻击面挡在模型调用之外,比事后过滤输出更可靠。
12.9 本章小结
- 信任必须分层,纵深防御的每一层只回答一个问题;
- RBAC 管角色、ABAC 管情境,双重判定方可放行;
- SOD 切断舞弊闭环,因此不可训练、不可绕过;
- 输入输出防线拦截攻击与泄露,频率限制拦截滥用;
- 七层审核链以哈希封签实现可证明性;安全与可用性的默认值因失败成本方向而异。