逐日AI
第 1 周 · D4约 5 小时

运行时管控:能力面、确认门、出口白名单、沙箱分层,以及密钥与租户隔离

假设注入已经发生,靠运行时把损失锁住:给每个工具声明能力面与危险等级,给高危动作加确认门,给文件与网络加白名单,再理清策略层与隔离层的分工,最后处理密钥、用户级凭据与多租户隔离。

今日目标 0/3

登录后可以勾选并保存进度。

今日目标

  1. 能给一批工具写出能力面与危险等级声明,并据此决定哪些动作必须过确认门
  2. 能实现路径白名单与网络出口白名单,并说清它们各自挡住了哪条外泄路径
  3. 能区分策略层与隔离层的职责,并说清多租户场景下凭据与日志该怎么隔离

前三天都在跟「坏话别进来」较劲:画信任边界、做输入侧防御、再用架构把不可信内容和有权限的那次调用隔开。今天换一个假设——注入已经发生了,模型已经打定主意要把客户名单发出去。读完回到页面顶部把三条目标勾掉。

小白版讲解

柜员的权限条:能做什么,是写在制度里的

去银行办事你会注意到一件事:柜员能查你的余额,能办小额转账,一旦金额上去,他就得起身去叫主管。有意思的地方在于,这套规矩不在柜员脑子里,它写在制度里、焊在柜台系统上。柜员今天心情好、被客户说服了、或者干脆被骗了,都不影响大额转账必须有第二个人点头这件事。制度不假设柜员永远清醒,它假设柜员总有不清醒的那一天。

Agent 的工具执行器就是这套柜台系统。前三天的防御都在跟「让模型保持清醒」较劲,它们都该做,但有一个共同前提:总有一次会失手。所以今天的立场变了:假设注入已经发生。deskmate 读完那条工单,现在打算查客户库、把结果发到 collector@attacker.example、再敲一个 https://169.254.169.254/latest/meta-data/。这一层的任务不是识破它,是让它做不成

这件事之所以可能,是因为工具执行器有模型没有的一样东西:它知道每个工具究竟能干什么。模型只看见工具名和描述,执行器看见的是真实的副作用——碰不碰私有数据、会不会产生对外流量、做错了能不能撤回。这三件事全是结构性事实,和那条工单里写了什么一个字都没关系。

于是柜台制度的三条能落地成三道闸:先写下每个工具能做什么,再决定哪些动作必须有人点头,最后管住数据能去哪。 但每一道闸真要动手时都会立刻卡住:能力面这张表谁来维护,加了新工具没人改怎么办?点头的那个人到底该站在哪一层?以及最要命的一条——你以为数据只能去白名单里那几个地方,攻击者总能找到你没登记过的那条边。 下面挨个拆。

能力面:危险等级是算出来的,不是人工拍的

给工具声明能力面,说白了就是回答三个问题:它是读还是写、它碰不碰私有数据、它做错了能不能撤回。 靶场里的六个工具照这三问填一遍,read_ticket 是「读、内部、可逆」,send_email 是「发、内部、不可逆」,search_customers 是「读、私有、可逆」。

关键是下一步:危险等级从这三个字段算出来,而不是再手工写一张「高危工具清单」。

capabilities.js
// 三个字段推一个等级。加新工具时,它自动落进正确的那一档。
export function riskTier(tool) {
  if (tool.effect === 'send' && !tool.reversible) return 'dangerous'
  if (tool.effect === 'send' || tool.effect === 'write') return 'sensitive'
  if (tool.sensitivity === 'private') return 'sensitive'
  return 'safe'
}

差别看起来只是写法,实际是失效方式完全不同。人工清单的失效是静默的:三个月后有人加了一个 export_report 工具,没人想起来去改那张清单,于是它默认落进最宽松的一档,而 CI 全绿、代码审查通过、没有任何一个地方会报错。算出来的等级没有这个漏洞——新工具必须填那三个字段才编译得过,填完等级自动就有了。

这也是为什么能力面要写成数据而不是散落的 if 判断:它是一张能被打印、被审计、被 diff 的表。某次提交把一个工具从「可逆」改成「不可逆」,代码审查里躲不掉。安全属性只有变成能被看见的东西,才有人会去看它。

确认门:必须拦在服务端的工具执行器里

人在回路(human in the loop)几乎是所有 Agent 产品的标配,但这道门放在哪一层,决定了它是真门还是一块装饰

最常见的错法是把它放在前端:模型说要发邮件,界面弹一个对话框,用户点确认才发。演示里没问题,真实攻击面前有两个致命缺口。第一,前端拦不住已经在服务端跑起来的运行——那次工具调用由服务端循环发起,弹框不过是事后通知。第二,前端拦不住改过的请求:那次带 confirmed: true 的请求能被原样重放。

正确的位置只有一个:服务端的工具执行器内部,和权限判定在同一个函数里。裁决出 confirm 之后运行就地挂起,等外部送回批准信号才继续,而信号走会话、不走模型的输出。

出口控制:先判目的地,再判要不要人点头

致命三件套里最值得拆的是「对外通信」那条边,理由很实际:攻击者必须用到它,而正常业务只用到很少几个目的地。 内部助手一天到晚发邮件,但收件人不是同事就是内部系统;它调 webhook,目标也就那一两个内部地址。这种「攻击者需要的自由度远大于业务需要的自由度」的地方,就是白名单最划算的地方。

两类目的地各一张白名单,默认拒绝。默认放行的白名单不叫白名单。

egress.js
export const ALLOWED_EMAIL_DOMAINS = ['deskmate.internal']
export const ALLOWED_HOSTS = ['hooks.deskmate.internal']
 
// RFC 1918 私网、回环与链路本地地址。云上元数据端点躲在 169.254 那一段里。
const BLOCKED_IP_RE = /^(10\.|127\.|169\.254\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/
 
export function checkEmail(address) {
  const domain = address.split('@')[1]?.toLowerCase() ?? ''
  if (!domain) return { allowed: false, reason: `不是一个邮箱地址:${address}` }
  // 精确匹配,不是后缀匹配。理由见下面那个 Callout。
  if (!ALLOWED_EMAIL_DOMAINS.includes(domain)) {
    return { allowed: false, reason: `收件域名不在白名单里:${domain}` }
  }
  return { allowed: true, reason: '' }
}
 
export function checkUrl(raw) {
  let url
  try {
    url = new URL(raw)
  } catch {
    return { allowed: false, reason: `不是一个合法地址:${raw}` }
  }
  if (url.protocol !== 'https:') return { allowed: false, reason: `只允许 https:${url.protocol}` }
  if (BLOCKED_IP_RE.test(url.hostname)) {
    return { allowed: false, reason: `目标是内网或回环地址:${url.hostname}` }
  }
  if (!ALLOWED_HOSTS.includes(url.hostname)) {
    return { allowed: false, reason: `目标主机不在白名单里:${url.hostname}` }
  }
  return { allowed: true, reason: '' }
}

网络这一条还挡住了 SSRF(服务端请求伪造)。SSRF 在 Agent 里的样子特别朴素:让它替你去敲一个你自己敲不到的地址。 169.254.169.254 是云上的元数据端点,公网打不着,但从 Agent 所在的机器上一敲就有回应,回应里往往就是这台机器的临时凭据。所以内网段和回环要单独拦一道——哪怕主机白名单在后面兜着,先拦这一道能在审计日志里留下「有人试过打元数据端点」这条更值钱的记录。

现在把三道闸串起来,顺序是这一天最不能记错的东西

engine.js
export function evaluate(action, ctx) {
  const spec = lookup(action.tool)
  // 1. 认不出的工具一律拒绝。默认拒绝才是白名单。
  if (!spec) return { decision: 'deny', reason: `未登记的工具:${action.tool}` }
 
  // 2. 先判目的地。到不了的地方,根本不该拿去问人。
  if (spec.effect === 'send') {
    const target = action.target ?? ''
    const verdict = target.startsWith('http') ? checkUrl(target) : checkEmail(target)
    if (!verdict.allowed) return { decision: 'deny', reason: verdict.reason }
  }
 
  // 3. 租户:只认会话里带来的范围。
  if (spec.sensitivity === 'private' && !ctx.allowedTenants.includes(ctx.tenantId)) {
    return { decision: 'deny', reason: `租户 ${ctx.tenantId} 不在本次会话的可访问范围内` }
  }
 
  // 4. 最后才轮到确认门。
  if (riskTier(spec) === 'dangerous') {
    return { decision: 'confirm', reason: '不可逆的对外动作,需要人确认' }
  }
  return { decision: 'allow', reason: `危险等级 ${riskTier(spec)}` }
}

把第 2 步和第 4 步换个位置,代码照样跑、测试照样绿,但语义塌了一半:发给攻击者的那封邮件会变成一个「等人点头」的动作,而不是一个被直接拒绝的动作。你等于在训练人为一个本来就该拒的动作按同意。 今天的实验里 #1(发给攻击者)与 #3(发给风控)拒绝理由不同,演示的正是这一条——前者压根没走到确认门。

沙箱的两层:你自己写的,和内核替你兜的

「加个沙箱」这句话在安全讨论里出现频率极高,但它指的往往是两件完全不同的事,混着说必然出事。

策略层是你自己写的代码:能力面、危险等级、确认门、路径与出口白名单、审计日志——今天写的这一整套都在这一层。它的位置在动作发生之前,管的是「这次调用允不允许发生」。

隔离层是别人替你兜底的机制:容器、虚拟机、gVisor、seccomp,以及 OS 级的沙箱工具。它的位置在动作发生之后,管的是「已经跑起来的那个进程还能碰到什么」。它不理解你的业务,只知道这个进程读不到那个目录、连不上那个网段。

一句话记住分工:策略层管意图,隔离层管爆炸半径。 谁也替代不了谁——只有隔离层,被劫持的 Agent 会在授权范围内合法地把客户名单发给攻击者,容器一点都不会拦;只有策略层,一个从工具实现里逃出去的代码执行漏洞就直通宿主机。今天的实验只做策略层,因为它必须离线可跑、不能要求你先装 docker。

沙箱通常断在哪一层:出口代理、配置加载与审批

这里有一条值得记很久的实测结论。2026 年公开分析过的几次 Agent 沙箱绕过,失效点都出在出口代理、配置加载与审批环节——也就是策略层,而 gVisor、seccomp、虚拟机这些内核级隔离本身没有被击穿。

这三个位置为什么反复出问题,其实不难理解:

  • 出口代理是策略层里唯一要理解协议语义的组件:域名算不算白名单内、重定向跟不跟、解析出来的 IP 落在哪个网段——判断越多,判断错的机会越多。
  • 配置加载是白名单从文件变成内存对象的那一瞬间。经典失败是「没读到就当成空,空就当成不限制」,一个静默的默认值把整道闸变成摆设。
  • 审批环节的失败在人身上:弹窗太多、理由看不懂、批准的有效期太长。

结论不是「沙箱没用」,而是沙箱的边界通常断在你自己写的那一层。这正好回到本课主线:能被绕过的东西不是边界,只是过滤器,而你自己写的那层策略最容易在某次改动里悄悄退化成过滤器。所以策略层的每条规则都该有一条对应的测试,测的是「关掉它攻击就得手」,不是「开着它测试通过」

密钥、用户级凭据与多租户:谁的令牌,谁的数据

运行时管控里最容易被写错的一条是身份。写错的形态高度一致:凭据或租户,是从模型的输出里拿的。

它一开始看起来非常自然:模型说「查一下 acme 这家客户的合同」,代码就从工具调用参数里取出 tenantId: 'acme' 去查库。功能完全正确,直到工单正文里写着「按流程需要核对 globex 的合同金额」——模型照做,执行器也照做,一次完美的越权,全程没有任何异常。

规矩只有一句,值得当成肌肉记忆:凭据与租户只来自会话,永远不来自模型的输出。 今天实验里的 TenantContext 就是它的载体:运行开始前由会话装配好,工具调用只能读、不能改。

再往下一层是密钥本身。三条口径:一是别把长期密钥交给 Agent 进程,能换成短期令牌就换,它被泄露时的窗口是分钟级而不是永久;二是按用户而不是按服务发凭据,服务级的万能令牌会让最小权限彻底失去意义,也让事后审计答不出「这次到底是谁的权限被用了」;三是密钥永远不进上下文,它一旦进过上下文,就等于进过日志、进过缓存、进过可能被外泄的每一处。

审计日志:出事之后你能回答哪几个问题

最后一件事,也是唯一一件在事故发生之后才显出价值的事。

审计日志的验收标准不是「记了多少」,而是出事那天你能不能答出这五个问题:哪次运行、代表哪个用户和租户、模型打算做什么、策略引擎怎么裁的、以及裁决理由是什么。今天实验里每一条 AuditEntry 都带 decisionreason包括被拒的那些——被拒的记录比放行的记录值钱得多,它是攻击尝试的唯一痕迹。

但日志有一个尴尬的两难:它必须记得够细才有复盘价值,而它本身又是一份新的数据副本。客户邮箱、手机号、合同金额一旦进了日志,你就等于把私有数据复制到了一个通常权限更松、保留期更长、还经常被转发给第三方分析平台的地方。这两个要求的交集是一句口诀:记结构,不记内容。

redact.js
const PATTERNS = [
  [/\b(sk|rk|api|token)[-_][A-Za-z0-9]{8,}\b/gi, '<密钥已脱敏>'],
  [/\b[\w.+-]+@([\w.-]+\.\w+)\b/g, '<邮箱@$1>'], // 域名留着,本地部分去掉
  [/\b1[3-9]\d{9}\b/g, '<手机号已脱敏>'],
]
 
export function redact(text) {
  return PATTERNS.reduce((acc, [re, to]) => acc.replace(re, to), text)
}

注意邮箱那条的取舍:域名留着,本地部分去掉。 复盘要回答的是「数据想去哪家」,attacker.example 必须留;收件人叫什么是个人数据,留着只增加风险不增加信息。脱敏是这样一条条权衡出来的,不是「像敏感数据的都打码」——打得太狠,日志就退化成一堆占位符,出事那天你什么也答不出来。

源码导读

动手实验

🧪 D4 实验:沙箱化的工具执行器:能力面声明加策略引擎加出口白名单加审计日志,带一份可逐条打勾的检查清单

代码位置:labs/agent-security-5days/day-04-sandboxed-executor

验收标准:

  1. 能力面表格打印出来,六个工具的危险等级由 riskTier() 算出而不是写死
  2. 一条被劫持的动作序列里,#1 因为收件域名不在白名单被拒,#3 因为等待确认未获批准被拒——两条理由不一样
  3. #2 的目标 169.254.169.254 被判为内网或回环地址而拒绝
  4. 审计日志里的邮箱被脱敏成保留域名的形式,decisionreason 对每一条裁决都在,包括被拒的
  5. ALLOWED_EMAIL_DOMAINS 加上攻击者域名重跑,外泄立刻得手(变异检验)

动手之前确认两件事:实验完全离线,不需要模型也不需要网络;它是一次性脚本,pnpm start 跑完打印表格就退出,验收判据是看到表格内容,不是退出码是 0。卡住了先读 policy/engine.ts 顶部的注释,四步顺序的理由全写在那里。

  1. 先在 solution 里跑一遍,盯住那条被劫持的序列如何依次撞上危险等级、确认门与出口白名单,记下 #1#3 的理由差异。
  2. 回到 starter 补全 policy/capabilities.ts 的能力面声明与 riskTier(),重跑确认六个工具各自落进正确的一档。
  3. 补全 policy/engine.tsevaluate():按四步顺序裁出放行、要确认还是直接拒绝,并让每次裁决都带上能读懂的理由。
  4. 补全 policy/egress.tscheckEmail()checkUrl(),拦住发往未登记域名的邮件和指向内网地址的请求。
  5. 补全 runtime/executor.ts 的审计写入与脱敏,确认密钥与客户联系方式不会原样进日志。
  6. 做变异检验:把出口白名单加上攻击者域名重跑,看外泄用例重新得手;再把确认门判断挪到出口检查前面,观察 #1 的理由从「域名不在白名单」变成「需要人确认」。

面试题

今天 3 道题在下方题库区,侧重最小权限与能力面设计、确认门的放置位置、沙箱两层分工,以及密钥与多租户隔离。展开后先看「分析过程」再看要点——照着推导练,比背要点管用。标注「国内高频 / 海外高频」方便按目标市场取舍。

检查清单与明日预告

  • 能给一批工具写出能力面与危险等级声明,并据此决定哪些动作必须过确认门
  • 能实现路径白名单与网络出口白名单,并说清它们各自挡住了哪条外泄路径
  • 能区分策略层与隔离层的职责,并说清多租户场景下凭据与日志该怎么隔离
  • 能说清判定顺序为什么是「先判目的地,再判要不要人点头」,换过来会坏在哪
  • 能说出白名单为什么必须精确匹配域名,后缀匹配会被什么样的地址骗过去
  • 实验的 5 条验收标准全部通过,包括那条关掉白名单就得手的变异检验
  • 3 道面试题不看要点也能答出至少 2 道

明天 D5 是最后一天《红队与演练:攻击用例集怎么长期维护、怎么进 CI,以及出事之后怎么办》:把这四天的防御变成一条能长期跑的循环——用例集怎么维护、红队脚本怎么出报告、怎么用攻击成功率与任务完成率双阈值卡进 CI,以及失控检测、断路开关与事故响应。顺序是有意的:没有今天这套可裁决、可复盘的运行时,红队跑出来的结论就无处落地——你会知道被打穿了,却说不出是哪道闸没关上。

面试题库

  • 人在回路的确认门,为什么必须放在服务端的工具执行器里?放在前端会出什么事?Why must a human-in-the-loop confirmation gate live inside the server-side tool executor? What breaks if you put it in the frontend?
    国内高频海外高频进阶#human-in-the-loop#tool-execution#trust-boundary

    分析过程 · 先想清楚再作答

    1. 这题的题眼是「拦截点在不在动作发生的那条路径上」。答成「前端体验不好」就跑偏了,面试官想听的是信任边界。
    2. 先问自己一个问题:这次工具调用是谁发起的?是服务端的 Agent 循环。那么前端的弹框就只是一次事后通知,它不在调用路径上,自然拦不住已经跑起来的运行。
    3. 第二条攻击面是请求可改:带着「已确认」标记的那次请求能被原样重放,客户端传来的任何断言都不能当授权用。这和「不要在前端做权限校验」是同一条老规矩。
    4. 结论:确认门要和策略判定在同一个函数里,裁决出「需要确认」之后运行就地挂起,批准信号走会话而不是走模型输出或请求体里的标记。
    5. 顺带给一条设计判据:判定顺序上,先判目的地再判要不要人点头。一个本来就该被拒的动作不该拿去问人,否则你是在训练人麻木地按同意。
    6. 可预期的追问:确认门会不会被用滥?答案是只有不可逆的对外动作那一档才过门,其余靠白名单与审计兜;再补一句批准要有有效期,不能点一次头就对整个会话生效。

    How to reason about it · think before answering

    1. The hinge is whether the interception point sits on the path where the action actually happens. Answering in terms of UX misses the question, which is about trust boundaries.
    2. Ask who initiates the tool call: the server-side agent loop. A frontend dialog is therefore an after-the-fact notification, off the call path, and cannot stop a run that is already executing.
    3. The second gap is request tampering: a request carrying a confirmed flag can simply be replayed. No assertion supplied by the client can ever serve as authorization, which is the same old rule as never enforcing permissions in the browser.
    4. Conclusion: the gate belongs in the same function as policy evaluation. On a confirm verdict the run suspends, and the approval arrives through the session, never through the model output or a flag in the request body.
    5. Add a design rule: check the destination before asking for human approval. An action that should be denied outright must never be shown to a human, or you are training people to click approve reflexively.
    6. Expected follow-up: does the gate get overused? Only the irreversible outbound tier goes through it; everything else is covered by allowlists and audit logs. Approvals also need a short lifetime rather than lasting for the whole session.

    答题要点

    • 拦截点必须在动作发生的那条路径上,前端弹框不在路径上,只是事后通知
    • 客户端送来的「已确认」标记可被重放或伪造,任何来自客户端的断言都不是授权
    • 确认门与策略判定同一处:裁出需要确认则运行挂起,批准信号来自会话
    • 先判目的地再判要不要人点头,该拒的动作不拿去问人,避免确认疲劳
    • 只有不可逆的对外动作过门,批准要有有效期,不能一次点头覆盖整个会话

    Key points

    • The interception point must sit on the path where the action executes; a frontend dialog is only a notification
    • A confirmed flag from the client can be replayed or forged, so no client assertion counts as authorization
    • Put the gate next to policy evaluation: suspend the run on a confirm verdict and take approval from the session
    • Check the destination first and only then ask a human, so actions that should be denied never reach a person
    • Only irreversible outbound actions go through the gate, and approvals expire instead of covering a whole session
  • 网络出口白名单能挡住哪些数据外泄路径?哪些是它挡不住的?Which data exfiltration paths does a network egress allowlist actually block, and which ones does it miss?
    国内高频海外高频深入#egress-control#ssrf#data-exfiltration

    分析过程 · 先想清楚再作答

    1. 这题考的是边界意识:能说清一个防御「挡不住什么」,比背下它挡得住什么更有区分度。只说前半截的人,通常没在生产里被绕过过。
    2. 先说它为什么划算:致命三件套里「对外通信」这条边上,攻击者需要的自由度远大于业务需要的自由度——业务的收件人就那几个内部地址,白名单的成本因此极低。
    3. 挡得住的部分:往未登记域名发邮件、往未登记主机发 webhook、以及让 Agent 去敲内网与云上元数据端点这类 SSRF。最后一条尤其值钱,元数据端点里往往就是这台机器的临时凭据。
    4. 挡不住的部分要老老实实列:数据写进一个合法目的地再由别人取走(把名单写回工单、提交到允许的仓库)、通过允许的目的地做隐蔽信道(把内容编码进 URL 路径或子域名查询里)、以及模型直接把私有数据说给当前这个本来就有权看结果的用户。
    5. 还有一类实现层的坑:白名单写成后缀匹配,攻击者用一个以你的域名结尾的子域就能骗过去;以及只校验域名不校验重定向与 DNS 解析结果。
    6. 可预期的追问:那怎么补?答案是配合出口代理集中流量、对允许的目的地也限制载荷形状与体积、再加上审计日志看异常的目的地与频率——但根本解法仍然是拆三件套里的另一条边,比如让这次运行根本拿不到私有数据。

    How to reason about it · think before answering

    1. This question tests boundary awareness. Being able to say what a control does not stop is a stronger signal than reciting what it does.
    2. Start with why it is cheap: on the outbound edge of the lethal trifecta the attacker needs far more freedom than the business does, because legitimate recipients are a handful of internal addresses.
    3. What it blocks: mail to unregistered domains, webhooks to unregistered hosts, and SSRF-style requests to internal ranges or cloud metadata endpoints, which often hand out short-lived machine credentials.
    4. What it misses, stated honestly: writing the data into an allowed destination that the attacker can later read, covert channels through an allowed destination by encoding content into a path or subdomain query, and the model simply telling the private data to the user who is already in front of it.
    5. There are implementation traps too: suffix matching lets an attacker-controlled subdomain that ends with your domain slip through, and validating only the hostname while ignoring redirects and DNS resolution.
    6. Expected follow-up: how do you cover the gap? Route traffic through an egress proxy, constrain payload shape and size even for allowed destinations, and watch audit logs for unusual destinations and rates. The real fix is still to cut another edge of the trifecta, such as denying this run access to private data at all.

    答题要点

    • 挡得住:发往未登记域名的邮件与 webhook,以及指向内网与云上元数据端点的 SSRF 请求
    • 挡不住:写入合法目的地后由他人取走,以及把内容编码进允许目的地的路径或子域的隐蔽信道
    • 挡不住:模型把私有数据直接说给当前这个已经有权看结果的用户
    • 实现上必须精确匹配整个域名,后缀匹配会被以你的域名结尾的子域骗过
    • 补法是出口代理集中流量、限制载荷形状与体积、审计异常目的地,根本解法是拆三件套的另一条边

    Key points

    • Blocks mail and webhooks to unregistered destinations, plus SSRF to private ranges and cloud metadata endpoints
    • Misses data parked in an allowed destination for later pickup, and covert channels encoded into allowed paths or subdomains
    • Misses the model simply telling private data to the user who already has the result in front of them
    • Match the full domain exactly; suffix matching is defeated by an attacker subdomain ending in your domain
    • Complement it with an egress proxy, payload limits and audit review, but the real fix is cutting another trifecta edge
  • 一个多租户的 Agent 服务里,凭据和日志分别该怎么隔离?In a multi-tenant agent service, how should credentials and logs be isolated?
    国内高频海外高频进阶#multi-tenancy#secrets-management#audit-logging

    分析过程 · 先想清楚再作答

    1. 这题在考你有没有真做过多租户。区分度在一句话上:租户身份到底从哪来——从会话来还是从模型的输出里来。
    2. 先讲凭据。最常见的错法是从工具调用参数里取租户 id,因为模型说要查哪家就查哪家;这条路被一句注入就能走通,而且功能表现完全正常,没有任何报错。规矩是凭据与租户只来自会话,模型可以提要求,但代表谁这件事不归它决定。
    3. 顺着往下是密钥本身的三条:别把长期密钥交给 Agent 进程,能换短期令牌就换;按用户发凭据而不是给服务一把万能令牌,否则最小权限和事后审计同时失效;密钥永远不进上下文,进过上下文等于进过日志与缓存。
    4. 再讲日志。它的两难是必须记得够细才有复盘价值,同时它又是一份新的数据副本,权限通常更松、保留期更长。交集是「记结构不记内容」:记运行 id、用户与租户、工具名、裁决与理由,不记客户联系方式与合同金额。
    5. 脱敏要按信息价值权衡而不是一刀切。邮箱保留域名去掉本地部分就是个好例子:复盘要回答的是数据想去哪家,收件人叫什么不增加信息只增加风险。
    6. 可预期的追问:日志本身怎么隔离?按租户分区存储、查询接口强制带租户条件、跨租户的聚合视图只给脱敏后的统计;再补一句被拒的记录也要留,它是攻击尝试的唯一痕迹。

    How to reason about it · think before answering

    1. This question separates people who have actually run multi-tenant systems. The hinge is one sentence: where does tenant identity come from, the session or the model output.
    2. Credentials first. The classic mistake is reading the tenant id out of tool-call arguments because the model asked for that customer. One injected line walks straight through it, and nothing looks broken. The rule is that credentials and tenant come only from the session; the model may request work but never decides who it acts as.
    3. Three rules follow for the secrets themselves: do not hand long-lived keys to the agent process when short-lived tokens will do; issue per-user credentials instead of one service-wide key, which would kill both least privilege and after-the-fact audit; and never let a secret enter the context window, since that means it entered the logs and caches too.
    4. Now logs. The tension is that they must be detailed enough for forensics while being a fresh copy of the data, usually with looser access and longer retention. The intersection is to record structure, not content: run id, user and tenant, tool name, decision and reason, but not contact details or contract amounts.
    5. Redaction should be weighed by information value rather than applied bluntly. Keeping the email domain while dropping the local part is the good example: forensics needs to know where the data was headed, while the recipient name adds risk and no information.
    6. Expected follow-up: how do you isolate the logs themselves? Partition storage by tenant, force a tenant predicate on every query path, and expose only redacted aggregates across tenants. Also keep denied entries, since they are the only trace an attempted attack leaves.

    答题要点

    • 租户与凭据只来自会话,绝不从模型输出或工具调用参数里取
    • 用短期令牌替代长期密钥,按用户而不是按服务发凭据,密钥永不进上下文
    • 日志口径是记结构不记内容:运行、用户、租户、工具、裁决与理由要全,客户数据不要
    • 脱敏按信息价值权衡,例如邮箱保留域名去掉本地部分,复盘要的是数据想去哪家
    • 日志本身按租户分区、查询强制带租户条件,被拒的记录必须保留

    Key points

    • Tenant and credentials come only from the session, never from model output or tool-call arguments
    • Prefer short-lived tokens over long-lived keys, issue per-user credentials, and keep secrets out of the context window
    • Log structure, not content: run, user, tenant, tool, decision and reason in full, customer data out
    • Redact by information value, for example keep the email domain and drop the local part, since forensics needs the destination
    • Partition logs per tenant, force a tenant predicate on queries, and always retain denied entries

评论