最小权限落地难:凭据发下去容易,收窄和撤销为什么总是做不干净

2026-07-29

数据截至 2026-07,各产品的额度与报错口径以官方最新说明为准。

多数人把最小权限做失败归因为”限制太多影响效率”,真实原因是凭据从一开始就没有边界,后面所有的收窄都变成了打补丁。 一把 key 建的时候就是全权限,发给三个人、抄进两个 CI、再被某个本地工具缓存了一份,等你想”只让 agent 读某个目录”的时候,你要收窄的对象根本不是一个东西,而是六份散落的副本。于是你收一处坏一处,最后干脆放弃,回到”全给”。最小权限不是一个开关,它是三个必须同时成立的性质:每一把凭据有明确的主人和用途、每一把凭据的作用域是从任务倒推出来的、每一把凭据能在不惊动其他系统的前提下单独作废。三条缺一条,这件事就落不了地。

站内有两篇相邻的文章,分工不同:给 agent 全放开权限之后是事故篇,讲权限已经出事之后怎么止血、怎么按可逆性分档;API Key 怎么管才不泄漏讲的是凭据别被自己交出去的那些朴素纪律。本篇管的是中间那段工程:凭据怎么发、作用域怎么裁、撤销链路怎么建,以及这三步各自会以什么现象卡住你。

一、先定位:你卡在下发、收窄,还是撤销

这三环的症状很像——都表现为”agent 干不了活”或者”限制形同虚设”——但处置动作完全不同,错判一次就是白折腾半天。先看表:

现象大概率成因怎么验证处置动作
agent 每一步都要人点确认,频繁被打断作用域是按工具类型切的,不是按资源切的把最近弹出确认的请求按”被确认的对象”归类,看是否集中在少数几个目录或几个接口,且大多是只读对高频且只读的对象放宽到目录级,写操作保持逐次确认
收窄之后任务直接跑不通,服务端返回 403凭据本身有效,但作用域把这条路径一起砍了用同一把凭据手工发一个最小的只读请求,只看返回码:仍是 403 就坐实是作用域砍过头;变成 401 说明凭据本身有问题,改看下一行403 就单独补回这一类读权限,别整体回滚到全权
同样的收窄动作,服务端返回 401凭据没送进进程,或者已经过期/被轮换掉在目标进程的环境里确认变量存在(只看是否为空,不打印值):为空就是没送进去;不为空就去签发方查这把凭据当前是否还有效前者重新注入,后者重新签发,两者都与作用域无关
撤销之后旧会话还能继续干活长会话持有的令牌未随撤销失效,或本地有缓存副本重启进程后用同样操作复测一次:重启后失败,说明是长会话攥着旧令牌;重启后仍然成功,说明本地还有缓存副本,或者签发方那一侧根本没撤干净前者把重启写进撤销步骤,后者顺着缓存路径和签发方逐一清
换了新凭据,监控里老凭据还在被调用凭据存在多处副本(CI 变量、本机 shell 配置、镜像层、同事本地)按调用来源 IP/客户端标识反查是哪一处先把副本清单列全再撤,否则撤一次挂一片
一撤销就有别的系统跟着挂一把凭据被多方共用,主人不唯一撤之前先查一段时间的调用日志,看这把凭据的来源标识是否超过一个;已经撤了就先恢复再查先按用途拆成多把,各自切换完再逐把撤

判别的关键是分清 401 和 403 这两类语义:401 是”我不认识你”,问题在凭据本身;403 是”我认识你但不许你干这个”,问题在作用域。很多人看到报错就一把回滚成全权限,等于把两类问题一起掩盖了。429 是另一回事,那是速率或额度层面的限制,跟权限设计无关,别混进这条排查线。

二、下发:让每一把凭据都有主人、有用途、有寿命

绝大部分收窄失败,根源在下发阶段就埋好了。判断你的下发是否合格,就问三个问题:这把凭据是给谁用的(人/服务/某个 agent 会话)?它只干哪一类事?它什么时候自然失效?三个问题里有一个答不上来,这把凭据就不具备被单独撤销的能力。

具体做法上,我的判断是先按”消费者”而不是按”环境”切。很多团队习惯按 dev/staging/prod 发三把,看起来干净,实际上 dev 那一把会被本机脚本、CI、临时调试、agent 一起用,出事根本不知道该撤谁。按消费者切之后,一个 agent 工作流一把、一条流水线一把、一个人的本机一把,撤销的时候影响面是可预测的。

发下去的形式也决定了后面撤不撤得掉。几条我一直坚持的:

  • 凭据只以环境变量形式注入进程,不落进任何被版本控制跟踪的文件。检查方法很直接,在仓库里找一下有没有误提交:git ls-files | grep -iE '\.env|credential|secret',有输出就先处理。
  • 需要写进本地文件时,用一个仓库外的路径,并且把权限收到只有当前用户可读:chmod 600 ~/.config/myapp/creds。放进仓库再靠 .gitignore 挡的做法,迟早会因为某次 git add -f 或者换台机器而失效。
  • 注入之后要能验证”送到了”,但不能把值打出来。用这种方式确认存在性即可:
python -c "import os; k='MY_API_KEY'; v=os.environ.get(k); print(k, '已注入' if v else '缺失', '长度', len(v or ''))"

打长度不打值,是为了避免凭据进日志。这一条在协作场景里格外重要:agent 会把终端输出读回上下文,一旦值被打印出来,它就可能被带进后续的会话记录、被你顺手贴进群里的截图里。

寿命这一项最容易被跳过。长期有效的凭据是最小权限的天敌——它让”撤销”从一个日常动作退化成一次事故响应。能设过期就设过期,设不了就靠定期轮换补上,轮换的节奏和操作顺序参考API 密钥轮换

三、收窄:从任务倒推,而不是从能力正推

收窄做不下去,通常是方向反了。正推的思路是”我先看看能关掉哪些”,这会陷入无穷试错,因为你不知道关掉之后哪一步会断。倒推的思路是先把这个 agent 真正要完成的动作列出来,再问每个动作最少需要什么。

以一个改代码的 agent 为例,倒推之后你会发现它的需求是分层的:读整个仓库、写限定的几个目录、执行测试命令、不需要推送远端、不需要访问生产配置。这五条一列出来,作用域自然就出来了,而且每一条都可验证。

三个维度可以往窄里裁:

资源维度。把可写范围限制到具体目录,而不是整个工作区。判断依据是这个目录里的改动是否可以靠 git diff 一眼看清、靠 git checkout 一条命令还原。能的就放开,不能的(构建产物目录、依赖目录、含有本地凭据的配置目录)就排除在外。

动作维度。读和写要分开给。绝大多数 agent 任务里,读的范围可以很大,写的范围必须很小,这两者混在一个开关里是很多产品的默认行为,但你在自己的工作区里可以拆开。执行类动作单独一档,尤其是会产生网络副作用或者触碰共享状态的命令。

时间维度。一次任务给一次授权,任务结束就失效,是比”永久授权 + 事后审计”更省心的形态。它天然解决了”忘记回收”的问题。工具侧的授权到期会带来一类专门的报错,处理方式见MCP 授权过期

收窄之后一定要跑一次验证,不要等真实任务去撞。验证的方法是构造两个请求:一个是任务确实需要的最小操作,应该成功;一个是你明确想禁掉的操作,应该被拒。两个都符合预期,收窄才算生效。只测第一个,你不知道限制有没有真的生效;只测第二个,你不知道是不是收过头了。

# 只作示意:应当成功的最小只读调用
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $TOKEN" https://example.com/api/read-only-resource

期望 200;换成那个应该被拒的操作时,期望 403。如果拿到 401,说明问题不在作用域,回到上一节。

四、可撤销:撤销不是删掉一行配置

可撤销是三条性质里最容易被高估的。很多人以为在控制台点一下删除就完事了,实际上一次真正的撤销要同时满足四件事:签发方不再认这把凭据、所有持有副本的地方被清掉、正在跑的长会话失效、你有办法确认前三条都做到了。

第一条是签发方的事,通常没问题。第二条是最脏的,副本会藏在这些地方:CI/CD 的变量配置、容器镜像的构建层、同事的本机、某个跑在后台的定时任务、你自己两周前开的一个还没关的终端窗口。第三条最隐蔽——长会话持有的令牌未必会因为签发方撤销就立刻失效,判断方法就是重启进程后复测。

给一个可操作的撤销顺序:

  1. 先列副本清单。凡是这把凭据被复制过去的位置,都要写下来。清单列不全就先别撤,否则你是在制造一次计划外的故障。
  2. 签发新凭据并发到所有位置,此时新旧并存。
  3. 观察一段时间,确认所有调用来源都切到了新凭据。旧凭据的调用量归零之前不要进第 4 步。
  4. 撤销旧凭据,重启所有持有它的长驻进程。
  5. 复测:用旧凭据发一个最小请求,期望 401;用新凭据发同样的请求,期望 200。

第 3 步是很多人省掉的一步,省掉的代价就是撤销当天线上一片红。这个顺序本质上是灰度,只不过灰度的对象是凭据。

还有一件事值得单独做:给撤销留一条不依赖你本人的路径。如果只有你知道去哪里撤、怎么撤,那这个能力在你休假的那一周是不存在的。写一份两三行的操作说明放进团队文档,比什么机制都管用。

五、什么情况下别再折腾

最小权限是有边际收益的,越往细里做,维护成本涨得越快,收益涨得越慢。下面几种情况,我的判断是停手,别继续往细里抠:

收窄之后每天被打断的次数超过了你能忍受的程度,而被拒的对象里没有一个是真正危险的。 这说明你的切分维度错了,不是切得不够细。回到第三节重新按资源倒推一次,而不是继续加规则。加规则解决不了维度错误。

为了收窄,你开始给 agent 写复杂的例外说明。 一旦边界要靠自然语言描述来维持,它就已经不是边界了。边界应该是工作区层面、文件系统层面、凭据层面的硬约束,靠叮嘱维持的约束在长会话里必然衰减。到这一步应该换条路:把危险操作从 agent 的可达范围里物理移走,而不是让它记住不要碰。

撤销一把凭据会牵连三个以上你说不清的系统。 这是欠债信号,说明凭据共用已经严重到无法安全操作的程度。此时正确的动作不是硬撤,是先按消费者拆分重发,把依赖关系摊开,再逐把处理。硬撤的结果通常是回滚,回滚之后所有人对这件事的信心都会下降一截。

你已经在一个问题上试了三轮以上,每轮都是”再调一个参数试试”。 这是典型的思维循环,投入产出比已经为负。停下来,退回到上一个已知可用的状态,把这件事记成一条待办,换个时间用别的思路做。相关的判断方法在AI 修不好时的思维循环里有更系统的描述。

回滚点在哪里,最好在动手之前就定好:动权限配置之前,把当前可用的配置完整备份一份(包括环境变量清单,只记名字不记值),并且确认你有一条不依赖当前凭据的登录路径。没有这条备用路径就动手,是把自己锁在门外的经典方式。

六、避坑清单

每条都写清楚为什么会踩,以及怎么避。

用一把凭据服务所有环境。 会踩是因为一开始只有一个环境,加环境的时候图省事复用了。代价是任何一次泄漏都等于全环境泄漏,任何一次撤销都等于全环境中断。避法是在第二个环境出现的那一天就拆开,那时候拆的成本最低。

把作用域写在文档里,不写进配置。 会踩是因为文档写起来快,配置改起来要动系统。代价是文档和实际权限从第二周开始就对不上,你以为收窄了,实际上没有。避法是每条约束都要有一个可执行的验证命令,验证不了的约束当作不存在。

撤销之后不重启进程。 会踩是因为控制台显示”已删除”,人就默认生效了。代价是旧凭据在长驻进程里继续活着,事故窗口被无声延长。避法是把重启写进撤销步骤,并且用旧凭据复测一次期望 401。

把凭据放进 agent 能读到的文件里,指望靠说明让它别读。 会踩是因为”就临时放一下”。代价是它会在某次搜索、某次全文替换、某次报错回显里被读进上下文,然后跟着日志、跟着截图流出去。避法是凭据只走环境变量,且存放路径不在 agent 的可达范围内。

只给 agent 收权限,不给人收。 会踩是因为大家默认人是可信的。代价是 agent 用的是人的凭据,你对 agent 的所有限制在凭据层面都不成立,出事之后审计日志里只看得到”某个人”。避法是给 agent 单独签发凭据,哪怕它的权限跟人一样,至少来源可区分。

忘了清理临时授权。 会踩是因为调试时开了个大权限说”待会儿收回来”,然后被别的事打断。代价是这个大权限会活很久,久到没人记得它为什么存在。避法是临时授权一律带过期时间,设不了过期就当场写进待办,别靠记忆。

撤销测试只测新凭据能用,不测旧凭据不能用。 会踩是因为业务恢复了就以为完成了。代价是旧凭据其实还有效,你以为的撤销只是换了一把。避法是两个方向都测,旧的期望 401,新的期望 200。

海外工具的凭据管理直接照搬。 部分海外 AI 工具官方对中国大陆有区域限制、不支持直连,账号与凭据体系在这里的行为跟文档描述未必一致;市面上存在第三方中转,但我不背书也不推荐具体渠道。会踩是因为照着通用做法配了一套,实际链路上多了一跳,撤销和审计都不在你手里。避法是先确认这条链路上凭据到底经过谁的手,再决定给它多大的作用域。

收个尾。最小权限落不了地,九成不是因为你不够严格,而是因为凭据在下发那一刻就没有边界,导致后面每一次收窄都变成了猜。把顺序倒过来做——先让每把凭据有主人有用途有寿命,再从任务倒推作用域,最后把撤销做成一个五分钟能完成的日常动作——这件事就从”安全项目”变成了普通工程习惯。

动手之前过一遍这份自检:

  • 这把凭据的主人是谁,能一句话说清吗?
  • 它的作用域是从任务倒推出来的,还是从默认全权限往下砍的?
  • 收窄之后,我跑过”该成功的成功、该拒绝的被拒绝”这两个验证了吗?
  • 如果现在要撤销它,副本清单我列得全吗?
  • 撤销之后,我会重启进程并且用旧凭据复测一次吗?
  • 这条撤销路径,除了我以外还有第二个人会操作吗?

六条里有两条答不上来,先别急着加新规则,把这两条补上,收益比继续收窄大得多。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。