把信任移进边界

今天 HN 上几篇文章,乍看不是同一个方向。

有人从求职面试里发现,连小团队也越来越爱上 Kubernetes;有人遇到伪装成 LinkedIn 招聘的代码仓库后门;有人把 OpenCode 接进自己的 homelab,用 PR 和 GitOps 管住 AI 修改;有人解释 Rust 和 C/C++ 的内存安全 CVE 为什么不能简单数数量;Iroh 1.0 发布,主张用 key 而不是 IP 来拨号;还有一篇讲 TimescaleDB 为什么能把时序数据压缩到很高比例。

这些主题跨度很大,但共同点很清楚:可靠性正在从人的记忆、习惯和临场判断,迁移到系统边界里。

过去很多系统靠人来维持秩序。某个老员工知道哪台 VM 上跑着支付服务,某个开发者知道哪个 repo 不能直接 npm install,某个维护者知道 C API 哪些参数不能传 NULL,某个运维知道哪张表不能大 DELETE,某个网络工程师知道某台设备现在在哪个 IP 后面。这些知识可以工作很久,直到人疲劳、离职、被钓鱼、写错命令,或者系统规模突然变大。

AI 和自动化让这个问题更明显。因为操作频率变高,生成速度变快,边界外的东西更容易被喂进系统。此时再说"大家小心一点"不够。好的工程会把该小心的地方变成结构:权限、类型、协议、Git 记录、沙箱、地址抽象、数据布局。人仍然重要,但不该成为最后一道脆弱防线。

Kubernetes 那篇面试反思很有代表性。作者发现,五年前公司基础设施还有几派:少数上 Kubernetes,一些用 VM 或 VPS 加 systemd,一些用 serverless。现在他面试的公司几乎全在 Kubernetes 上,甚至十个人、两个服务的小团队也在用。作者一开始从技术角度看这件事,觉得小公司并不需要调度、弹性、复杂拓扑这些能力。但 CTO 们给出的理由并不是这些。

他们要的是统一性、可雇佣知识和可追踪性。

统一性是所有服务用同一种方式部署,不再有某个服务跑在一台没人敢碰的 VM 上,靠 2019 年留下来的 bash 脚本续命。可雇佣知识是 Kubernetes 已经成了基础设施 lingua franca,新人打开 Helm chart 和 Kube 配置,一小时内就能大致理解系统怎么跑,而不是追问某个离职同事的脑内文档。可追踪性则来自 GitOps:不是谁 SSH 上去手动改,而是 Helm chart 进 Git,经 MR 审批,再由 FluxCD 或 ArgoCD 部署。

这解释了 Kubernetes 为什么会在一些技术上并不需要它的团队里胜出。它不只是调度器,也是一种组织记忆格式。复杂度是真的,CrashLoopBackOff 也会浪费时间;但它把"服务怎么部署"从人脑和机器状态里提取出来,放进一套标准边界中。这个边界让新人、审计、权限和交接都更容易。

也就是说,小团队买的不是 Kubernetes 的全部技术能力,而是一个共同语言。系统知识从人身上移到了 YAML、chart、controller 和 Git 历史里。

LinkedIn 招聘后门那篇则是反面教材。作者收到一个小 crypto startup 招聘消息,对方让他 review 一个 GitHub repo,说要看 deprecated Node modules issue。他觉得不对劲,没有本地 clone 后直接安装,而是在 Hetzner 上开了 throwaway VPS,再用只读 agent 扫代码:pi --tools read,grep,find,ls。agent 很快发现 app/test/index.js 里藏着约 250 行伪装成测试的 payload。

后门把 URL 拆成碎片,组合成 https://rest-icon-handler.store/icons/77,然后执行服务器返回的内容。更糟的是,它不需要测试运行。app/index.js 会 require ./test,而 package.json 里的 prepare 脚本会在 npm install 后自动执行 node app/index.js。换句话说,攻击者给你的任务是"看看旧 Node 模块问题",真正目标是诱导你运行 npm install

这类攻击很现实。仓库看起来像坏掉的项目,代码像初学者写的烂测试,commit 身份还冒用了真实开发者,LinkedIn recruiter 也借用了真实记者的身份。攻击不是靠一个技术漏洞,而是靠社交上下文、工具默认行为和开发者疲劳叠加。

作者做对的事情,是没有把信任放在自己当下状态上。他没有说"我今天应该能看出来",而是先放进只读沙箱。agent 只能读文件,不能执行命令。VPS 是一次性的,不是自己的工作机。这个边界让攻击链断在 npm install 之前。

这点对 AI coding 也很重要。读代码的 agent 很有用,文章里 agent 几秒就定位了后门;但 agent 的权限必须分层。读、grep、find、ls 和执行 shell 完全不是同一类能力。越是让 agent 帮你处理不可信输入,越要先把它关在只读边界里。

Homelab AI Dev Platform 那篇是在日常维护里做同一件事。作者把 OpenCode Web UI 部署成一个 VM 上的服务,接入 Git,可以持续保存 coding session,也能跨设备使用。他用它维护 home infra:总结服务 release notes、更新容器、给 compose stack 加 healthcheck、调整网络配置。关键不是"AI 能帮忙改配置",而是它被放进了一个受控平台。

OpenCode 有自己的 Git 用户和 SSH key。它能 clone 项目、push feature branch,但不能直接 push 到 deploy branch。它所在 VM 有互联网和 Git server 访问权,但不能访问实际服务。AI 可以在 VM 里装工具、测试依赖,必要时甚至有 root,但 blast radius 只在这台 VM 里。部署由 PR review 和 GitOps 接管,未审查代码不能直接进生产。

这比"让 AI SSH 到服务器上帮我修一下"健康得多。它承认 AI 有用,也承认 AI 不该直接碰真实服务。Git branch、PR、网络隔离、专用用户、GitOps,这些边界把 agent 的能力变成可审计流程,而不是凭感觉的遥控器。

作者还提到一个缺口:CI feedback。GitHub Actions logs 可以给 coding agent 用来诊断测试失败、lint 错误、stack trace 和 IaC plan 变化;Forgejo Actions 的公开 API 不方便拿日志,所以反馈闭环没那么顺。这个细节很真实:好的 agent 平台不是只给模型一个编辑器,还要给它可控的反馈和可控的权限。反馈不足,agent 会瞎猜;权限过宽,agent 会闯祸。

Rust/C++ 内存安全 CVE 那篇把"边界"说得更形式化。作者指出,比较 Rust 和 C/C++ 的 CVE 数量很容易误导,因为两者对"谁负责内存安全"的边界不同。

在 C 里,如果你调用 curl_getenv(NULL) 导致 segfault,通常不会被当作 libcurl 的 CVE。大家会说这是错误使用。问题是,C 的类型系统通常不能精确表达所有前置条件,也没有 safe/unsafe 的语言级边界。几乎每个 API 都可能被"拿错姿势"触发 UB。如果所有潜在误用都报 CVE,C/C++ 生态会被淹没。

Rust 的规则不同。如果一个 safe Rust API 可以在调用者不写 unsafe 的情况下触发内存错误,那就是库的 soundness bug,应该归因到库。因为 safe API 的承诺就是:调用者无法用安全代码制造内存不安全。只有当调用者显式进入 unsafe block,责任边界才回到类似 C/C++ 的模式。

这就是边界的力量。Rust 不是说 bug 不存在,而是把责任放到可见位置。safe 代码默认不能导致内存 UB;unsafe 代码必须标出来,review 时一眼可见。库内部如果用 unsafe 包装出 safe API,就必须承担 soundness 义务。

这比"大家正确使用 API"强得多。正确使用是一种隐性知识,safe/unsafe 是语言边界。前者靠人记得,后者靠编译器、审查和生态规则共同维护。

Iroh 1.0 则把边界从进程和代码扩展到网络。它的口号是 Dial Keys, not IPs。IP 地址会变,设备在 NAT、防火墙、移动网络和局域网之间迁移时,地址经常失效;key 是设备自己生成和控制的,可以作为稳定身份。Iroh 用同一把 key 来建立安全连接、身份、权限和归因,并通过 QUIC multipath、QUIC NAT traversal、本地优先发现和自定义 transport,让设备无论在哪里都能被安全寻址。

这其实是在重画互联网应用的信任边界。传统网络里,地址和身份经常分裂:IP 是位置,TLS 证书和账号系统才是身份,中间还要处理 NAT、relay、端口转发和防火墙。Iroh 把"拨号对象"改成 key。连接细节可以变,路径可以在多个网络之间 hot swap,甚至可以接 BLE、LoRa、WiFi Aware 或 Tor,但上层面对的是同一个 key。

对本地优先和 agent 应用,这个抽象很有吸引力。设备、服务、agent、文件同步、聊天、游戏、模型训练节点,都可以用稳定身份互相发现,而不是把公网 IP、内网穿透和云 relay 当成唯一入口。Iroh 说 95% 数据通常可以 direct transfer,公共 relay 主要帮忙打洞和兜底。更少云中转意味着更低 egress,也意味着更多边缘设备可以直接协作。

当然,key 不是魔法。key 丢了、权限设计错了、relay 策略不清楚,还是会出问题。但它把"我在和谁通信"从网络位置中剥离出来,变成一个更稳定、可撤销、可归因的边界。这比追着 IP 跑更符合现实互联网。

TimescaleDB 压缩文章讲的是数据形状。PostgreSQL 的 TOAST 处理单个大字段,而 TimescaleDB 面对的是时序数据:同一指标、同一设备、同一时间方向上的大量相似值。它的 hypercore 用混合行列存,把时间序列按 segment 和 order 组织起来,再用 delta encoding、delta-of-delta、Gorilla XOR、run-length encoding 等算法压缩。文章强调,想达到很高压缩比,关键在 segmentbyorderby 设计。通常按设备、传感器、host 这类高基数维度 segment,按 time 排序。

这也是把知识移进边界。你当然可以把所有指标塞进普通行存表,然后期待通用压缩救你。但时序数据的规律不是单个值大,而是相邻值相关、同一实体连续、时间递增。把这个规律写进 hypertable、chunk、segment 和 order,数据库才能用正确的物理形状处理它。

换句话说,压缩不是最后一步打包,而是 schema 设计的一部分。数据生命周期、查询模式和物理布局需要对齐。否则你只是把成本留给磁盘、缓存和查询执行器。

把这些文章合起来看,会看到同一个工程原则:系统应该承载知识,而不是把知识留给人类临场发挥。

Kubernetes 把部署知识放进标准资源和 GitOps;招聘后门提醒我们不可信代码先进入只读沙箱;homelab AI 平台把 agent 权限放进 VM、Git branch、PR 和网络隔离;Rust 把内存安全责任放进 safe/unsafe 边界;Iroh 把网络身份放进 key;TimescaleDB 把时序数据规律放进存储形状。

这些都不是单纯的工具偏好。它们是把信任从人身上移到边界里。

这对 AI 时代尤其重要。因为 AI 会放大两件事:一是执行速度,二是不可信输入的进入频率。agent 会更频繁地读 repo、改配置、运行测试、总结日志、生成代码、调用工具。每一步如果都靠"这个人应该会小心",迟早会撞上疲劳、误判或上下文污染。

更可靠的做法是把边界设计成默认路径。

不可信 repo 默认只读分析,不默认安装依赖。AI 修改默认走 branch 和 PR,不默认直接部署。服务配置默认进 GitOps,不默认 SSH 手改。内存安全默认由 safe API 承诺,不默认靠调用者记住所有前置条件。设备通信默认拨 key,不默认追 IP。时序数据默认按查询和生命周期塑形,不默认让通用存储硬扛。

这听起来会慢一点。嗯,一开始是会慢。写 Helm chart、配 GitOps、建 sandbox、区分 safe/unsafe、设计 segmentby、管理 key,都会比直接上手麻烦。但它换来的是第二个人、第二十次部署、第二百个 agent 任务时的稳定性。

这也是为什么很多基础设施最后都会从"技术最优"变成"组织最优"。小团队未必需要 Kubernetes 的调度能力,但需要部署知识不丢。个人 homelab 未必需要完整开发平台,但需要 AI 不能直接碰服务。Rust 的 unsafe 标记看起来繁琐,但它让 review 聚焦在真正危险的地方。Iroh 的 key abstraction 不是消灭网络复杂性,而是把复杂性藏到稳定身份后面。

好的边界不是为了限制创造力,而是为了让创造力不用一直担心地板塌掉。

如果说前几天的主题是"验证进入系统形状",今天可以接着说:信任也要进入系统边界。验证回答"这件事对不对",信任回答"谁可以做这件事、在什么范围内做、出了问题归因到哪里"。两者都不能只靠最后一个人。

以后做 AI 工程和基础设施,我会更关注一个问题:这个系统里的关键知识在哪里。如果答案是"在某个人脑子里""在一次聊天记录里""在大家都知道的习惯里",那它还没有进入工程状态。真正进入工程状态,是它被写进类型、配置、权限、网络身份、数据布局、审计日志和部署流程里。

人会忘,IP 会变,repo 会骗人,agent 会误读,磁盘会膨胀。边界设计得好一点,系统就少一点靠运气。