裸跑 Ollama 只是玩具:把单卡算力做成私有化 Agent 系统的六道工程防线

Reading Time: 16 minutes

结论先行

单卡把模型跑起来,只完成了整套东西的很小一部分。把它做成一个能长期托付的私有化 Agent 系统,剩下那些工作决定成败。

写这篇的直接触发是一个数字:Ollama 的一个未鉴权内存泄露漏洞,CVSS 评分 9.1,影响约 30 万台暴露在公网的实例。

攻击者不需要任何凭据,三次 API 调用就能读出整个推理进程的内存(CVE-2026-7482,2026 年 5 月 4 日披露,Ollama 0.17.1 之前的所有版本)。

一年前这类事故还只是「有人白嫖了你的显卡」。2026 年 6 月 12 日,Sysdig 的研究团队记录到攻击者把一台未鉴权的 Ollama 直接当作自动化攻击工具的推理引擎。

同一个月,一个叫 NadMesh 的僵尸网络被发现在扫描公网上的 AI 服务,它的目标清单里包括模型服务和图像生成界面,也包括工作流引擎和低代码平台。

本文写的是我把自己那台常年在线的推理主机,从「能跑」做到「敢让它替我做长任务」的过程。里面每一条都来自真实的踩坑记录,包含一个我在加固过程中把自己关在门外的夜晚。

涉及的六道防线按从外到内的顺序:绑定地址与鉴权;防火墙规则的路径;最高权限的作业执行器;安全垫的回读确认;中间层的缓存;数据落地与容器边界。

本文对具体地址、域名与网络拓扑做了脱敏处理,只保留可复用的判断方法。

背景:默认配置是给演示用的

本地推理工具的默认配置,设计目标是让你五分钟内看到第一个 token。这台机器值不值得买,我在另一篇算过价格账,这一篇只谈买回来之后怎么守。这个目标跟「连续三十天稳定服务」之间隔着一整套运维工作。

最典型的是监听地址。

Ollama 默认监听 11434,并且不带任何鉴权。

llama.cpp 的服务端默认监听 8080,同样没有鉴权;它的官方参数里准备了 API key 相关的选项,但绝大多数教程给出的启动命令是一个绑定到全网卡的地址,附带很少的安全提示。

这套默认值在单机自用场景下没有问题。问题出在你把它接进一个自动化系统之后。

那台主机上现在跑着一个 27B 级别的本地模型,被工作流引擎当作推理后端调用。它能装下这个模型的前提是内存算得对,内存账在这里。它要能被我远程唤醒,能在夜间跑批处理,也要能接受来自存储服务器的请求。

到了这个状态,「绑定地址」就不只是一个配置项,它是你的暴露面定义。

私有化 Agent 系统需要的六道防线

下面六件事,每一件都是我实际遇到过问题的环节。它们之间没有替代关系,任何一层失守,其他层的复杂性都白费。

私有化 Agent 系统的六道工程防线分层图:网络暴露面、防火墙路径、提权面、变更纪律、中间层、数据与容器边界,外加调度层
六道防线按从外到内排列。虚线框是调度层,它与前六层性质不同:前六层管谁能进来,它管进来的任务能不能跑完。

推理服务的绑定地址和鉴权,默认值不是给你长期用的

先确认你现在的服务绑在哪里。在本机执行一条命令就能看出来:监听地址如果是全网卡,任何能到达这台机器的设备都能调用你的模型。

鉴权方面,llama.cpp 的服务端提供了 API key 参数,可以指定单个密钥或者从一个文件读多个。

Ollama 在 0.17.1 之后修掉了那个内存泄露漏洞,但「默认无鉴权」这个设计没有变,需要你自己在前面套一层反向代理或者网关来做认证。

这里有一个容易被忽略的连带影响。推理服务的接口如果没有任何认证,那么任何能访问到它的人不只是能用你的显卡,还能用你的模型处理他们自己的数据。你的机器会变成一个免费的推理池,而账单是你付。

防火墙规则存在,不等于在你的访问路径上生效

我在这件事上踩了一个很典型的坑。

为了收窄暴露面,我把推理服务的入站规则限制到内网网段。看起来干净利落。问题在于这台主机同时接入了一个私有组网,它的地址段落在运营商级 NAT 的范围里,不在内网网段内。

结果是:局域网访问正常,通过私有组网访问会静默失效。没有报错,没有日志,就是连不上。

主机的防火墙按「配置文件」分层,同一套规则在不同网络类别下的行为完全不同。一块网卡被系统判定为公用网络时,只写给专用网络的规则根本不会被应用。

这几个类别是三个不同的世界,规则写在哪个世界里决定了它在什么情况下生效。

判断方法很直接:把每条允许规则的来源地址和目标端口,连同它生效的配置文件一起列出来,然后逐个问自己「我从哪条路进来,这条路属于哪个配置文件」。任何一条路径没有对应规则,就是失效的。

还有一个更隐蔽的陷阱。

规则里的来源地址如果写成斜杠加位数的形式,读回来往往变成点分掩码的形式。用脚本做完改动后去断言,如果拿写入时的字符串去比对读回的值,会得到「改动失败」的假结论。

看起来是检查出了问题,其实是格式归一化。

用最高权限跑的作业执行器,本身就是提权原语

那台主机上有一个我自己设计的作业模式:一个以最高系统权限运行的计划任务,每隔一分钟读一次作业文件,按白名单执行动作。这个设计的目的是让我能从外部触发受控操作,不用开一堆常驻服务。

它的安全性完全依赖两件事:动作白名单,以及作业文件所在目录的写权限。

如果那个目录对普通用户可写,那么任何一个在机器上落地的低权限程序,都可以往里写一条作业,换来一次以系统权限执行的机会。这是本地提权的标准路径,不需要任何漏洞。

白名单本身也需要被验证。我在这里犯过一次错:为了检查一个「任务是否存在」,我用了在命令输出里搜索任务名的写法。那条命令在任务不存在时会报错,而报错的文本里恰好包含了完整的命令行,里面就有那个任务名。

于是检查恒为真。它永远告诉你任务还在,无论真实状态如何。

修法是两条。

检查执行结果的状态码或者结构化字段,不要在输出里搜自己刚输入的字符串。给这类检查配一个阴性对照:找一个确定不满足条件的对象跑同一个检查,它必须报失败。

如果对照也通过,说明这个检查什么都检查不了。

安全垫必须读回确认,否则等于在裸奔

远程改防火墙有把自己关在门外的风险。业界通行的做法是先挂一个定时回滚任务,几分钟后自动恢复原状态,改完确认能连上再把它删掉。

这个思路是对的,我照做了,而且它救过我一次:某次修改触发了网络重新评估,连接被打断,是那个定时任务把配置恢复回来的。

但我在另一次操作里犯了一个更严重的错误。

那次我以为安全垫挂好了,就直接改了网络类别。整个过程没有出问题,收尾时我去删那个定时任务,系统告诉我找不到它。也就是说,任务从来没有创建成功,那场高危操作是在完全没有保护的情况下跑完的。

问题出在我没有确认。

创建命令的输出是空的,我把它当成了正常,继续往下走。

一条通用法则:安全垫的价值不在「我做了这个动作」,而在「我现在能证明它处于武装状态」。挂完必须读回一次,读回为空就停下来重新挂。一个静默失败的安全垫比没有安全垫更危险,因为它让你按有保护来估算风险。

带鉴权的响应也会被缓存在半路上

这条跟直觉相反,所以值得单独说。

我在给一篇文章做 SEO 时,需要读回后台的草稿正文做校验。多次读到的是旧版本,而我确定数据库里已经是新的。

排查到最后发现,中间层的缓存把带鉴权的接口响应也存了下来,尽管那个响应明确声明了「私有,不可缓存」。

更严重的问题在后面。

我用一个不带任何凭据的请求去打同一个地址,拿到的是完整草稿正文。

也就是说,那段时间里任何人访问那个地址都能读到还没发布的文章。草稿没有公开页面,不代表它的内容不可读。

处置顺序是先清掉那几条缓存,然后必须用匿名请求复验,返回权限拒绝才算关闭。这里有个细节:不要用带凭据的请求去验证,那会把缓存重新灌满,等于自己把门又打开。

根治的办法是在 CDN 上为接口路径加一条绕过缓存的规则。这项工作需要在账号侧完成。

通用法则是:任何带鉴权的读取,都可能在链路上留下一个对外可见的副本。只读不写也会扩大暴露面,所以能用本地工具直连数据库就别绕远路。

数据落地与容器边界:权限在谁手上

前五层管的是「谁能连进来」,这一层管「进来之后能碰到什么」。数据落到哪里、谁能读它,是数据主权这件事在工程层的具体形态。

容器里的默认身份问题最容易出事。我用的那台存储服务器上,进入容器执行命令默认就是最高权限,可以直接改宿主机挂载进来的所有文件。这在单人环境里是方便,在多组件环境里就是一个横向移动的通道。

文件权限同理。

我维护的知识库目录设成了全开放权限,理由是多个容器都要写它。这个选择的代价是任何能进入这台机器的人或者进程都可以改它。

这一层的判断标准可以量化。每个组件问三个问题:它以什么身份运行,它能写哪些目录,这些目录里有没有凭据文件。任何一个组件能读到凭证或者能写入别人的配置,这条边界就不存在。

长任务为什么会断:上下文策略才是主因

前六道防线解决的是「不被人拿走」。这一节解决的是另一个更常见的失败:没人攻击你,任务自己就跑不下去。

把 27B 级别的本地模型接进自动化调度之后,我遇到最多的问题很少出在模型不够聪明。跑到十几步之后,它开始做一些莫名其妙的事,重复已经做过的动作,忘记前面确认过的事实,把手上的中间结果当成最终结论。

多数人第一反应是换更大的模型。我在读完两篇相关论文之后改变了看法:这类失败的主因通常不在参数规模,而在上下文策略。

需要原始输入的时刻,集中在最前面

第一篇论文研究的是多模态检索场景,跟踪了大约一万条执行轨迹。它发现一个清晰的阶段性规律:需要查看原始图像的时刻高度集中在前几步,之后证据的获取方式转为文本检索和网页访问。

具体数字是,全部被标注为「需要图像」的步骤里,有 97.8% 出现在前五步之内。第一步的推理文本里 95.2% 被标注为需要图像,从第二步开始,这个比例急剧下降,「已从图像提取」成为主要类别。

这个规律可以推广到非多模态的场景:一个任务里真正需要高保真原始输入的时刻,通常集中在最初的定位阶段。之后模型做的是推演和操作,它需要浓缩过的事实,原始载荷在这个阶段只是负担。

折回:把子任务的结果变回文字,丢掉原始载荷

基于上面那个观察,那篇论文提出的做法叫上下文折回。

它维护一个持久的纯文字主上下文,负责高层规划;一旦需要查看原始图像,就临时开一个分支上下文,把相关图像加载进去,做完子任务之后,把结果压成一段简短文字折回主上下文,图像和分支轨迹随即丢弃。

效果是平均精度提升 6.3 个百分点,同时工作上下文长度缩短 27.5%(论文基准上,主上下文约 44.1K 对 54.6K)。

这组数据里更值得一提的是它的消融实验,两个方向都有代价。

把原始图像换成文字说明,精度掉得最狠:在某些模型上分别是 16.3 和 21.4 个百分点。用描述替代原始等于丢失,精细的定位信息在描述那一步就没了。

反过来,让原始图像一直留在主上下文里,精度掉 6.4 和 7.5 个百分点,几乎退回到不做折回的基线水平。

两个方向都错。

上下文折回的消融实验:用文字替代原图损失 16.3 到 21.4 个百分点,原图常驻主上下文损失 6.4 到 7.5 个百分点
消融实验的精度损失,两个基座模型并排。左侧最重的一根是用文字说明替代原图,右侧最轻的一根是把原图一直留在主上下文。数据来自论文原文。

正确的位置在中间:需要的时候加载,用完折回文字,原始载荷立刻丢掉。

还有一个容易忽略的环节。去掉初始化阶段,也就是让模型在开始时就建立一次视觉基准,精度会掉 7.7 到 13.0 个百分点,在需要浏览的任务上跌幅尤其明显。

前面说的「集中在最前面」不只是频率规律,那一阶段的工作质量本身是不可替代的。

一个反直觉的实测结果:可召回几乎没人用

第二篇论文研究的是编码类 Agent 的框架设计,它把上下文管理分成五个档位,其中一个档位的设计是让被剔除的历史可召回:内容被移出上下文,但存到外部,需要时通过工具调用取回。

这个设计在工程上很讨人喜欢,直觉上它同时拿到了节省和完整。实测结论是它很少被调用,而且相比只做剔除,精度没有提升。

这个结果解释了我自己系统里的一些现象。我在压缩上下文时留了恢复标记,实际使用次数极少。

它的实际含义是:「可召回」不能当作保留长上下文的理由。你的上下文策略必须假定模型不会去把丢掉的东西找回来,所以该保留的事实要在折回那一步就写进主上下文。指望后面把它捞回来是不成立的。

规划比模型大小更决定长任务成败

同一篇论文里有一组更刺眼的数字,关于「规划」这个组件。

在没有规划的情况下,68.6% 的执行在「没有编辑过任何一个文件」的状态下终止,58.4% 卡在定位阶段。开启规划之后,这两个数字降到 27.8% 和 10.4%。

换成参数小一些的模型,规划带来的成功率提升是 11.6 个百分点。

工具集的影响同样大:给一个预定义的工具集,成功率提升 15.0%;而把工具集砍到只剩命令行,任务的失败位置会大幅前移,有 32.8% 的执行在还没编辑文件时就结束了。

这组数据对我的意义是:把本地模型接进调度系统时,第一件该做的事是给它一个明确的规划环节和一套固定的工具集。换更大的模型要排在这两件事后面。失败的形态变了,从「做完但不对」变成「根本没开始做」。

在 27B 上落地时的形状

把上面这些结论落到一台单卡机器上,我的做法是这样。

主上下文保持纯文字,只放三样东西:任务目标与当前进度;已经确认的事实;每个子任务折回之后的一句话结论。原始工具输出不进主上下文,长的中间结果落在文件系统里,路径写进结论。

需要看大块原文或者图片时,另起一个分支处理,处理完只把结论折回。

分支的完整轨迹丢掉。

每个长任务开头强制一轮规划,把任务拆成可验证的步骤,规划结果本身写进主上下文。这一轮的开销在整条轨迹里占比很小,但它决定了后面九成的执行有没有方向。

关于模型规模,我现在的判断是:在这个流程搭好之前,换更大的模型只是在提高「更快地做错事」的速度。

代价:这套防线花了什么

诚实说清楚成本。

时间上,把这套私有化 Agent 系统的六道防线从零做齐,大约是几个工作日的投入,其中一半花在验证上。配置本身很快,确认它真的生效很慢。

复杂度上,代价是持续的。防火墙开启之后,一些原本默默工作的东西会显式失效。

我自己就遇到了探活的问题:主机在防火墙开启后不再回应入站的探测请求,因为系统默认只为「目标不可达」这类消息放行,回显请求是关闭的。

后果是我自己的监控脚本开始把一台健康的主机报成离线。修法是放行局域网范围内的回显请求,并且把来源限制在局域网,不要用任意来源。

这个例子说明一个更普遍的代价:安全加固会改变系统对外的可观测性,而依赖那些信号的下游工具需要同步调整。加固完成后,第一件该做的事是把监控跑一遍,看有没有东西开始误报。

过度加固同样有代价。

把所有东西都锁死之后,正常运维的摩擦会上升,而摩擦上升会让人绕过流程。我给自己划的线是:能自动化的验证尽量自动化,不靠人的纪律去维持安全状态。

自查:怎么知道自己的部署在裸奔

下面这几条可以今天做完。

确认模型服务的版本。Ollama 在 0.17.1 修掉了那个 CVSS 9.1 的内存泄露漏洞,接口的版本字段可以直接读到当前版本号。

低于这个版本的要升级。

确认监听地址。

看服务的监听地址是不是全网卡。是的话,明确回答一个问题:这台机器会接入哪些网络,那些网络上的设备能不能信任。

确认鉴权。

你的模型接口有没有密钥。没有的话,任何能到达这个端口的人都能用你的算力处理他的数据。

确认访问路径。

把允许规则和实际访问路径对齐,特别注意私有组网、虚拟网卡这类不在主网段内的路径。

确认作业执行器的目录权限。高权限执行器读取的输入目录,普通用户能不能写。

确认缓存行为。

有没有中间层在缓存带鉴权的接口响应。用匿名请求打一次自己的后台接口,看返回什么。

确认探活方式。加固之后,你的监控靠什么判断服务在线。如果靠回显请求,现在它可能已经失效了。

参考来源

下一步

这套防线我是在自己那台机器上逐个撞出来的,中间把自己关在门外过一次,也让一篇没发布的草稿在缓存里公开过一阵。写出来是因为这些坑的可复用性比结论更高。

如果你正在把本地模型接进生产流程,或者准备把一台家用算力机器做成团队可用的私有化 Agent 系统,可以在下面留言说你的形态,我会针对具体的失败点回答。

以上内容基于公开信息整理与个人实测,仅供学习参考。涉及具体版本与配置的处置,请以官方文档为准。

请喝一杯咖啡
这些内容都是我一个人查资料、跑数据、核事实写出来的。如果对你有用,可以请我喝一杯咖啡。
微信扫码 · 微信支付微信扫码 · 微信支付
支付宝扫码支付宝扫码
个人收款码 · 微信支付 / 支付宝

本文采用 CC BY 4.0 许可。欢迎转载与引用,请注明作者并附上原文链接。
Licensed under CC BY 4.0. Quoting and republishing are welcome with attribution and a link back to this article.

发表评论