容器与虚拟机的隔离边界
咖啡与风衣
9 分钟阅读
容器和虚拟机都叫"隔离",但隔离的边界完全不同:虚拟机是各自跑独立内核的强边界,容器是共享主机内核的弱边界。把容器当成虚拟机来信任,是生产环境里最常被误用的安全假设之一。
隔离来自哪一层
虚拟机(VM)由 Hypervisor 模拟出完整硬件,每个 VM 有自己的 Guest 内核、自己的系统调用入口。Guest 想碰宿主机,必须先突破 Hypervisor——这是一个被大量审计、攻击面相对收敛的薄层。
容器(container)没有自己的内核。它靠 Linux 内核的 namespaces(隔离视图)和 cgroups(限制资源)制造"各跑各的"的假象,但所有容器共享宿主内核。哪个容器能碰到内核漏洞,哪个容器就可能逃出边界。
一句话判断:VM 的信任边界在内核以下,容器的信任边界在内核以上。内核是容器共同的命门。
namespaces:隔离的是"看见什么"
Linux 提供几类 namespace,决定容器内能看到哪些资源:
| namespace |
隔离内容 |
| pid |
进程号空间,容器内看不到宿主进程 |
| net |
网络栈,独立 IP/端口/路由 |
| mnt |
挂载点,独立文件系统视图 |
| uts |
主机名 |
| user |
用户映射,容器内 root 可映射到宿主普通用户 |
| ipc |
进程间通信 |
边界条件:默认容器仍共享部分全局资源。最典型的是内核密钥环、部分 /proc 条目、/sys 的某些接口。你以为"看不见宿主进程就安全了",但宿主的 /proc/sys/kernel 等仍可能从容器内被读到或写坏——这正是很多加固项要禁掉 CAP_SYS_ADMIN 的原因。
cgroups:限制的是"用多少",不是"能不能越界"
cgroups 管 CPU、内存、IO、pids 上限,解决的是"一个容器把宿主拖垮"。它不提供安全边界,只是资源配额。经验上,只配了 cgroups 没配 seccomp/AppArmor,等于只防了"吵闹的邻居",没防"恶意的邻居"。
容器逃逸的几类根因(仅用于理解防御)
讲原理不提供可利用细节,但理解成因才能选对加固:
- 内核漏洞:容器共享内核,命中一个提权漏洞即可能拿到宿主特权。
- 危险权限与能力:给了
CAP_SYS_ADMIN、挂载宿主 /、开 privileged 模式,几乎等于没隔离。
- 配置错误:把宿主目录挂进容器 writable、用
host network 又没限制、以宿主 root 跑容器。
- 暴露的运行时 socket:把 docker.sock 挂进容器,容器内就能操控宿主 Docker 守护进程。
这些不是攻击步骤,而是体检清单:逐条对照你的部署,命中即该收。
把容器的弱边界补强
对抗"共享内核"带来的弱隔离,靠纵深而非单点:
# 1) 丢弃全部能力,按需只加回极少数
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE app
# 2) 禁止特权提权
docker run --security-opt=no-new-privileges app
# 3) 用 seccomp 默认档收窄系统调用面
docker run --security-opt=seccomp=unconfined:false app
# 4) 只读根文件系统,临时写区挂 tmpfs
docker run --read-only --tmpfs /tmp app
# 5) 非特权用户运行,避免容器内 root 即宿主 root
docker run --user 1000:1000 app
AppArmor/SELinux 提供第二层强制访问控制,限制容器进程能碰的文件、socket、能力,即使逃逸尝试拿到某种权限,也受 MAC 约束。
user namespace 是更彻底的解法:把容器内的 root 映射到宿主上一个无特权 UID,即便容器内提权成功,在宿主侧也只是一名普通用户。代价是部分存储驱动和老版本内核兼容性问题,需实测。
该用 VM 还是容器
按信任级别选:
- 跑互不信任的多租户、或跑来源不可控的代码,优先 VM,或"VM 里再跑容器"(例如每个租户一个 VM,VM 内用容器做密度)。
- 同信任域内跑自家已审计的服务,容器 + 上述加固足够,且密度和启动速度优势明显。
- 高敏感负载可用 microVM(如基于 KVM 的轻量虚拟机),把容器的快和 VM 的强边界结合。
踩坑:用 privileged: true 图方便跑需要设备的容器,等于把宿主 root 交给应用;用 hostPID/hostNetwork 调试后忘了改回,让容器暴露在宿主命名空间里。
边界是分层的,不是开关
隔离强度是一条光谱:裸进程 < 容器默认 < 容器+cap-drop+seccomp+MAC < 容器+userns < 轻量 VM < 完整 VM。安全设计要做的,是让"负载的信任级别"匹配"所选边界的强度"。把不可信代码塞进默认容器,是拿弱边界扛强威胁;把全部自家服务都塞进完整 VM,是浪费密度。边界选错方向,比不隔离还危险,因为会给你虚假的安全感。
威胁模型:你防的是谁
选边界前先明确对手。同信任域的内部服务互相不需要强隔离;跑用户上传代码的平台,每个负载都是潜在敌人,才需 VM 级边界。混淆两者要么过度工程、要么防护不足。经验上,按负载来源是否可信把工作负载分三档:自研已审用容器加加固、第三方已审用容器加 userns 加 MAC、不可信外部代码用轻量 VM 或完整 VM,再按档分配边界。
轻量虚拟机:把内核也隔离掉
为兼顾容器的快和 VM 的强边界,出现了在 KVM 或用户态内核上跑容器的方案:每个容器配一个极小的 VM 或一份独立内核,即使容器内逃逸,也只到那一层薄隔离,摸不到宿主内核。代价是略高的内存与启动开销、部分内核特性不支持,需按负载实测兼容性。
用 RuntimeClass 把边界写进调度
Kubernetes 里可用 RuntimeClass 给不同负载指定不同运行时,把不可信负载走强隔离运行时变成调度策略而非人工约定:
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: kata
handler: kata
边界条件:RuntimeClass 只是声明,节点上真装了对应运行时才生效;没装却引用会调度失败。要配合节点池与准入控制,强制高敏感负载落到具备强隔离运行时的节点。
多租户架构的边界选型
SaaS 多租户常见三档:共享进程靠应用逻辑隔离、共享集群分命名空间靠网络策略与 RBAC、每租户独立 VM 或集群靠强边界。隔离强度与成本同步递增。选档依据不是想要多安全,而是租户间信任假设与合规要求——给互不信任的租户共用进程,是把全部安全押在应用代码无 bug 上。
一份可勾选的加固清单
默认丢弃全部 Linux capabilities 按需加回;开启 no-new-privileges 禁止提权;加载默认 seccomp 与 AppArmor 或 SELinux 策略;根文件系统只读仅临时区可写;非 root 用户运行优先 user namespace 重映射;不挂 docker.sock、不 privileged、不 hostPID hostNetwork 除非有书面理由且补偿控制到位;高不可信负载走轻量 VM 运行时。把清单做成准入策略拒绝不合规的 Pod,让忘了加固在创建时就被拦下,而非靠人 review。
别迷信默认配置
容器运行时的默认加固程度因发行版和版本而异,有的默认开着部分危险能力,有的默认不加载 seccomp。上生产前用基线扫描工具核一遍实际生效的配置,而不是假设装好就安全。安全边界是配置出来的,不是产品自带的。
浏览 0
点赞 0
评论 0