2、容器安全基础:容器运行时安全、镜像安全、容器逃逸防护

聊到容器安全,我见过太多团队把精力全扑在镜像扫描上,觉得只要镜像干净就万事大吉。说实话,这远远不够。容器安全是个立体工程,得从镜像、运行时、逃逸防护三个维度同时下手。今天我就把这几个核心模块拆开揉碎了讲给你听。

2.1 镜像安全:你的基础镜像靠谱吗?

镜像安全是容器安全的起点。你想想看,如果基础镜像里就藏着漏洞,那跑起来的容器等于裸奔。我个人习惯是,所有镜像必须经过三道关卡才能进生产环境。

2.1.1 基础镜像的选择

别图省事直接拉 ubuntu:latest。我建议用官方维护的 Distroless 镜像,或者 Alpine 这种极简镜像。为什么?因为攻击面小。Distroless 镜像里连 shell 都没有,你想进去执行命令?门都没有。

核心原则:镜像越小,攻击面越小。别把不需要的工具包塞进镜像里。

我在项目中遇到过一件事:有个团队用了一个第三方维护的 Node.js 镜像,结果里面预装了 curl、wget、甚至 Python。攻击者通过应用漏洞拿到 shell 后,直接用 curl 下载了挖矿脚本。嗯,这就是典型的镜像臃肿带来的灾难。

2.1.2 镜像扫描与漏洞管理

镜像扫描不是跑一遍就完事了。你得建立持续扫描机制。我常用的工具有 TrivyClairAnchore。其中 Trivy 我个人用得最多,因为它快,而且支持多种格式。

扫描命令很简单:

trivy image --severity HIGH,CRITICAL myapp:latest

但这里有个坑:扫描出漏洞后怎么办?不是所有漏洞都要立刻修。你得评估漏洞的可利用性。比如一个需要本地访问权限的漏洞,在容器化环境里其实风险很低。

我的经验:建立漏洞分级制度。CRITICAL 级别的漏洞 24 小时内必须修复,HIGH 级别的 72 小时内。LOW 和 MEDIUM 可以走正常迭代周期。

2.1.3 镜像签名与完整性校验

镜像在传输过程中被篡改怎么办?用 NotaryCosign 做签名。我习惯在 CI/CD 流水线里加一步:构建完镜像后自动签名,部署时强制校验签名。

# 使用 Cosign 签名
cosign sign --key cosign.key myregistry/myapp:latest

# 部署时校验
cosign verify --key cosign.pub myregistry/myapp:latest

你想想看,如果没有签名校验,攻击者只要拿到镜像仓库的权限,就能把恶意镜像推上去。我曾经见过一个案例,攻击者替换了镜像里的一个动态链接库,所有跑这个镜像的容器都成了肉鸡。

2.2 容器运行时安全:跑起来之后怎么管?

镜像安全搞定了,容器跑起来之后才是真正的战场。运行时安全说白了就是两件事:限制权限监控行为

2.2.1 最小权限原则

别让容器以 root 身份运行。这是最基本的要求。我见过太多人图省事,直接 docker run --privileged。这等于把整个宿主机的控制权拱手让人。

正确的做法是:

  • 使用非 root 用户运行容器
  • 去掉不必要的 Linux Capabilities
  • 启用只读文件系统

举个例子:

docker run \
  --user 1000:1000 \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  --read-only \
  myapp:latest

这里我解释一下:--cap-drop ALL 先丢掉所有权限,然后只添加你真正需要的。比如你的应用只需要监听 80 端口,那就只加 NET_BIND_SERVICE

警告:永远不要在生产环境使用 --privileged 参数。如果你需要挂载设备或操作内核,请使用更细粒度的 Capabilities 控制。

2.2.2 Seccomp 与 AppArmor

这两个东西很多人觉得复杂就跳过了。其实没那么难。Seccomp 限制容器能调用的系统调用,AppArmor 限制文件访问和网络权限。

我建议的做法是:先用默认的 Seccomp 策略跑一段时间,然后通过审计日志分析容器实际用到了哪些系统调用,再生成定制策略。

一个简单的 Seccomp 配置示例:

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": ["SCMP_ARCH_X86_64"],
  "syscalls": [
    {
      "names": ["read", "write", "open", "close", "mmap"],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

嗯,这里要注意:默认动作是拒绝所有系统调用,只放行你明确允许的。这样就算攻击者拿到了 shell,他能做的事情也极其有限。

2.2.3 运行时监控与异常检测

运行时监控我推荐 Falco。它是 CNCF 的项目,专门做容器运行时安全监控。Falco 能检测到很多异常行为,比如:

  • 容器内启动了新的 shell 进程
  • 容器尝试挂载宿主机目录
  • 容器内出现了网络扫描行为

我曾经用 Falco 抓到过一个真实案例:有个容器突然启动了 bash 进程,而且开始执行 curl 命令。Falco 立刻告警,我们一看,果然是攻击者通过 RCE 漏洞进来了。要不是 Falco 及时发现,后果不堪设想。

Falco 的规则配置也很直观:

- rule: Shell in Container
  desc: 检测容器内启动 shell
  condition: container.id != "" and proc.name = bash
  output: "Shell spawned in container (user=%user.name container=%container.id)"
  priority: WARNING

2.3 容器逃逸防护:守住最后一道防线

容器逃逸是最高风险的安全事件。一旦发生,攻击者就拿到了宿主机的控制权。说白了,整个集群都沦陷了。

2.3.1 常见的逃逸路径

我总结了几种最常见的逃逸方式:

逃逸方式 原理 防护措施
挂载宿主机目录 容器挂载了 /var/run/docker.sock 禁止挂载 docker.sock
内核漏洞利用 利用内核漏洞获取宿主机权限 及时更新内核,启用 Seccomp
Capabilities 滥用 利用 SYS_ADMIN 等 Capabilities 最小化 Capabilities
容器逃逸工具 使用 cdk、amicontained 等工具 运行时监控,限制系统调用

我记得有一次,一个开发为了方便调试,在容器里挂载了 /var/run/docker.sock。结果攻击者通过应用漏洞进入容器后,直接执行了 docker run -v /:/host --privileged,瞬间拿到了宿主机的 root 权限。这就是典型的低级错误。

2.3.2 逃逸检测与阻断

逃逸检测不能只靠人工。我建议用 Falco 加上 AppArmor 做双重防护。Falco 负责检测异常行为,AppArmor 负责阻断越权操作。

一个关键的检测规则:

- rule: Mount Docker Socket
  desc: 检测容器内挂载 docker.sock
  condition: container.id != "" and mount.dst = /var/run/docker.sock
  output: "Docker socket mounted in container (user=%user.name)"
  priority: CRITICAL

另外,我强烈建议启用 用户命名空间(User Namespace Remapping)。这个功能可以把容器内的 root 用户映射成宿主机上的非特权用户。就算攻击者逃逸了,他在宿主机上也没有实际权限。

避坑指南:我曾经在配置用户命名空间时踩过坑。如果你用了 NFS 或某些存储插件,用户命名空间可能会引发权限问题。建议先在测试环境验证,再推生产。

2.3.3 纵深防御体系

逃逸防护不是靠单一手段就能搞定的。你得建立纵深防御体系:

  1. 镜像层:最小化镜像,去除不必要的工具
  2. 配置层:最小权限原则,禁用特权模式
  3. 内核层:Seccomp + AppArmor + 用户命名空间
  4. 监控层:Falco 实时检测,自动告警
  5. 响应层:自动化阻断,容器隔离

你想想看,攻击者要突破这五层防线,得花多大功夫?就算他突破了前四层,最后一层的自动化响应也能在几秒内把容器隔离掉。

2.4 总结

容器安全没有银弹。镜像安全、运行时安全、逃逸防护,这三块缺一不可。我个人习惯是:从镜像开始管,在运行时持续监控,最后用逃逸防护兜底。记住一句话:安全不是做一次就完事,而是持续的过程。

嗯,今天就聊到这里。下一章我会讲 Kubernetes 集群安全,包括 RBAC、网络策略、Pod 安全标准这些内容。到时候见。