ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

GPU集群安全加固:OpenShell沙箱、凭据代理与L7策略实战解析

GPU集群安全加固:OpenShell沙箱、凭据代理与L7策略实战解析

前阵子给内部的一批GPU训练节点做安全加固,接触到了NVIDIA OpenShell这套方案。OpenShell做的事情,用一句话概括:把用户和GPU节点之间的每一次shell交互都收拢到一个受控边界里,然后通过沙箱、凭据代理和L7策略执行这三层机制,管住“谁能跑、跑在哪、拿到什么凭据、执行什么操作”。它解决的是AI集群里最常见的头痛问题:一个有权访问训练节点的工程师,实际需要接触的底层资源,和他一旦拿到shell之后能碰到的系统能力,中间永远隔着一条巨大的鸿沟。

这文章主要写给三类人看:给GPU集群做运维的SRE、管机器学习平台基础设施的MLOps、以及负责安全评审但又不想天天追着工程师要权限的同事。里面不会有太多炫技的架构图,更多是原理拆解、配置示例,还有我实际落地时踩过的坑。如果你现在正被“用户要root才能跑训练”“开发机上一堆AK/SK散落着”“出了事查不到是谁敲了那条命令”这几个问题困扰,那OpenShell这套思路值得你花十分钟认真看一下。

1. 为什么AI集群需要一个OpenShell

1.1 场景痛点:裸奔的GPU节点

做AI基础设施的人应该都有这种经验:GPU节点一旦搭好,很快就会被当成一台普通Linux服务器来用。算法工程师要登录上去看nvidia-smi、改环境变量、装PyTorch、调试分布式训练脚本,甚至直接拿节点的公网IP往外传数据。一开始大家还守着“不要给root”的底线,但用着用着就变味了。有的人为了装一个系统级依赖,软磨硬泡要求sudo权限;有的人直接把.docker目录挂到工作目录,顺手把宿主机暴露给了容器;还有人把云厂商的密钥写进~/.bashrc,整台机器上所有账号、所有会话、所有历史命令都能读到。

这个问题本质上是“shell访问”的安全边界没有定义清楚。传统做法是靠堡垒机加一层跳板,但堡垒机只能管住“谁能登录”,管不住“登录之后这个人能跑什么”。更麻烦的是,GPU节点的价值恰恰在于它和真实训练任务、真实数据紧密耦合,你要真把权限锁死,业务又跑不动。所以OpenShell这类组件出现的背景很明确:它不是要取代Kubernetes、不是要取代堡垒机,而是要在用户和GPU之间插入一个可编程的、可审计的、能感知应用层命令的执行闸门。

1.2 OpenShell的定位:在用户与GPU之间加一道闸门

OpenShell整体的工作方式,和我之前用的各类容器安全方案都不太一样。常规做法是:给用户一个容器,容器里能跑什么,由镜像和默认的Docker配置说了算,用户只要拿到一个能访问宿主机的挂载点,或者一个带有特权的容器,前面的隔离就白做了。OpenShell则是把“一次交互式shell会话”本身作为最小管控单元,用户无论通过SSH、Web终端,还是内部跳板机进入节点,都会被强制引导到一个OpenShell管理的沙箱会话里,而不是直接落到宿主机bash。

这个沙箱会话启动时,会完成三件事:第一,用内核隔离能力把用户的进程、网络、文件系统和宿主机隔开,让命令跑在一个封闭环境里;第二,在会话内挂载一个凭据代理,用户访问对象存储、镜像仓库、代码仓库时,拿到的是动态签发的短期令牌,而不是一个永久AK;第三,shell中每一条要执行的命令,都会先经过L7策略引擎的检查,通过后才真正交给execve执行。这三层并不是各自独立的插件,而是串在一条链路上:先决定进程落在哪、再决定访问外部资源时用什么身份、最后决定命令本身是否被允许。

这套定位的价值在于,权限控制的粒度足够细,而且用户几乎无感。我做验证时让算法组同学照常用python train.py跑任务,他们只感觉到首次进入节点时多了一步认证,之后的训练、调试、看日志流程基本没变化。但安全侧已经完全不一样了:任何一条rm -rf、任何一次ssh到另一台机器、任何一次读取宿主机敏感文件的尝试,都会被策略引擎拦下来并记录到审计日志里。

2. 沙箱层:先让命令跑在一个“密闭容器”里

2.1 沙箱的构成

OpenShell的第一层是沙箱,这一层解决的是“进程运行环境隔离”的问题。你可以把它理解成一个加固过的容器运行时,但它的目标不是像Docker那样“方便打包应用”,而是“让用户的所有命令都跑在一个默认不信任的环境里”。沙箱内部会用到内核提供的好几类隔离能力,缺失任何一类,这个沙箱的安全水位都会明显下降。

第一类是命名空间隔离。OpenShell会为每个用户会话创建独立的PID、mount、network、UTS和IPC namespace。PID namespace保证用户只能看到自己会话内的进程,不能通过ps aux观测到宿主机上的其他任务;mount namespace用来控制文件系统视图,把宿主机的/换成沙箱自己的根目录;network namespace则让沙箱内的网络栈与宿主机隔离,即便用户命令里有网络探测行为,也不会直接看到节点上的所有网卡和监听端口。

第二类是资源控制。沙箱会给每个会话分配固定的CPU、内存、GPU设备配额,防止一个跑飞的数据预处理脚本把整台节点拖死。配置上不光是cgroup限制,还会对可以访问的GPU设备做显式过滤:默认情况下会话内只能看到系统分配给它的那几块GPU,而不是节点上的全部/dev/nvidia*设备。

第三类是系统调用过滤。OpenShell默认会加载一套seccomp profile,把用户进程可用的系统调用裁剪到日常开发和训练所需的最小集合。像mount、ptrace、perf_event_open这类高风险调用,普通会话一律拒绝。再配合LSM层面的能力降级,沙箱里的进程即使是root用户,也拿不到CAP_SYS_ADMIN、CAP_NET_ADMIN这类高危capability。这套组合拳打下来,沙箱的强度已经比普通Docker容器高一个级别。

2.2 沙箱创建与接入步骤

实际接入OpenShell时,第一步是定义沙箱模板。内部我们习惯把沙箱模板和训练镜像放在一起管理,比如一个典型的PyTorch沙箱模板:

openshell sandbox create \ --name lab01 \ --image registry.internal/pytorch:23.01 \ --cpu 8 \ --mem 32g \ --gpu 1 \ --gpus 0 \ --readonly-root \ --writable /workspace,/tmp \ --device /dev/nvidia-uvm,/dev/nvidiactl \ --seccomp-profile default \ --cap-drop ALL

这里有几个参数值得展开说。--readonly-root让整个根文件系统只读,用户能写入的只有显式声明的/workspace和/tmp两个目录,这样一来,即便用户通过Python或包管理器写入了一个恶意脚本,也很难污染镜像本身。--gpu 1 --gpus 0意思是为该沙箱分配第一张GPU卡,宿主机上的/dev/nvidia0会被映射进沙箱,但/dev/nvidia1、/dev/nvidia2这些设备对用户完全不可见。--cap-drop ALL是切掉所有capability,之后如果某个场景确实需要额外权限,再单独加回,而不是默认放权。

第二步是把用户登录入口绑定到沙箱。最常见的做法是改SSH配置,让用户通过SSH登录时直接进入OpenShell会话:

sshd_config ForceCommand openshell session --profile lab01 --user %u

配置好之后,用户执行ssh alice@gpu-node-01,会先完成OpenShell身份认证,然后被放进一个属于她的、隔离的沙箱bash中。此时她运行nvidia-smi能看到分配的GPU,但运行lsblk只能看到沙箱内的虚拟块设备,运行ip addr看到的也是一个非宿主的网络接口。这个体验很接近“一个被装进保险柜里的Linux终端”,用户没有明显卡顿,但操作边界已经完全变了。

2.3 沙箱的边界与限制

沙箱不是虚拟机,这是我落地过程中反复给团队强调的一点。它共享宿主机内核,所以如果宿主机内核本身有严重漏洞,沙箱理论上存在被击穿的风险。OpenShell能做的,是用seccomp、capability drop、只读文件系统这些手段把漏洞利用成本拉高,但要在关键业务集群上使用,宿主机内核自身的补丁管理和安全基线一样不能放松。

另一个实际问题是与GPU驱动的兼容性。沙箱容器里如果要跑CUDA程序,镜像里的驱动版本和宿主机驱动版本必须匹配,否则用户一进沙箱跑torch.cuda.is_available()就返回False,或者干脆报CUDA driver version is insufficient。这里我建议的做法是:宿主机驱动固定一个版本,所有沙箱镜像基于这个版本重新构建CUDA基础镜像,同时在沙箱模板中显式挂载宿主机/usr/lib/x86_64-linux-gnu/libcuda.so.1这类驱动库文件,而不是让每个镜像都自带一份驱动。

还有一类坑是临时数据丢失。虽然沙箱默认给/workspace加了可写权限,但如果用户习惯把中间结果写到/root或/home下,会话一旦退出,这些数据会随着沙箱销毁而消失。考虑到训练任务经常要跑几十个小时,我建议在OpenShell策略里明确约定:所有需要持久化的数据必须放在共享存储路径下,沙箱内其他目录一律视为临时空间。我们在实践里甚至会把/workspace自动挂载成NFS或PVC,这样用户既觉得“和本地写文件一样”,数据又不会丢。

3. 凭据代理:让令牌不再散落在每台机器上

3.1 凭据代理的运作流程

GPU节点上最容易被忽视的安全隐患,是长期凭据。很多团队为了省事,会把云服务商的AK/SK、镜像仓库的登录密码、GitLab的Personal Access Token直接写进镜像或工作目录里。用户一旦有了shell权限,把这些凭据翻出来是分分钟的事。OpenShell的第二层,凭据代理,解决的就是这个问题。

凭据代理的思路很简单:用户环境里不放任何长期凭据,所有凭据统一集中在一个受保护的凭据后端(比如Vault、内部KMS),用户会话启动时,OpenShell在沙箱内启动一个本地代理进程,监听在/run/openshell/cred.sock。当用户的应用需要访问某个外部服务时,应用先把请求发给本地代理,代理根据请求的目标服务,匹配对应的访问策略,然后向中央凭据后端申请一个短期令牌,最后把这个令牌返回给应用。整个过程中,用户看不到、摸不到原始长期凭据,拿到手的只是有效期十几分钟的一次性凭证。

这个模式可以用一个生活类比解释:你住酒店时,前台帮你订外卖,外卖小哥不需要知道你房间的房卡怎么配。以前是直接把房间钥匙给外卖小哥,现在是你只告诉前台“我要一份饭”,前台用自己手里的总卡帮你去取,取回来交给你,而且这张总卡只能开你房间的门,有效期只有半小时。对于AI集群来说,好处非常直接:即使某个沙箱被攻破,攻击者拿到的也是临时令牌,几分钟后就会失效,无法横向到其他系统。

3.2 接入方案与配置示例

OpenShell的凭据代理支持配置化接入,我这边用了Vault作为凭据后端,配置写在/etc/openshell/broker.yaml里:

broker: socket: /run/openshell/cred.sock backend: vault vault_addr: https://vault.internal:8200 auth_method: kubernetes ttl: 15m policies: - target: registry.example.com role: model-pull type: dynamic - target: s3.example.com role:>source <(openshell cred env --target s3.example.com)

这样当前会话就临时注入了对应的AWS_ACCESS_KEY_ID和AWS_SECRET_ACCESS_KEY。对于没有原生支持代理SDK的应用,OpenShell还提供了文件挂载方式:把临时凭据写入一个内存tmpfs路径,应用从该路径读取,会话结束后文件随即消失。

我在部署时遇到过比较棘手的一点:部分老旧的训练脚本会硬编码读取~/.aws/credentials,根本不会走环境变量或代理SDK。对这类脚本,我不推荐改脚本,而是用OpenShell的cred mount --path ~/.aws/credentials命令,在会话启动时自动生成一个带正确权限的临时凭据文件,脚本无感知,凭据同样不落盘。这个方案对业务改动最小,落地阻力也小很多。

3.3 凭据生命周期管理

如果你的团队还没有统一的凭据后端,我强烈建议不要跳过这一层直接套OpenShell,否则凭据代理只能退化成“密码本读取工具”。凭借据的生命周期管理,OpenShell有几个设计原则值得借鉴。

第一是动态签发。Vault这类后端支持动态生成数据库账号、云厂商临时密钥,用完即销毁。生产环境里我都是优先配dynamic类型,而不是把静态账号密码放进去长期使用。第二是会话绑定。代理签发出来的令牌会和OpenShell会话ID绑定,用户如果把令牌从沙箱复制到另一个节点上,远端会因为缺少会话上下文而拒绝使用。第三是审计闭环。每次凭据申请的时间、目标服务、使用者、会话ID都会写入审计日志,后期做安全回溯时,不用再靠猜。第四是回收。会话退出时,代理会主动调用凭据后端的revoke接口,把令牌提前吊销,而不是等它自然过期。

实际运营中,最容易被忽视的其实是环境变量泄漏。虽然已经用短期令牌,但用户自己可能会把openshell cred env的输出重定向到日志文件里,或打印到终端,导致令牌出现在日志中心。我的建议是,除了代理侧做好TTL控制,还要在L7策略层加上一条规矩:禁止把包含SECRET、TOKEN、AK等关键字的环境变量内容写入日志文件。否则辛辛苦苦改造的凭据体系,最后还是可能被一条echo $AWS_SECRET_ACCESS_KEY > debug.log给击穿。

4. L7策略执行:在第七层给命令装上“红绿灯”

4.1 为什么是L7:IP白名单不够用

有了沙箱和凭据代理,命令的执行环境已经被限制住了,但还不能解决“用户执行了不该执行的命令”这个问题。你可能会想:防火墙做一下限制,只允许SSH端口通过,不允许访问内网其他机器,不就行了?但对GPU集群来说,安全策略如果停留在IP和端口这个层面,粒度太粗了。用户执行python train.py和python -c "import os; os.system('rm -rf /workspace')",从网络层看,动用的端口、连接的远端地址完全一样,可实际操作天差地别。只靠防火墙,根本区分不了这两类行为。

L7策略执行,就是在应用层对shell会话内容做实时检查。OpenShell拦截的位置在命令真正执行之前:用户每敲一条命令,shell会把命令、参数、当前目录、用户身份、会话上下文打包成一个策略请求,交给策略引擎评估,返回allow才执行,返回deny则给出拒绝原因并记录审计日志。这样权限控制从“谁能通过22端口登录”细化到了“用户alice能不能在/workspace目录下用python运行train.py脚本”。

4.2 策略语言与执行点

OpenShell的策略引擎默认支持OPA/Rego风格的策略,也支持简单的CEL表达式。我用得最多的还是Rego,因为写起来更灵活,用户可以自己定义函数做正则匹配。一个最基础的策略长这样:

package openshell default allow = false # 允许读取GPU状态 allow { input.command == "nvidia-smi" } # 允许在 /workspace 下运行 python 脚本 allow { input.command == "python" startswith(input.args[0], "/workspace/") } # 允许在 /workspace 内查看文件 allow { input.command == "ls" startswith(input.cwd, "/workspace") } # 禁止强制删除任何目录 deny { regex.match("^rm -rf .*", concat(" ", input.args)) }

这里input里包含的信息很多,除了命令和参数,还有input.user、input.group、input.session_id、input.cwd、input.env、input.shell等字段。我在定策略时,最少会用到前五个字段,因为只有把这些信息全部串起来,策略才不会误伤正常用户。比如用户在自己的工作目录下删临时文件很正常,但删除别的目录下的文件就要警惕,这种情况写进同一条策略里,判断逻辑会变得非常复杂,不如拆成多条具体规则,让命中路径更清晰。

另一个重要设计是deny优先级高于allow。不管多少条allow规则匹配,只要一条deny命中,命令就必须被拦截。这个设计能从根上防止策略漏洞,避免出现“因为某条宽松规则放行了、其他拒绝规则就失效”的情况。

4.3 典型策略示例

我结合自己的场景整理了四类最常用的L7策略模板,第一类是训练相关命令白名单,第二类是数据访问控制,第三类是危险命令黑名单,第四类是网络命令限制。这里用表格做一个对照:

策略类别允许示例拒绝示例
训练任务python train.py、pytest /workspace/testspython -c "import os; ..."
数据文件ls /workspace、cat /workspace/README.mdcat /etc/shadow、cat /proc/1/environ
危险操作find /workspace -name "*.py"rm -rf /、mkfs、dd if=/dev/zero
网络行为curl https://s3.example.com/pathssh root@10.0.0.x、scp /etc/passwd user@host

最容易被忽略的是网络行为的限制。很多训练脚本会通过curl或wget拉取公开数据集,但如果完全放开,用户就可以用ssh直接登录内网其他机器,横向移动就是几秒钟的事。所以我对网络命令的策略一般这样写:允许curl和wget访问公司内部的几个白名单域名,但禁止任何形式的ssh、scp、sftp命令。另外像debugfs、unshare、nsenter这类能让用户突破容器上下文、探查宿主机内核的命令,全部直接拒绝,这比单纯依赖沙箱的seccomp更贴近业务表达。

4.4 策略调试与灰度

L7策略引擎最怕误杀,策略写得严,用户正常的命令被误伤了,第一反应就是绕过运维直接抱怨;策略写得松,又起不到安全效果。我的调试思路是先让策略处于audit模式,只记录日志不下发拦截,观察几天,再针对拦截日志调整规则,最后灰度推给部分用户组。

openshell policy watch --profile lab01 --mode audit --duration 72h openshell policy diff --from audit-profile --to enforcement-profile --summary openshell policy apply --profile lab01 --namespace ai-team --strategy rolling

第一次在训练集群上启用时,我开了三天audit,结果发现有一个标注为“禁止chmod”的规则,把所有用户执行chmod +x train.sh的正常操作也拦下来。我原本的意图是禁止对系统文件改权限,但忘了chmod在用户自己的脚本上其实是无害的。后来我把规则改成了“只禁止对/etc、/usr等系统路径下的文件执行chmod”,误杀量瞬间降为零。这个案例也说明:策略规则最好具体到路径和用户,不要为了省事写一个高风险命令黑名单,然后期待它在所有场景下都精准。

5. 常见问题与排查实录

5.1 沙箱启动慢或无法访问GPU

很多团队第一次接入OpenShell时,最常遇到的报错是用户进入沙箱后nvidia-smi正常,但一跑深度学习训练就报CUDA错误。这个问题大半出在镜像和宿主机驱动不匹配上。排查步骤很简单:先在宿主机执行nvidia-smi | head -n 5确认驱动版本,再进沙箱执行ls -l /usr/lib/x86_64-linux-gnu/libcuda.so.1,看看沙箱内用的印driver是不是宿主机的。如果两边版本号对不上,最省事的方案是改沙箱模板,把宿主机对应的驱动库目录只读挂载进去。

还有一类问题是沙箱启动特别慢,有的甚至要三四秒。这个通常是镜像体积太大或者沙箱初始化时拉了太多外部依赖。我的建议是OpenShell沙箱镜像做得越“原生”越好,基础镜像只装CUDA、Python运行环境,不要在里面预装一堆训练框架,训练框架通过会话内的只读工作区挂载进来,避免每次启动都解压几个GB的包。

5.2 凭据代理获取不到令牌

凭据代理最常见的报错是permission denied while reading socket,一般原因不是凭据后端出问题,而是沙箱内用户对/run/openshell/cred.sock没有访问权限。OpenShell在创建会话时,会把代理socket的属主设置为会话用户,但如果沙箱模板里用了固定的uid/gid,而用户登录时的uid和它不一致,就会出现权限错位。解决方式是在沙箱模板里用--run-as参数显式指定用户uid,并确保所有用户都在同一个openshell组内。

另一个高频问题是Vault后端不可达。内部的Vault地址如果配置成了公网域名,但沙箱的网络namespace自己有一套DNS,往往解析不到内网域名。排查时先openshell broker health看代理进程状态,再curl一下Vault地址,如果网络不通,就需要在网络namespace配置里加一层代理或放行内网DNS。

5.3 L7策略误杀与绕过

策略误杀一般有两种,一种是策略写得太宽,比如禁止所有rm命令,结果用户删除/workspace/tmp下的临时文件也被拦;另一种是策略写得太窄,比如只允许python一个命令,用户用python3就立刻被deny。针对第一种,我会把规则改成“禁止删除系统关键路径”,并在deny规则里增加排除条件;针对第二种,最好一开始就把命令别名和常见二进制版本都列进白名单。

至于绕过,我见过比较典型的行为是用户通过Python的subprocess模块去执行shell指令,比如python -c "import subprocess; subprocess.call(['rm','-rf','/foo'])"。这里如果只拦截顶层命令python,其实是拦不住的。所以L7策略不能只看第一层命令,还必须结合沙箱的能力降级和系统调用过滤,让用户即使诱导进程做危险调用,权限也不足以造成实际破坏。OpenShell里的做法是同时传入input.process.parent_cmdline字段,策略引擎看到父进程是python且参数中有subprocess调用时,会自动提高检查等级。

5.4 快速排查口诀

排查OpenShell问题时,我给自己定了一个固定的检查顺序,遇到问题先走一遍,基本能定位90%的情况:

  1. 先确认用户是不是真的进入了OpenShell沙箱,执行echo $OPENSHILL_SESSION_ID看有没有会话ID。
  2. 看GPU是否可见,执行nvidia-smi -L,看不到就查驱动映射。
  3. 查凭据代理状态,执行env | grep -i OPENSHILL和openshell broker health。
  4. 翻策略审计日志,执行openshell policy audit --follow,看刚才那条命令是被哪个规则拦掉的。
  5. 最后看宿主机内核日志和OpenShell服务日志,定位是沙箱层、代理层还是策略层的问题。

这套顺序帮我省了很多时间。特别是第三步,很多用户一报错就说“访问S3失败”,其实不是网络问题,是代理socket权限没配好,白白查了几小时防火墙。

6. 一些实践心得,以及后续还能怎么扩展

OpenShell这套机制在内部跑了近两个月,我最大的体会是:安全方案能不能落地,关键不在于规则写得多复杂,而在于用户侧的负担有多小。沙箱保证环境隔离,凭据代理保证不泄露长期密钥,L7策略保证命令行为可控,三者凑在一起,才形成了一条完整闭环。单独拎出其中一个去用,效果都会大打折扣——没有沙箱,L7策略拦住了命令但拦不住进程逃逸;没有凭据代理,沙箱做得再好,长期密钥还是会从一个被攻破的会话里流出去。

如果后续要继续扩展,我比较想往两个方向做:一个是把L7策略和模型训练任务的数据血缘打通,比如记录下每个用户会话访问了哪些数据集、跑了哪些模型训练脚本,方便训练溯源;另一个是增强会话内的动态授权,让用户申请一次临时高危操作的权限时,能走审批流自动放行,而不是让运维手工改策略。OpenShell这套设计给了一个很好的起点,后面能做的东西其实还很多。

返回列表