你在Bicep文件里精心摆好了私有终点,又给项目配上托管VNET,以为所有流量都老老实实地待在虚拟网络的高墙之内。直到安全审计时才发现,某个毫不起眼的第三方库,早就悄悄连上公网,把你的数据、遥测乃至Agent决策上下文都送出去了。这不是假设,而是任何一个多Agent系统落地时都要直面的“依赖背叛”问题。当你把功能打磨得差不多,企业安全团队一定会把同一个问题甩到你面前:你怎么证明,一个Agent走的网络路径真的可信,它只做自己权限范围内的事,而且半年后当别人问起“这Agent当时干了什么、为什么这么干”,你还能拿出经得起推敲的审计记录?这篇文章不做泛泛而谈的零信任宣讲,只拆三件具体的事——私有网络到底怎么封、自治Agent的身份模型应该怎么变、以及那条扛得住事后追问的审计线索该怎么留。
第一件事:别信你写的代码,要信你的依赖包里没有“内鬼”
默认情况下,你的编排层调用Foundry模型时,走的是公共互联网。流量虽然是加密的,但路由完全暴露在公网上。但凡涉及合规或监管红线的工作负载,这种路径就不及格。于是你会启用Private Link,把流量拽进一个专用端点里:
![]()
resource foundryPrivateEndpoint 'Microsoft.Network/privateEndpoints@2023-04-01' = {name: 'pe-foundry-project'location: locationproperties: {subnet: {id: subnetIdprivateLinkServiceConnections: [name: 'foundry-connection'properties: {privateLinkServiceId: foundryResourceIdgroupIds: ['account']上面这段Bicep把Foundry的“account”组映射到你的子网上,目的很纯粹:让调用链路跟公共互联网说拜拜。可你以为到这里就锁死了吗?远远没有。
你还需要在项目级别启用托管虚拟网络(Managed VNET),强制所有从计算环境发出的出站调用,都留在私有网络边界内。这个计算环境,就是你在Part 7里跑Agent代码的编排层。一旦Managed VNET套上,理论上你的Agent容器再发出去的请求,都只能走私有通道。听起来完美,但真正的破口往往不来自你手写的业务逻辑,而来自你完全无感知的依赖库网络行为。
现代Agent项目拉个依赖列表,动辄上百个包。某个日志库可能内置了对远程服务的回调,把错误堆栈发送给作者;某个模型推理包可能默认连接公网下载配置;甚至一个看似无关的字符串处理库,都可能因为集成了遥测SDK而悄悄发HTTP请求。这些调用完全绕过了你精心设计的私有终点,从Pod或容器内部直接奔向公网。你的网络策略再有条理,也拦不住这种“应用层叛变”。所以,原文才特别强调:出事故的第一个检查点不是自己代码的网络调用,而是去审计所有第三方依赖项的网络行为——看看有没有包在你不知情的情况下做了意外的对外连接。这需要你借助网络抓包、依赖项静态分析或者eBPF级别的运行时监控,把每一个依赖的出站目标都翻出来,而不是只盯着自己的业务代码自欺欺人。
那项目配置上还有什么细节能堵漏?
Private Link配置里,子网的ID必须精确对应到你隔离网络的那个子网,否则后端服务发现就会失败,流量又退回到公网路由。而且groupIds一定要填'account',因为微软Foundry的专用链接是按账户级别暴露的,如果填错,Private Endpoint根本不会对目标资源生效。这些看起来是配置项,实则都是零信任的第一道关口:如果你连资源的正确入口都弄混了,后面的一切身份和审计都建立在沙子上。
第二件事:Agent的身份模型,不该是“一人得道,全家升天”
传统的托管标识(Managed Identity)是为那种定时跑批、行为可预测的服务设计的。一个后台任务,几点几分调一次API,拿一次令牌就够了。但自治Agent完全不同——它要独立做决定,上下文在每一次工具调用、每一次推理链中都可能发生变化。安全模型需要回答的不只是“这次调用有没有通过身份验证”,而是“这一刻、在这个上下文里,这个Agent有没有被授权执行这项具体操作”。
这就是Entra Agent ID被引入的直接原因。它不再给多Agent系统挂一个宽泛的服务主体,让所有Agent共享同样的权限范围,而是要求你为每一个Agent都创建独立的、带作用域的身份,并明确列出它被允许执行的动作列表。看看下面的代码骨架:
from azure.identity import DefaultAzureCredentialclass AgentIdentityContext:def __init__(self, agent_id: str, allowed_actions: list[str]):self.agent_id = agent_idself.allowed_actions = allowed_actionsself.credential = DefaultAzureCredential()def authorize_action(self, action: str, resource_scope: str) -> bool:if action not in self.allowed_actions:log_authorization_denial(self.agent_id, action, resource_scope)return Falsetoken = self.credential.get_token(resource_scope)return token is not Nonedef execute_agent_action(agent_context: AgentIdentityContext, action: str, resource_scope: str, fn):if not agent_context.authorize_action(action, resource_scope):raise PermissionError(f"Agent {agent_context.agent_id} not authorized for {action}")return fn()一眼就能看出关键设计点:AgentIdentityContext在初始化时,就把agent_id和它的allowed_actions绑死了。每一次要执行一个动作,authorize_action先检查这个动作是否在白名单里,不在的话立刻记录一次授权拒绝,并返回False,根本不会去拿令牌。哪怕后续的令牌获取成功,也必须在动作被允许的前提下才有意义。这就把“认证”和“授权”彻底解耦了,而且授权粒度细化到了单个动作级别。
为什么要这样?因为在多Agent链式协作里,一个被提示注入攻击或者自身逻辑出错的Agent,如果共享一个过于宽泛的身份,它就能越界调用不该碰的API。比如你有个专门处理退款的Agent,它的职责仅仅是调用退款接口、查询订单状态。但如果它跟另一个能修改商品价格或删除用户数据的Agent共用一个服务主体,那么一旦退款Agent在运行中被恶意指令诱导,它就可能直接去执行价格变更——因为身份上完全可行。Entra Agent ID的做法是:退款Agent的身份上下文里,allowed_actions只包含refund和query_order,任何其他操作请求都会在authorize_action这一步就被拦下,并且会生成一条明确的授权拒绝日志,供后续审计关联。
这里还有一个容易忽略的安全设计点:令牌获取用的是DefaultAzureCredential,它会按照一定的凭据链去尝试获取身份令牌。这意味着,在开发环境和生产环境里,你可以通过不同的凭据注入方式,让同一个Agent身份在非生产环境里拥有更受限的权限,而不会因为硬编码凭据导致测试环境的配置泄露到线上。每个Agent的独立身份不仅隔离了攻击面,也把环境差异的安全风险兜住了。
那么上下文信息怎么办?resource_scope这一参数不仅仅是拿来获取令牌的受众,它本身也承担着资源级授权的作用。即使一个动作被允许了,目标资源范围也会经由微软Entra ID的条件访问策略或身份保护做进一步的校验。比如退款Agent的resource_scope被限定在某个特定的存储账户或特定区域的数据库,那么它连一条跨区域的查询都会被令牌端点拒绝。这种双重检查,让“在特定上下文里授权特定动作”不再是空话。
第三件事:半年后有人问“这Agent当时干了什么”,你能拿出什么?
原文开头就把审计线索的核心价值挑明了:半年后,当有人追问“这Agent做了什么、为什么那么做”,你的回答不能只靠运维人员的记忆或者散落在日志系统里的模糊记录。审计跟踪需要扛得住质疑,意味着每一条记录都必须是结构化的、不可篡改的,并且能把一次Agent决策的前后上下文全部串起来。
从零信任网络和Agent身份的设计里,自然就能推导出审计跟踪至少要囊括哪些要素:
-
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.