第二部 原理篇:规则驱动的智能体设计

第 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 认证的两个常被忽视的工程点:

原理: 认证的工程本质是"密钥与时效的治理"。无状态令牌把"撤销谁"的问题转移为"令牌多久过期",因此 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 本章小结