ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

GPU算力平台安全架构实践:沙箱隔离、凭据代理与L7策略全解析

GPU算力平台安全架构实践:沙箱隔离、凭据代理与L7策略全解析

1. “三个各管各”的安全断点,是 OpenShell 这类壳架构出现的真正原因

我最初接触 NVIDIA OpenShell 这个概念,是在处理一个多租户 GPU 算力平台的混乱现场。训练任务由不同团队提交,有人要 A100 跑大模型微调,有人只想要一小块显存跑数据分析;安全团队的要求很明确——隔离要到位、凭据不能出现在作业脚本里、服务之间的访问必须做 L7 粒度控制。可实际上呢?机器的分配靠人工协调,任务一多就互相抢占;为了能访问内部数据仓库,作业脚本里直接写着明文 token;团队之间的服务调用,最多就是靠网络层白名单挡一下。听起来很离谱,但这是不少 GPU 集群的真实常态。

这三个问题单独看都不算新东西:容器能隔离,Kubernetes Secret 能管凭据,防火墙和网关能做访问控制。但放在 GPU 场景里,传统方案就暴露了一个共性短板:它们不是按 GPU 计算的特征设计的。GPU 设备不是普通外设,它有独立的显存、独立的调度器、特殊的驱动上下文;计算作业也不是普通 Web 服务,它们吞吐大、延迟敏感、生命周期长短不一,而且经常要访问外部数据源和模型仓库。用一套“通用容器 + 通用密钥 + 通用网络策略”的组合拳去套,表面能用,实际处处漏风。

NVIDIA OpenShell 拆开来看,其实就是把沙箱、凭据代理、L7 策略执行这三件事,重新按照 GPU 工作负载的特征封装成一个统一入口。沙箱决定算力怎么隔离,凭据代理决定身份怎么流转,L7 策略执行决定请求在什么前提下放行。这篇内容想做的事,就是把这套架构的每一层拆开讲清楚,说透它设计背后的逻辑,再给出一个能照着落地的验证路径。适合正在搭 GPU 算力平台、做模型服务上线,或者负责集群安全策略的工程师参考。

2. 沙箱层拆解:从进程命名空间到 GPU 设备面的完整链路

2.1 普通容器沙箱为什么管不住 GPU

大多数团队对沙箱的理解停在“容器隔离”这个层面:进程跑在独立的命名空间里,文件系统、网络栈、进程号是隔离的,资源被 cgroup 限制住,好像就安全了。但 GPU 的隔离远没有这么简单。容器化的本质是复用宿主机内核,GPU 设备通过/dev/nvidia*设备节点暴露给容器,而真正控制设备的其实是驱动栈和 CUDA runtime。换句话说,即使容器在命名空间层面做得再干净,只要设备节点映射进去了,进程对 GPU 的访问路径就是直通的。

直通意味着什么?意味着隔离的重心从“进程边界”转移到了“设备边界”。普通容器可以用 cgroup 限制 CPU 和内存,但显存不一样——显存是 GPU 板载资源,操作路径是驱动通过 ioctl 完成的,容器 runtime 很难精细地限制“这个容器最多用 8GB 显存”。传统方案只能做到整卡分配:要么把整张 GPU 给一个容器,要么用环境变量把某几张卡指给某个任务,粒度粗得可怜。多个任务共享一张卡时,一个任务的显存溢出或崩溃,被影响到的可能是同一张卡上所有的任务。

还有一层隐藏得更深的问题:驱动状态是全局的。CUDA context、显存分配表、GPU 调度器的状态都挂在驱动层,普通容器的 namespace 隔离管不到这些。所以哪怕是两个看起来完全独立的容器,只要操作同一张物理 GPU,它们在驱动层就是互通的。这也是为什么单纯靠 Docker 跑 GPU 任务,会频繁出现“A 容器崩了,B 容器也连带报错”的现象。

2.2 MIG、时间切片与设备隔离的颗粒度选择

真正解决显存和算力隔离的,是 GPU 硬件层和驱动层提供的切分能力。NVIDIA 的 MIG(多实例 GPU)是目前颗粒度最硬的方案:它把一张物理 GPU 在硬件层面切分成多个独立实例,每个实例有独立的显存、独立的 L2 缓存、独立的内存带宽,甚至独立的安全边界。MIG 模式下,一个实例的崩溃和另一个实例基本无关,隔离性接近物理隔离。在 OpenShell 这类壳架构里,MIG 实例天然就是最合适的“沙箱单位”,因为它的隔离是被硬件保证的,不需要在软件层做太多补丁。

MIG 的坑在于配置和调度都不够灵活。启用 MIG 需要先设置 GPU 的工作模式,某些型号甚至要求重启节点;实例的划分是按计算切片模板来的,不能像容器那样动态调整。而且 MIG 目前主要支持数据中心级 GPU,消费级显卡和部分工作站显卡用不了。如果你的集群全是 RTX 卡,MIG 这条路就走不通,只能用时间切片这种软隔离方案。

时间切片是驱动层把 GPU 算力按时间片轮流分给不同进程,显存依然是共享的。它不需要硬件支持,任何支持 CUDA 的 GPU 基本都能用,但隔离强度明显弱:一个任务的显存压力会直接影响其他任务的可用性,一个任务的长时间占用也会挤占时间片。OpenShell 如果在这种模式下做沙箱,就必须叠加额外的显存监控和抢占策略,否则“隔离”就只是名义上的。我的判断是,生产环境里能上 MIG 就上 MIG,时间切片更适合开发测试场景。

2.3 设备访问控制、seccomp 与驱动稳定性

沙箱的最后一层防护,是对设备访问路径的收敛。GPU 设备的控制通道是驱动暴露的 ioctl 接口,攻击面就在这里。常规容器会把/dev/nvidiactl、/dev/nvidia0这些设备节点直接交给容器,等于把一整条驱动控制通道全部开放。OpenShell 这类架构会在设备访问层加一道过滤,配合 seccomp 限制容器进程能发起的系统调用,尽量把 ioctl 的范围收窄到 CUDA 正常使用需要的部分。

这一层很容易被忽略,但恰恰是实际事故的高发区。我在排查 GPU 沙箱问题的时候,碰到过好几次容器里nvidia-smi报 “couldn't communicate with the NVIDIA driver”,宿主机上一切正常,容器里就是不通。大多数人第一反应是驱动装错了,其实根因往往在沙箱配置:设备节点没映射完整,或者 seccomp 规则把关键的系统调用拦了。NVIDIA 容器工具包(Container Toolkit)能解决大部分映射问题,但在自定义沙箱层的情况下,设备映射和 seccomp 是需要手动核查的重灾区。

驱动版本和 CUDA 版本的匹配也要放在沙箱稳定性里一起考虑。容器里的 CUDA runtime 和宿主机的驱动版本之间有兼容矩阵,新版驱动可能要求容器里至少是某个 CUDA 版本。如果沙箱环境里多个任务跑着不同 CUDA 版本,驱动升级一次,某些老任务的运行环境就坏了。我以前处理过一个 case:宿主机驱动从 535 升到 550,所有基于 CUDA 11.8 的旧容器全部报错,排查了半天才发现是驱动对旧 runtime 的兼容性收紧了。所以在设计沙箱层的时候,一定要把驱动生命周期规划进去,别等升级了再补救。

3. 凭据代理机制:用短期令牌替代作业里的静态密钥

3.1 静态凭据在 GPU 作业中的三个麻烦

GPU 作业要访问外部资源,这件事比普通应用更麻烦。训练任务需要拉取模型权重、读写数据湖、把结果推送到对象存储;推理服务需要定期更新模型版本。这些操作都要凭据。大多数团队嫌麻烦,直接把密钥写进环境变量或者挂载成文件,然后让作业脚本去读。表面上是省事了,实际上埋了三颗雷。

第一颗雷是泄露面不可控。作业脚本、镜像层、临时文件、日志输出,任何一个环节泄露都会把密钥带出去。GPU 作业经常在分布式环境下跑多节点,密钥在多台机器间复制,泄露风险跟着节点数一起涨。第二颗雷是轮换成本高。密钥一旦要换,所有引用它的作业都得重新构建或重新配置,在自动化程度不高的集群里,这基本意味着停机维护。第三颗雷是审计追查困难——哪个作业在什么时候用哪把钥匙访问了哪个资源,静态凭据体系里根本查不清楚,出了安全事件只能抓瞎。

3.2 凭据代理的工作方式:短期令牌的一次性注入

凭据代理解决的就是这三个问题。它的核心思路是:作业本身不携带任何长期凭据,而是启动时向凭据代理发起请求,由代理根据作业的身份和授权范围签发一个短期令牌。这个令牌只在作业生命周期内有效,到期自动作废,就算泄露,损失窗口也被压缩到了作业的运行时长。

听起来和 K8s 里 ServiceAccount Token 类似,但 GPU 场景对性能的要求更高。训练任务动辄跑几小时甚至几天,令牌如果几分钟就过期,任务中途就会因认证失败中断;令牌如果太长又增加了泄露风险。凭据代理在实际设计中,通常采用“双令牌”结构:一个短期访问令牌用于每次具体资源访问,一个稍长的刷新令牌由代理侧的守护进程持有,定期续期。作业进程接触到的永远是短期令牌,长期令牌只存在于代理的受控区域里。

交互链路大致是这样:作业启动前,调度器先向凭据代理申请令牌,代理校验作业身份和资源请求后签发;作业启动时,令牌通过安全通道注入作业环境;作业访问外部资源时,请求由代理或代理注入的 SDK 统一加上令牌;作业结束,代理撤销令牌并记录审计日志。整个过程里,作业脚本不感知密钥内容,开发者只配置“我的作业需要访问哪个资源”,不用关心具体用什么密钥。

3.3 与 Kubernetes 身份体系的联动细节

凭据代理要真正落地,必须和集群已有的身份体系打通。在 Kubernetes 环境里,最自然的做法是绑定 ServiceAccount:每个作业对应一个 ServiceAccount,凭据代理在签发令牌时先校验这个 ServiceAccount 的授权范围。比如设计一个内部模型仓库,只有绑定了特定 ServiceAccount 的训练作业才允许拉取模型文件,凭据代理在签发令牌时就把这个限制写进令牌的 scope 里。

SPIFFE 标准在这里也很关键。SPIFFE 以spiffe://cluster/namespace/serviceaccount这样的格式统一标识工作负载身份,凭据代理签发令牌时可以直接用这种身份标识作为主体。L7 策略层也能和它配合——策略文件里不再写 IP 或者用户名,而是写 SPFFE 身份,这样即使作业被调度到不同节点,身份也不会漂移。

实操中需要注意的一点是,作业容器里的进程经常会通过环境变量读取凭据。如果只是简单地在容器启动时把令牌塞进环境变量,凭据代理就退化成“换个地方存密钥”,没有本质提升。正确做法是提供一个轻量级的 SDK 或 sidecar 进程,作业进程通过本地 Unix socket 调用它来获取令牌,避免令牌落盘。对于已经跑起来的作业框架,实在改不动代码的,可以退而求其次用内存文件系统挂载令牌,但一定要保证容器退出时卷也跟着销毁。

4. L7 策略执行:让访问控制能看懂请求内容

4.1 端口放行在 GPU 服务场景下的盲区

GPU 集群里的服务调用有个特点:绝大多数是集群内流量,走的都是内部网络。如果策略只做到 L4 层,面对的情况就是——所有 Pod 的 IP 段都一样,端口就那几个,用 IP 和端口做白名单等于没有白名单。更麻烦的是,模型服务的调用往往带有明确的业务语义:某个业务线只能调用某个模型的推理接口,但不能调用模型管理接口;某个训练任务能读取数据集,但不能删除数据集。这些需求全靠 IP 和端口是做不出来的,必须把策略的判断粒度提升到请求层。

L7 策略执行要做的事,就是让访问控制“看懂请求内容”。不只是看目的 IP 和端口,还要看 HTTP 方法、路径、gRPC 服务名,甚至请求体里的关键字段。比如一个 LLM 推理服务,管理员可以规定:team-a的服务账号只能 POST/v1/generate,不能调用/v1/delete-model;某个管理后台的请求必须携带特定的身份头,否则一律拒绝。这种粒度的控制在 L4 层完全不可能实现。

4.2 策略描述模型与判定过程

L7 策略执行的关键,是把策略从“防火墙规则”变成“可编程的判定逻辑”。策略不再是简单的 allow/deny 列表,而是一套条件匹配规则。以我实际接触过的策略设计来看,规则通常长这样:

service "inference.default.svc.cluster.local" { allow principal "spiffe://cluster/ns/team-a/*" method POST path "/v1/*" deny principal "*" method GET path "/internal/*" allow principal "spiffe://cluster/ns/admin/*" method "*" path "/**" }

每条规则都包含四个要素:目标服务、请求主体身份、请求方法、路径表达式。判定过程就是拿实际请求的这四个属性去和规则列表做匹配,命中哪条就执行哪条的动作。这里最重要的设计决策是“默认拒绝”。没有显式匹配到规则的请求,一律拒绝。很多团队在这个点上会偷懒,默认放行只是记录日志,结果策略形同虚设。

比 HTTP 路径更精细的是请求体校验。比如模型推理服务可以规定“请求体重必须包含model_id字段,且该字段值必须在当前服务已加载的模型列表里”。这个能力已经超出了传统 API 网关的范畴,更接近策略引擎的定位。OpenPolicyAgent(OPA)是目前最常用的策略引擎之一,它用 Rego 语言定义规则,能对请求体的任意字段做断言。GPU 服务场景下,这种能力对多模型共存、多租户隔离特别有用。

4.3 数据面部署:代理模式、sidecar 与性能代价

策略引擎有了,策略得有人去执行。执行点放在哪,是 L7 方案最大的分水岭。最主流的做法是数据面代理模式:每个服务旁边部署一个 sidecar 代理,所有入站出站流量都经过代理,代理在转发前向策略引擎发起判定。这种模式和服务网格的思路一样,好处是对业务代码零侵入,坏处是每多一层代理就多一跳网络,延迟会增加,GPU 推理服务对延迟又特别敏感。

实际数据里,sidecar 代理的额外延迟大概在 0.5ms 到 3ms 之间,取决于策略复杂度和代理的 CPU 分配。对于 batch 推理来说这点延迟可以忽略,但对在线推理服务就很要命。所以我不建议所有服务无脑上 sidecar,更合理的做法是分层:核心在线推理服务用进程内 SDK 方式执行策略,避免额外网络跳数;离线训练和批量任务用 sidecar 模式,反正对延迟不敏感。

gRPC 场景还需要额外注意。GPU 服务内部通信大量使用 gRPC,策略执行不能只看 HTTP 路径,还要能解析 gRPC 的 method 名。/package.Service/Method这种格式要能拆解成服务名和方法名,再和策略规则匹配。很多方案在这里踩坑——HTTP 路径规则写得挺好,gRPC 流量一来全放行,等于策略白配。

5. 最小环境落地:一次完整的验证路径

5.1 基础环境准备与常见安装坑

如果想把前面这套思路验证一遍,我建议从一台带 NVIDIA GPU 的 Ubuntu 服务器开始,不着急上大规模集群。首先是驱动安装。这一步有个高频问题:驱动装完了,nvidia-smi却显示 “couldn't communicate with the NVIDIA driver”。遇到这种情况先不要重装驱动,按这个顺序排查:

# 确认内核模块是否加载 lsmod | grep nvidia # 查看驱动版本与内核版本 modinfo nvidia | grep ^version # 确认 device 节点是否存在 ls -l /dev/nvidia*

绝大多数情况是内核模块没加载,或者设备节点没创建。如果lsmod里没有 nvidia 模块,执行sudo modprobe nvidia,还是不行就查内核版本和驱动版本的兼容性。Ubuntu 上驱动装好后会自动创建/dev/nvidia*设备节点,但如果装的是来自官网的.run包,有时会和系统自带的新内核模块冲突,需要重新生成模块依赖。

驱动跑通之后,装 NVIDIA Container Toolkit。注意它的安装顺序必须在驱动之后。验证是否可用:

nvidia-container-cli info

能正常输出 GPU 信息和驱动信息,说明容器运行时已经可以透传 GPU 了。再跑一个最基础的验证容器:

docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi

容器里能正常打印 GPU 信息,基础链路就通了。这个步骤看起来简单,但我在多台机器上实操过,经常栽在镜像拉取慢和驱动版本不匹配这两个问题上。

5.2 验证沙箱与设备隔离效果

真实集群里的沙箱验证,不能只验证“容器能看到 GPU”,还要验证“容器只能看到被分配的那部分 GPU”。如果你用的是支持 MIG 的卡,可以这样验证:

# 启用 MIG 模式 sudo nvidia-smi -mig 1 # 查看当前 GPU 支持的 MIG 实例模板 sudo nvidia-smi mig -li # 创建两个 MIG 实例 sudo nvidia-smi mig -cgi 19,19 -C

创建完成后,用nvidia-smi -L查看 MIG 实例列表。然后分别启动两个容器,每个容器通过 UUID 指定使用某个 MIG 实例:

docker run --rm --gpus '"device=GPU-<uuid>"' nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi -L

容器里应该只能看到自己绑定的那个 MIG 实例。用压力工具同时跑两个实例,观察显存和算力是否互相隔离。如果用的是不支持 MIG 的卡,就只能验证时间切片下的显存限制和进程隔离了,隔离强度相比 MIG 会明显弱,这个要心里有数。

5.3 验证凭据代理与 L7 策略的完整链路

凭据代理和 L7 策略的验证不适合手动操作,最好写一个小的测试脚本:模拟一个推理服务和一个客户端,客户端通过凭据代理获取短期令牌,然后携带令牌调用推理服务。推理服务侧的代理收到请求后,先验令牌,再交给策略引擎判定。

验证重点是四个场景:合法的客户端带合法令牌是否能通过;合法客户端带过期令牌是否被拒;非法身份调用越权接口是否被拒(比如用 team-a 的身份调用管理接口);没有任何令牌的匿名请求默认是否拒绝。这四个场景能跑通,说明整个链路是闭合的。

我在这个环节的体会是,最容易出问题的不是策略引擎,而是令牌在作业进程里的传递链路。如果作业进程是 Python 脚本,直接读环境变量最省事,但前面说过这会退化成“换地方存密钥”。比较折中的做法是用一个本地代理进程持有令牌,业务进程通过 localhost 的 Unix socket 调用它获取认证头。测试的时候可以故意在作业跑到一半时吊销令牌,确认代理能感知吊销并阻止后续请求,这样才能验证短期令牌的动态性。

5.4 性能开销与调整记录

把整套体系跑通之后,一定要做一轮性能摸底,否则上线后会被业务团队骂。我实测下来,重点看三个指标:容器启动延迟、推理请求的额外延迟、CPU 占用。

容器启动延迟很容易被忽略。凭据代理签发需要时间,如果每个作业启动都同步等待令牌签发,大规模并行拉起任务时,调度器等令牌会形成瓶颈。解决办法是代理侧做令牌预取和缓存,作业启动时本地已经有可用令牌,签发过程异步化。

推理请求的延迟开销主要来自 L7 策略判定。如果策略简单(只匹配 path),一次判定的 CPU 时间可以忽略不计;如果用 OPA 做请求体内容的复杂断言,单次判定可能消耗几十毫秒级别。对在线推理来说很可观。我的调整思路是把策略分层:第一层用轻量级规则做快速路径,命中放行的直接放行,不进入重策略引擎;只有快速路径无法决断的请求才走完整策略判定。用这种方式能把 95% 请求的额外延迟压到 1ms 以内。

CPU 占用方面,sidecar 模式的代理要预留至少 100 毫核的 CPU 配额,否则高并发下会成为瓶颈。记住,GPU 服务器的 CPU 也是宝贵资源,很多团队把 CPU 全部配给业务应用,忘了给数据面代理留余量,流量一上来整个节点 CPU 被打满。

6. 边界、坑,以及我对后续演进的判断

6.1 目前最头疼的边界情况

这套架构在实际使用中,有一些绕不开的边界情况,我在前文提到的都有对应的坑。这里集中总结一下。

第一,旧作业兼容性问题。已经有大量存量作业在跑,它们不会主动适配凭据代理。强制切换的代价很高,但如果不切换,等于集群里同时存在两套安全体系:新作业走严格路径,旧作业继续裸奔。最稳妥的过渡方案是“影子模式”:凭据代理先以只读和日志方式接入,不动旧作业,等审计数据证明了安全性提升,再逐步迁移。

第二,机器学习框架生态的适配。很多框架(比如 PyTorch 的分布式训练)会自己拉起子进程,子进程不一定继承父进程的凭据上下文。如果凭据只注入到主进程,分布式 worker 访问外部资源时就会认证失败。解决办法是把凭据代理挂在节点级别的守护进程上,让所有子进程都能通过本地 socket 访问,而不是把令牌绑定到具体作业进程。

第三,策略误伤。L7 策略比网络层策略精细,意味着误伤的可能性也更高。某次规则把 path 写得太宽或太窄,可能直接挡住正常业务。务必在策略变更时做灰度发布,先写审计模式观察一段时间,再切强制执行。

6.2 后续值得跟进的方向

从我个人的判断来看,这套“沙箱 + 凭据代理 + L7 策略”的架构接下来会朝着两个方向演进。

第一个方向是机密计算。GPU 的显存数据在物理内存里是明文,如果宿主机被攻破,其他租户的恶意进程理论上可以尝试侧信道读取。把凭据代理和硬件信任根结合起来,让令牌签发和策略判定都在可信执行环境里完成,可以显著缩小攻击面。我已经看到和 GPU 机密计算相关的技术在逐步落地,这会是高档安全需求的首选路径。

第二个方向是策略和调度的联动。现在的 L7 策略主要管“请求能不能过”,但还可以往前一步:让策略影响调度。比如某个模型服务只允许特定租户调用,那这个服务的 GPU 资源就可以优先分配给对应租户的节点,避免跨租户的资源竞争。策略从“防守”变成“编排”,这个想象力很大。

如果是刚开始搭建这套体系,我的建议是别一上来就追求功能齐全,先把沙箱做扎实,凭据代理跑通,L7 策略选一条业务路径做试点。一次只推进一层,每层都留观察期,等积累足够多的运行数据再扩展。毕竟安全体系这东西,最怕的不是少一个功能,而是功能太多,最后谁都不知道哪条策略在保护什么。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进