2. 容器基础与隔离机制

面试官问“容器是怎么隔离的”,很多候选人会脱口而出“靠 Namespace 和 Cgroups”。这个回答只能算及格。真正拉开差距的,是能否把“隔离”拆成几个正交的维度,并说清每种机制解决什么问题、边界在哪里、组合起来又留下哪些攻击面。本节就按这个思路展开。

一、先建立一张“隔离维度”地图

容器隔离不是单一技术,而是多层机制的叠加。可以按“看得见什么、用得了多少、能做什么、被允许做什么”四个问题来划分:

  • Namespace:解决“看得见什么”。让进程拥有独立的视图,看不到宿主机和其他容器的资源。
  • Cgroups:解决“用得了多少”。限制和统计 CPU、内存、IO 等资源的使用量。
  • Capabilities:解决“能做什么”。把 root 的超级权限拆成细粒度能力,按需授予。
  • Seccomp:解决“能调用哪些系统调用”。从内核入口处过滤 syscall。
  • AppArmor / SELinux:解决“被允许访问哪些对象”。基于路径或标签的强制访问控制。

面试中如果能把这张地图先画出来,再逐层深入,基本就掌握了主动权。下面逐层拆解。

二、Namespace:视图隔离的八种类型

Namespace 是容器“看起来像一台独立机器”的根本原因。Linux 目前提供八种,面试常问的是前六种:

Namespace隔离内容典型表现
PID进程 ID 空间容器内 PID 1 是应用进程,看不到宿主机进程
Mount挂载点视图容器有独立根文件系统
Network网络设备、协议栈、端口独立 IP、独立端口空间
UTS主机名与域名容器内 hostname 可独立设置
IPC信号量、消息队列、共享内存进程间通信与宿主机隔离
User用户与组 ID 映射容器内 root 可映射为宿主机普通用户
CgroupCgroup 根目录视图隐藏宿主机 cgroup 层级
Time时钟偏移视图较新内核,用于时间命名空间

面试追问高频点:“容器里的 root 和宿主机的 root 是同一个吗?” 默认情况下,如果不启用 User Namespace,容器内 UID 0 就是宿主机 UID 0,一旦逃逸就是真 root。启用 User Namespace 后,容器内 UID 0 可映射到宿主机某个非特权 UID(如 100000),逃逸后也只是普通用户。这就是 rootless 容器的核心原理。

另一个追问:“Namespace 能防住资源耗尽吗?” 不能。Namespace 只管“看得见”,不管“用多少”。一个容器可以 fork 炸弹把宿主机内存打满,这就要靠 Cgroups。

三、Cgroups:资源限制与统计

Cgroups v2 是当前主流,采用单一层级树。面试常问的是几个关键控制器:

  • cpu.max:格式为“配额 周期”,例如 50000 100000 表示每 100ms 周期内最多用 50ms CPU,即 0.5 核。
  • memory.max:内存硬上限,超过触发 OOM Killer。
  • memory.high:软上限,超过会限流但不杀进程。
  • pids.max:限制进程数,防 fork 炸弹。
  • io.max:限制块设备 IO 带宽和 IOPS。

实际验证时,可以在容器内跑一段压测,然后读 /sys/fs/cgroup/ 下的对应文件观察 cpu.stat 中的 nr_throttled 和 throttled_usec,判断是否被限流。如果 nr_throttled 持续增长,说明 CPU 配额偏紧,应用延迟可能被拖高。

面试追问:“Cgroups 能防住所有资源攻击吗?” 不能。它主要管 CPU、内存、IO、PID 这几类,对内核对象(如 inode、文件描述符、网络连接跟踪表)的消耗无能为力。攻击者仍可能通过大量文件描述符或 conntrack 表项耗尽宿主机资源。

四、Capabilities:把 root 拆开

传统 root 权限是“全有或全无”。Capabilities 把它拆成约 40 种细粒度能力。容器默认会丢弃大部分危险能力,只保留少量必要项。面试常问的几个:

  • CAP_NET_ADMIN:配置网络接口、路由、防火墙。危险,默认丢弃。
  • CAP_SYS_ADMIN:挂载、命名空间操作等,被称为“新 root”,最危险。
  • CAP_SYS_PTRACE:跟踪其他进程,可读取敏感内存。
  • CAP_NET_BIND_SERVICE:绑定 1024 以下端口,常保留。
  • CAP_CHOWN:修改文件属主,常保留。

实践建议:用 --cap-drop=ALL 先丢弃全部,再按需 --cap-add 添加。例如一个只监听 80 端口的 Web 服务,只需 CAP_NET_BIND_SERVICE。可以用 capsh --print 或读取 /proc/1/status 中的 CapEff 字段验证当前生效的能力位图。

五、Seccomp:系统调用过滤

Seccomp 在内核入口处拦截 syscall。Docker 默认启用一个约 300 条规则的 seccomp profile,禁止 ptrace、mount、reboot 等危险调用。Kubernetes 从 1.19 起支持 RuntimeDefault 作为默认 seccomp 配置。

面试追问:“Seccomp 和 Capabilities 有什么区别?” Capabilities 控制“能不能做某类操作”,Seccomp 控制“能不能发起某个系统调用”。两者互补。例如即使有 CAP_SYS_ADMIN,如果 seccomp 禁止了 mount,也挂载不了。

自定义 seccomp 时,常用 SCMP_ACT_ERRNO 返回错误码而非直接杀进程,便于应用优雅降级。可以用 strace -f -c 统计应用实际用到的 syscall,再据此裁剪白名单,避免误杀。

六、AppArmor:路径级强制访问控制

AppArmor 基于路径做访问控制,比 SELinux 更易上手。Docker 默认的 docker-default profile 会限制容器对 /proc、/sys 的写入,禁止挂载等。Kubernetes 通过注解 container.apparmor.security.beta.kubernetes.io/<container> 指定 profile。

面试追问:“AppArmor 和 Seccomp 怎么配合?” Seccomp 管 syscall 层面,AppArmor 管文件路径和网络访问层面。一个攻击者即使能调用 open,AppArmor 仍可拒绝其打开 /etc/shadow。两者叠加形成纵深防御。

七、面试实战:如何回答“容器隔离够安全吗”

建议按“分层 + 边界 + 加固”三段式回答:

  1. 分层:Namespace 管视图,Cgroups 管资源,Capabilities 管权限,Seccomp 管 syscall,AppArmor 管对象访问。
  2. 边界:共享内核是最大风险。内核漏洞、/proc 与 /sys 泄露、CAP_SYS_ADMIN 滥用都可能逃逸。
  3. 加固:启用 User Namespace 做 rootless,--cap-drop=ALL,--security-opt no-new-privileges,只读根文件系统,RuntimeDefault seccomp,自定义 AppArmor profile,配合 gVisor 或 Kata 做更强隔离。

下面给出一份可直接参考的加固配置示例,用于面试中展示“我知道怎么落地”:

docker run -d \
  --name hardened-app \
  --user 1000:1000 \
  --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,size=64m \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  --security-opt no-new-privileges \
  --security-opt seccomp=/etc/seccomp/web.json \
  --security-opt apparmor=web-profile \
  --pids-limit=200 \
  --memory=512m \
  --cpus=1.0 \
  nginx:alpine

验证时依次检查:capsh --print 确认能力位图,cat /proc/1/status | grep Seccomp 确认 seccomp 模式为 2(filter),cat /proc/self/attr/current 确认 AppArmor profile 生效,cat /sys/fs/cgroup/pids.max 确认 PID 限制。任何一项不符合预期,都说明加固未完全生效。

最后提醒一个高频陷阱:“容器不是虚拟机”。共享内核意味着隔离强度天然弱于虚拟机。面试中主动点出这一点,并给出 gVisor、Kata Containers 等强隔离方案作为补充,会显著提升回答的深度。