mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
3301 字
9 分钟
容器安全:seccomp/AppArmor/Capabilities
2021-09-18

容器的安全模型是纵深防御,Namespace 提供第一层隔离,Capabilities 限制特权操作,seccomp 过滤系统调用,AppArmor 控制文件访问。这四层防护叠加在一起,构成了容器的安全边界。

但这个边界并非无懈可击。特权容器可以访问宿主内核的所有系统调用,内核漏洞可以突破 Namespace 隔离,配置不当的 seccomp 规则形同虚设。理解每一层安全机制的原理和局限,是构建安全容器的基础。

容器安全的历史是一部”事故驱动”的演进史。容器早期默认以 root 身份运行,Capabilities 几乎不做限制,seccomp 和 AppArmor 更是无人问津。2015 到 2016 年间,keyctladd_key 等内核密钥环漏洞接连被用来从容器提权逃逸,Docker 在 1.11(2016 年)把 seccomp 从”可选”变为默认启用,就是对这些内核攻击面的直接回应。此后逃逸事件仍在继续:2019 年的 CVE-2019-5736 允许攻击者通过 /proc/self/exe 符号链接覆盖宿主机上的 runc 二进制文件实现容器逃逸,它推动的不是 seccomp,而是 runc 自身的加载机制改造(改用 memfd_create 加载自身)以及 noNewPrivileges 的普及;2021 年的 CVE-2021-22555 则是内核 netfilter 提权逃逸,再次提醒共享内核始终是容器隔离的最大缺口。每一次逃逸事件都推动了特定安全机制的加固:Capabilities 从”全部授予”变为”最小子集”,rootless 容器从”实验性”变为”生产可用”。容器安全配置之所以复杂,是因为每一个限制都是对真实攻击的回应,而且对应关系要落到具体机制上,不能笼统归因。

前置知识#

Important
  • Ch02 Linux Namespace 深入:Namespace 是容器安全的第一层防线,理解 Namespace 的隔离边界是理解安全加固的前提

  • Ch03 Cgroup v2 深入:Cgroup 是资源限制的第二层防线,防止容器耗尽宿主资源

  • Linux 安全模块基础:DAC(自主访问控制)、MAC(强制访问控制)、Capabilities 机制

Note

本章讨论的安全机制基于当前最佳实践,新的 CVE 和防御手段会不断出现。

一、容器安全模型#

1.1 纵深防御层级#

graph TB subgraph 安全层级["容器安全纵深防御"] L1["第 1 层:Namespace<br/>视图隔离"] L2["第 2 层:Cgroup<br/>资源限制"] L3["第 3 层:Capabilities<br/>权限细分"] L4["第 4 层:seccomp<br/>系统调用过滤"] L5["第 5 层:AppArmor/SELinux<br/>强制访问控制"] L6["第 6 层:User Namespace<br/>用户映射"] L7["第 7 层:只读文件系统<br/>不可变基础设施"] end L1 --> L2 --> L3 --> L4 --> L5 --> L6 --> L7 ATTACK["攻击面"] --> L1 L1 -.->|"Namespace 逃逸"| L3 L3 -.->|"Capability 滥用"| L4 L4 -.->|"绕过 seccomp"| L5 style 安全层级 fill:#c8e6c9,stroke:#2e7d32 style ATTACK fill:#ffcdd2,stroke:#c62828

先说清一个常见的误区:容器之所以不靠 chroot 做根隔离。chroot 只改了进程看到的根目录,对 root 用户几乎不构成约束。一个在 chroot 环境里的 root 进程,可以靠 mknod 创建内存块设备直接读写宿主磁盘、靠 ptrace 附加到 chroot 外的宿主进程、或者通过重新挂载 /proc 访问宿主文件系统,这三条逃逸路径在 Ch01 容器全景 里有过交代。正因如此,容器改用 mount Namespace 配合 pivot_root 切换根文件系统,再叠加下面几层把 root 的能力收窄,才形成可用边界。chroot 在容器里更多作为 pivot_root 之后的补充手段,而不是独立的安全机制。

Warning

另一处容易漏的盲区是 /dev/shm。容器默认有独立的 /dev/shm(tmpfs),但如果用 --ipc=host 或手动把宿主的 /dev/shm 挂进容器,两个容器就能通过共享内存文件互相读写,IPC Namespace 的隔离就此失效。runc 为每个容器挂载独立 tmpfs 就是为了堵这个口子,机制详见 Ch02 Namespace 深入

1.2 容器 vs 虚拟机的安全边界#

容器共享宿主内核,这是它与虚拟机安全模型的根本差异。容器与虚拟机的整体对比详见 Ch01 容器全景,这里只关注安全维度:

安全维度容器虚拟机
内核漏洞影响影响所有容器只影响一个 VM
攻击面系统调用接口虚拟硬件接口
逃逸难度较低(内核漏洞)较高(需突破 VMM)
安全模块seccomp/AppArmor/SELinux硬件虚拟化扩展
Warning

容器共享宿主内核意味着:一个内核漏洞可能导致所有容器被逃逸,这也是容器隔离弱于虚拟机的根本原因。

二、Linux Capabilities#

2.1 从 root 到 Capabilities#

传统 UNIX 模型只有 root 和非 root 两种权限,过于粗粒度。Linux Capabilities 将 root 权限拆分为 40+ 个细粒度能力:

flowchart TB subgraph 传统模型["传统 UNIX 权限模型"] ROOT_ALL["root (UID 0)<br/>全部权限"] USER_NONE["普通用户<br/>无特权"] end subgraph Capabilities模型["Capabilities 权限模型"] CAP_ADMIN["CAP_SYS_ADMIN<br/>系统管理"] CAP_NET["CAP_NET_ADMIN<br/>网络管理"] CAP_KILL["CAP_KILL<br/>发送信号"] CAP_BIND["CAP_NET_BIND_SERVICE<br/>绑定特权端口"] CAP_CHOWN["CAP_CHOWN<br/>修改文件所有者"] CAP_PTRACE["CAP_SYS_PTRACE<br/>追踪进程"] CAP_MODULE["CAP_SYS_MODULE<br/>加载内核模块"] MORE["... 30+ 其他"] end ROOT_ALL -->|"拆分为"| CAP_ADMIN ROOT_ALL -->|"拆分为"| CAP_NET ROOT_ALL -->|"拆分为"| CAP_KILL ROOT_ALL -->|"拆分为"| CAP_BIND ROOT_ALL -->|"拆分为"| CAP_CHOWN ROOT_ALL -->|"拆分为"| CAP_PTRACE ROOT_ALL -->|"拆分为"| CAP_MODULE ROOT_ALL -->|"拆分为"| MORE CAP_ADMIN -->|" 极高风险"| DANGER["接近 root 权限"] CAP_PTRACE -->|" 极高风险"| DANGER CAP_MODULE -->|" 极高风险"| DANGER style 传统模型 fill:#ffcdd2,stroke:#c62828 style Capabilities模型 fill:#c8e6c9,stroke:#2e7d32 style DANGER fill:#ffcdd2,stroke:#c62828
# 查看所有 Capabilities
capsh --print
# 查看当前进程的 Capabilities
cat /proc/self/status | grep Cap
# CapInh: 0000000000000000
# CapPrm: 0000003fffffffff
# CapEff: 0000003fffffffff
# CapBnd: 0000003fffffffff
# CapAmb: 0000000000000000
# 解码 Capabilities
python3 -c "
caps = 0x3fffffffff
for i in range(40):
if caps & (1 << i):
print(f'CAP_{i}')
"

2.2 容器相关的 Capabilities#

Capability功能容器默认风险等级
CAP_AUDIT_WRITE写审计日志
CAP_KILL发送信号
CAP_NET_BIND_SERVICE绑定特权端口
CAP_CHOWN修改文件所有者
CAP_DAC_OVERRIDE绕过文件权限检查
CAP_FOWNER绕过文件所有者检查
CAP_SETFCAP设置文件 Capabilities
CAP_SYS_ADMIN系统管理操作极高
CAP_SYS_PTRACE追踪进程极高
CAP_NET_ADMIN网络管理
CAP_SYS_MODULE加载内核模块极高

2.3 Docker 的默认 Capabilities#

# 查看 Docker 容器的默认 Capabilities
docker run --rm alpine capsh --print | grep "Current:"
# Docker 默认授予的 Capabilities:
# CAP_AUDIT_WRITE, CAP_CHOWN, CAP_DAC_OVERRIDE,
# CAP_FOWNER, CAP_FSETID, CAP_KILL,
# CAP_MKNOD, CAP_NET_BIND_SERVICE, CAP_NET_RAW,
# CAP_SETGID, CAP_SETUID, CAP_SETFCAP,
# CAP_SETPCAP, CAP_SYS_CHROOT
# 添加/删除 Capabilities
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx
docker run --cap-add=SYS_ADMIN alpine # 危险!

2.4 Capability Sets 及其关系#

每个进程有五组 Capability 集合,它们共同决定进程实际能执行哪些特权操作:

集合含义限制作用
Bounding(边界集)进程可获得 Capability 的上限任何 Capability 一旦从 Bounding set 移除,就无法再被加入任何集合
Permitted(许可集)进程可以使用的 CapabilityEffective 和 Ambient 的子集上限
Effective(生效集)进程当前正在使用的 Capability内核检查权限时看的就是这个集合
Inheritable(可继承集)exec 后可以保留的 Capability决定新程序能否继承这些 Capability
Ambient(环境集)exec 后自动生效的 Capability(Linux 4.3+)非 root 进程通过 exec 执行新程序时,Ambient 中的 Capability 自动加入新进程的 Permitted 和 Effective
flowchart TB subgraph Capability_Sets["进程的 Capability 集合"] BND["Bounding Set<br/>(上限边界)"] PRM["Permitted Set<br/>(允许使用)"] EFF["Effective Set<br/>(当前生效)"] INH["Inheritable Set<br/>(可继承)"] AMB["Ambient Set<br/>(环境,4.3+)"] end BND -->|"限制上限"| PRM BND -->|"限制上限"| INH BND -->|"限制上限"| AMB PRM -->|"是子集"| EFF INH -->|"exec 时传递"| INH_EXEC["新进程的 Inheritable Set"] AMB -->|"exec 时自动加入"| NEW_PRM_EFF["新进程的 Permitted + Effective"] style BND fill:#ffcdd2,stroke:#c62828 style PRM fill:#fff9c4,stroke:#f9a825 style EFF fill:#c8e6c9,stroke:#2e7d32 style INH fill:#bbdefb,stroke:#1565c0 style AMB fill:#e1bee7,stroke:#6a1b9a
Note

Bounding set 是单向门:只能从中移除 Capability,不能添加。一旦某个 Capability 从 Bounding set 中被 drop,该进程及其所有子进程都永远无法重新获得它。这是 Capability 机制的安全基石。

Tip

Ambient set 解决了一个长期痛点:非 root 进程 exec 新程序后,即使文件有 file capability,如果 Inheritable 为空,Capability 也会丢失。Ambient 让非 root 进程也能跨 exec 保留 Capability,这对 rootless 容器至关重要。

runc 在启动容器时的 Capability 设置流程:

  1. 在容器进程 fork 之后、exec 之前,通过 prctl(PR_CAPBSET_DROP, cap_value) 逐个将不需要的 Capability 从 Bounding set 移除。第二个参数 cap_value 是 Capability 的数值编号(如 CAP_SYS_ADMIN 为 21)
  2. 通过 capset() 系统调用设置 Permitted、Effective、Inheritable 三个集合,它们的值都不能超过 Bounding set
  3. 通过 prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, cap_value) 逐个设置 Ambient 集合
  4. exec 执行容器入口程序后,内核根据文件的 file capability 和进程的 Inheritable/Ambient 集合,重新计算新进程的 Permitted 和 Effective set
# 手动操作 Capability 集合的示例(使用 libcap 的 capsh 命令)
# 查看当前进程的五个集合
cat /proc/self/status | grep Cap
# CapInh: 0000000000000000 ← Inheritable
# CapPrm: 0000003fffffffff ← Permitted
# CapEff: 0000003fffffffff ← Effective
# CapBnd: 0000003fffffffff ← Bounding
# CapAmb: 0000000000000000 ← Ambient
# 从 Bounding set 移除 CAP_SYS_ADMIN(capability number 21)
# 移除后无法恢复
capsh --drop=cap_sys_admin -- -c "cat /proc/self/status | grep CapBnd"
# CapBnd: 0000003fffdfffff ← 第 21 位变为 0
# 同时设置 Permitted 和 Effective
# capsh 会先 drop Bounding,再 capset Permitted/Effective/Inheritable
capsh --drop=cap_sys_admin --caps="cap_net_bind_service+ep" -- -c "./my-server"
# +ep 表示添加到 Effective 和 Permitted
Warning

capsh --drop 操作的是 Bounding set,是单向的。--caps 操作的是 Permitted/Effective/Inheritable,受 Bounding set 限制。如果 Bounding set 中没有某个 Capability,--caps 也无法添加它。

三、seccomp#

3.1 seccomp 原理#

seccomp(Secure Computing Mode)限制进程可以调用的系统调用。它使用 BPF(Berkeley Packet Filter)程序在内核层面过滤系统调用:

sequenceDiagram participant APP as 容器进程 participant LIBC as glibc participant SYSCALL as 系统调用入口 participant BPF as seccomp BPF participant KERNEL as 内核 APP->>LIBC: open("/etc/passwd", O_RDONLY) LIBC->>SYSCALL: syscall(SYS_openat, ...) SYSCALL->>BPF: BPF 程序评估系统调用 alt 系统调用在允许列表 BPF-->>SYSCALL: SECCOMP_RET_ALLOW SYSCALL->>KERNEL: 执行 openat KERNEL-->>APP: 返回文件描述符 else 系统调用在拒绝列表 BPF-->>SYSCALL: SECCOMP_RET_ERRNO SYSCALL-->>LIBC: 返回 EPERM LIBC-->>APP: Operation not permitted else 默认策略 BPF-->>SYSCALL: SECCOMP_RET_LOG / SECCOMP_RET_KILL end

3.2 Docker 的默认 seccomp 配置#

Docker 默认使用 seccomp 配置文件,禁止约 44 个危险系统调用:

{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": ["accept", "access", "arch_prctl", "bind", "brk", ...],
"action": "SCMP_ACT_ALLOW"
},
{
"names": ["keyctl", "add_key", "request_key"],
"action": "SCMP_ACT_ERRNO"
},
{
"names": ["mount"],
"action": "SCMP_ACT_ERRNO",
"args": []
}
]
}

3.3 被默认 seccomp 禁止的系统调用#

系统调用原因风险
keyctl内核密钥环操作内核漏洞利用
add_key添加密钥内核漏洞利用
request_key请求密钥内核漏洞利用
mount挂载文件系统文件系统逃逸
umount2卸载文件系统文件系统逃逸
pivot_root切换根目录文件系统逃逸
bpf加载 BPF 程序内核代码执行
perf_event_open性能监控信息泄露
kcmp比较进程资源信息泄露
userfaultfd用户态页错误处理内核漏洞利用

3.4 自定义 seccomp 配置#

# 使用自定义 seccomp 配置
docker run --security-opt seccomp=/path/to/seccomp.json nginx
# 禁用 seccomp(危险!)
docker run --security-opt seccomp=unconfined nginx
# 使用 Docker 默认 seccomp 配置作为模板
wget https://raw.githubusercontent.com/moby/moby/master/profiles/seccomp/default.json

3.5 用 seccomp-tools 分析#

# 安装 seccomp-tools
gem install seccomp-tools
# 分析容器的系统调用
seccomp-tools dump -c "docker run alpine ls" | head -30
# 追踪特定系统调用
seccomp-tools trace -c "docker run alpine cat /etc/passwd"

四、AppArmor#

4.1 AppArmor 原理#

AppArmor 是 Linux 的强制访问控制(MAC)系统,基于路径定义文件访问规则:

# 查看 AppArmor 状态
sudo aa-status
# 查看容器使用的 AppArmor 配置
docker inspect mycontainer --format '{{.AppArmorProfile}}'
# Docker 默认使用 docker-default 配置

4.2 Docker 的默认 AppArmor 配置#

# 查看 docker-default 配置
sudo cat /etc/apparmor.d/docker-default
# 关键规则(简化):
# deny /sys/[^f]/** wklx, # 禁止写 /sys(除 /sys/fs)
# deny /sys/f[^s]/** wklx, # 禁止写 /sys/f(除 /sys/fs/cgroup)
# deny /sys/fs/[^c]/** wklx, # 禁止写 /sys/fs(除 /sys/fs/cgroup)
# owner /** rw, # 允许读写自己拥有的文件
# /proc/** r, # 允许读 /proc
# /dev/** rw, # 允许读写 /dev

4.3 自定义 AppArmor 配置#

# 创建自定义 AppArmor 配置
sudo cat > /etc/apparmor.d/mycontainer << 'EOF'
#include <tunables/global>
profile mycontainer flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
# 允许读 /etc
/etc/** r,
# 允许写 /tmp
/tmp/** rw,
# 禁止读 /etc/shadow
deny /etc/shadow r,
# 允许网络连接
network inet tcp,
network inet udp,
# 允许信号
signal (receive) peer=/usr/bin/docker,
}
EOF
# 加载配置
sudo apparmor_parser -r /etc/apparmor.d/mycontainer
# 使用自定义配置
docker run --security-opt apparmor=mycontainer nginx

五、Rootless 容器#

5.1 Rootless 容器原理#

Rootless 容器利用 User Namespace 将容器内的 root 映射到宿主机的普通用户,无需 root 权限即可运行容器:

graph TB subgraph 传统容器["传统容器(需要 root)"] ROOT1["容器内 UID 0<br/>= 宿主机 UID 0 (root)"] RISK1["容器逃逸 = 获得 root 权限"] end subgraph Rootless容器["Rootless 容器"] ROOT2["容器内 UID 0<br/>= 宿主机 UID 100000 (普通用户)"] SAFE2["容器逃逸 = 只获得普通用户权限"] end style 传统容器 fill:#ffcdd2,stroke:#c62828 style Rootless容器 fill:#c8e6c9,stroke:#2e7d32

5.2 Podman 的 Rootless 模式#

# Podman 天然支持 rootless
podman run -d nginx
# 底层原理:
# 1. 创建 User Namespace
# 2. 配置 UID 映射:0 → 100000, 1 → 100001, ...
# 3. 在 User Namespace 内创建其他 Namespace
# 4. 使用 fuse-overlayfs 替代 OverlayFS(无需 root)
# 5. 使用 slirp4netns 或 pasta 替代 veth pair(无需 root)

5.3 Docker 的 Rootless 模式#

# 安装 Docker rootless 模式
dockerd-rootless-setuptool.sh install
# 运行 rootless 容器
docker run -d nginx
# 限制:
# - 不能绑定特权端口(< 1024)
# - 网络性能较低(slirp4netns)
# - 不能使用 AppArmor
# - 不能修改 sysctl 参数

5.4 Rootless 容器的技术栈#

组件传统容器Rootless 容器
运行时runcrunc (rootless mode)
文件系统OverlayFSfuse-overlayfs
网络veth + bridgeslirp4netns / pasta
CgroupCgroup v2 (root)Cgroup v2 (delegation)
UID 映射User Namespace

六、安全基线与最佳实践#

6.1 容器安全检查清单#

检查项安全配置风险等级
特权容器不使用 —privileged极高
Capabilities—cap-drop=ALL + 按需添加
seccomp使用默认或自定义配置
AppArmor使用默认或自定义配置
只读文件系统—read-only
PID 限制—pids-limit=100
内存限制—memory=512m
User Namespacerootless 模式
镜像签名Docker Content Trust
镜像扫描Trivy/Snyk/Grype

清单里”不使用 --privileged”排在极高,值得说清它到底关掉了什么。--privileged 一次性做了四件事:赋予全部 Capabilities、禁用 seccomp、禁用 AppArmor、把宿主所有设备挂进容器。等于把前面几层防护全部拆掉,容器进程对宿主内核几乎和真 root 一样。验证很简单,对比一下就看得见差距:

# 普通容器的 capability 集(被裁剪)
docker run --rm alpine capsh --print
# 特权容器的 capability 集(全部恢复)
docker run --rm --privileged alpine capsh --print

--privileged 并列的高危操作是挂载 docker.sock。把宿主的 /var/run/docker.sock 挂进容器,等于把宿主 root 交了出去:容器进程通过这个 socket 调 Docker API,可以再起一个 --privileged 容器并把宿主根文件系统挂进去,逃逸就完成了。这条路径不靠任何内核漏洞,纯粹是配置失误,所以 CI 构建、监控类容器一旦需要访问 Docker,都该换成受限的 socket 代理或 rootless 方案,而不是直接挂 socket。

# Kubernetes Pod 安全上下文
apiVersion: v1
kind: Pod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000
seccompProfile:
type: RuntimeDefault
containers:
- name: nginx
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]
resources:
limits:
memory: "512Mi"
cpu: "1"

YAML 里的 allowPrivilegeEscalation: false 对应 OCI 配置里的 noNewPrivileges,它堵的是 setuid 这条路。即使容器 drop 了所有 Capabilities,只要容器里有 setuid 的二进制(比如 sunewgrp),进程 exec 它之后就会拿到完整 root,之前的 capability 裁剪全部作废。noNewPrivileges 通过 prctl(PR_SET_NO_NEW_PRIVS, 1) 让 exec 后不再因 setuid bit 提权,把 capability 模型补成闭环。机制细节在 Ch05 OCI 规范 的 config.json 安全字段里展开过。

6.3 容器安全扫描#

# 使用 Trivy 扫描镜像漏洞
trivy image nginx:latest
# 输出示例:
# nginx:latest (debian 12.4)
# ===========================
# Total: 42 (UNKNOWN: 0, LOW: 15, MEDIUM: 20, HIGH: 6, CRITICAL: 1)
# ┌──────────────┬────────────┬──────────┬───────────────────┐
# │ Library │ Vulnerability│ Severity │ Installed Version │
# ├──────────────┼────────────┼──────────┼───────────────────┤
# │ libc-bin │ CVE-2021-3042│ CRITICAL │ 2.34-9 │
# │ libssl3 │ CVE-2020-1356│ HIGH │ 1.1.1 │
# └──────────────┴────────────┴──────────┴───────────────────┘

七、动手实践#

7.1 容器安全审计脚本#

#!/bin/bash
# 容器安全审计脚本
CONTAINER=$1
echo "=== 容器安全审计: $CONTAINER ==="
echo ""
echo "1. 特权模式检查"
PRIVILEGED=$(docker inspect -f '{{.HostConfig.Privileged}}' $CONTAINER)
if [ "$PRIVILEGED" = "true" ]; then
echo " 容器运行在特权模式!"
else
echo " 容器未运行在特权模式"
fi
echo ""
echo "2. Capabilities 检查"
CAPS=$(docker inspect -f '{{.HostConfig.CapAdd}}' $CONTAINER)
echo " 添加的 Capabilities: $CAPS"
if echo "$CAPS" | grep -q "SYS_ADMIN"; then
echo " CAP_SYS_ADMIN 已添加,风险极高"
fi
echo ""
echo "3. seccomp 检查"
SECCOMP=$(docker inspect -f '{{.HostConfig.SecurityOpt}}' $CONTAINER)
if echo "$SECCOMP" | grep -q "unconfined"; then
echo " seccomp 已禁用!"
else
echo " seccomp 已启用"
fi
echo ""
echo "4. AppArmor 检查"
APPARMOR=$(docker inspect -f '{{.AppArmorProfile}}' $CONTAINER)
if [ "$APPARMOR" = "" ] || [ "$APPARMOR" = "unconfined" ]; then
echo " AppArmor 未配置"
else
echo " AppArmor: $APPARMOR"
fi
echo ""
echo "5. 资源限制检查"
MEM=$(docker inspect -f '{{.HostConfig.Memory}}' $CONTAINER)
CPU=$(docker inspect -f '{{.HostConfig.NanoCpus}}' $CONTAINER)
PIDS=$(docker inspect -f '{{.HostConfig.PidsLimit}}' $CONTAINER)
echo " 内存限制: $MEM"
echo " CPU 限制: $CPU"
echo " PID 限制: $PIDS"
echo ""
echo "6. 只读文件系统检查"
RO=$(docker inspect -f '{{.HostConfig.ReadonlyRootfs}}' $CONTAINER)
if [ "$RO" = "true" ]; then
echo " 根文件系统为只读"
else
echo " 根文件系统可写"
fi
echo ""
echo "7. 挂载检查"
MOUNTS=$(docker inspect -f '{{.Mounts}}' $CONTAINER)
echo " 挂载点: $MOUNTS"
if echo "$MOUNTS" | grep -q "/var/run/docker.sock"; then
echo " 挂载了 Docker socket,可逃逸到宿主机!"
fi

附、实践:容器安全审计#

Note

本节用真实命令审计容器的安全配置,包括 Capabilities、Seccomp、只读文件系统等。需要 Docker 环境和 root 权限。

附.1 默认容器的 Capabilities#

# 启动一个默认配置的容器
docker run -d --name sec-demo alpine:latest sleep 300
# 查看容器进程的实际 Capabilities
CONTAINER_PID=$(docker inspect -f '{{.State.Pid}}' sec-demo)
cat /proc/$CONTAINER_PID/status | grep -i Cap
CapInh: 0000000000000000
CapPrm: 00000000a80425fb
CapEff: 00000000a80425fb
CapBnd: 00000000a80425fb
CapAmb: 0000000000000000

这个十六进制掩码 a80425fb 对应 Docker 默认授予的约 14 个 Capabilities(包括 CAP_NET_BIND_SERVICECAP_KILLCAP_SETUID 等)。虽然比 root 的全部 Capabilities 少很多,但仍然包含潜在风险。

附.2 最小权限容器:丢弃所有 Capabilities#

docker run --rm --cap-drop=ALL alpine:latest sh -c 'cat /proc/self/status | grep Cap'
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000000000000000
CapAmb: 0000000000000000

所有 Capabilities 为 0,容器进程没有任何特权操作能力。这是最安全的配置,但可能导致某些应用无法正常运行(如 ping 需要 CAP_NET_RAW)。

附.3 只读根文件系统#

docker run --rm --read-only alpine:latest sh -c \
'echo "read-only fs works" && touch /tmp/test 2>&1 || echo "写入 /tmp 失败(预期行为)"'
read-only fs works
touch: /tmp/test: Read-only file system
写入 /tmp 失败(预期行为)

--read-only 让容器的根文件系统变为只读,攻击者无法写入恶意文件。如果应用需要写入临时文件,可以用 --tmpfs /tmp 挂载内存文件系统。

附.4 安全基线检查脚本#

CONTAINER=sec-demo
echo "=== 容器安全基线检查 ==="
echo "1. 镜像: $(docker inspect -f '{{.Config.Image}}' $CONTAINER)"
echo "2. 运行用户: $(docker inspect -f '{{.Config.User}}' $CONTAINER || echo '(默认 root)')"
echo "3. 特权模式: $(docker inspect -f '{{.HostConfig.Privileged}}' $CONTAINER)"
echo "4. 只读根 FS: $(docker inspect -f '{{.HostConfig.ReadonlyRootfs}}' $CONTAINER)"
echo "5. CapAdd: $(docker inspect -f '{{.HostConfig.CapAdd}}' $CONTAINER)"
echo "6. CapDrop: $(docker inspect -f '{{.HostConfig.CapDrop}}' $CONTAINER)"
echo "7. SecurityOpt: $(docker inspect -f '{{.HostConfig.SecurityOpt}}' $CONTAINER)"

安全基线建议:

  • 运行用户:非 root(USER 指令或 --user
  • 特权模式:必须为 false
  • 只读根 FS:建议 true
  • CapAdd:应为空或仅添加必要 Capabilities
  • CapDrop:建议 ALL,然后按需添加
Note

实验结束后清理:docker rm -f sec-demo

八、本章小结#

上一章理解了 docker run 的完整流程。

安全机制防护范围默认启用配置方式
Namespace视图隔离runc spec
Cgroup资源限制—memory, —cpus
Capabilities权限细分是(部分)—cap-add, —cap-drop
seccomp系统调用过滤—security-opt seccomp=
AppArmor文件访问控制宿主支持时1—security-opt apparmor=
SELinux类型强制访问宿主支持时1—security-opt label=
User Namespace用户映射rootless 模式
只读文件系统文件不可变—read-only
Note

容器安全不是单一机制能解决的,而是多层防护的组合。最安全的容器配置是:rootless + —cap-drop=ALL + 自定义 seccomp + AppArmor + 只读文件系统 + 资源限制。但这种配置可能影响应用兼容性,需要根据实际场景权衡。

Footnotes#

  1. AppArmor 和 SELinux 是互斥的两种 MAC 实现,是否默认启用取决于宿主发行版:Ubuntu/Debian 默认装 AppArmor,Docker 在其上会默认加载 docker-default profile;RHEL/CentOS 默认用 SELinux,Docker 在其上默认套用有限的容器 SELinux 策略;Alpine 等不带 MAC 的发行版则两者都不启用。 2

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

容器安全:seccomp/AppArmor/Capabilities
https://blog.souloss.cn/posts/container-runtime/container-security/
作者
Souloss
发布于
2021-09-18
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

相关文章 智能推荐
1
容器网络
容器运行时 容器网络的核心问题是,隔离的 Network Namespace 如何与外部通信?详细解读 veth pair(虚拟网卡对)、bridge(虚拟网桥)、iptables/NAT(地址转换)、CNI(容器网络接口)的完整链路,以及 Docker 的四种网络模式和 Kubernetes 的 Pod 网络模型,从「容器能 ping 通外网」到「理解每一条网络规则」。
2
OverlayFS:容器文件系统
容器运行时 OverlayFS 是容器分层文件系统的核心。全面剖析 OverlayFS 的 lowerdir/upperdir/workdir 三层结构、whiteout 标记文件机制、copy-up 写时复制语义、多层叠加规则,以及容器运行时如何用 OverlayFS 实现镜像层复用和容器可写层,理解「为什么容器启动这么快」和「镜像层是怎么共享的」。
3
容器完整流程:docker run 背后
容器运行时 当你执行 docker run nginx 时,背后发生了什么?本章完整追踪从 Docker CLI 到容器进程启动的每一步,镜像拉取、OCI Bundle 生成、shim 启动、runc 创建、Namespace/Cgroup/OverlayFS 配置、容器进程执行,让你对 docker run 的每一步都了如指掌。
4
容器运行时深入系列导读
容器运行时 从 Linux 内核的 Namespace、Cgroup、OverlayFS 出发,深入 OCI 规范、runc 源码、containerd 架构与 shim 机制,再覆盖容器安全、网络、镜像构建三个运行时核心维度,建立从内核机制到工业实现的完整认知链。
5
容器全景:从 chroot 到 OCI
容器运行时 从 1979 年的 chroot 到 2015 年的 OCI 标准,容器技术走过了三十多年的演进之路。本章梳理这条脉络,并辨析容器与虚拟机、容器进程与普通进程、镜像与容器与 Bundle 几组核心概念,为后续深入打下认知基础。