香港服务器上的Linux系统如何通过SELinux与AppArmor强化安全策略,防止容器逃逸攻击?

那天是凌晨两点,我还在香港葵涌的机房里守着屏幕。空调的冷风呼呼直吹,机柜里的风扇呼啸得像飞机起飞一样,监控屏幕上不断跳出容器告警。CI/CD 节点上的一组容器突然出现了异常的系统调用尖峰,行为模式与我们熟悉的“容器逃逸”测试用例几乎一模一样。那一刻,我脑子里闪过最糟糕的画面:如果有人通过容器打穿了宿主机,那些存放在 NVMe 阵列里的客户源代码和关键数据,可能瞬间就会暴露无遗。站在嘈杂的机房通道里,我摸了下身边机柜的门锁,心里告诉自己:必须把 SELinux 和 AppArmor 的安全策略拉到最高档,否则这一层“护城河”迟早会被攻破。
香港机房与主机:
机房:香港葵涌 A 区(10G 上联,跨运营商 BGP)
服务器 1(主节点,跑核心业务与 CI/CD)
- 型号:Supermicro 2U(8×U.2 NVMe)
- CPU:AMD EPYC 7402P(24c/48t)
- 内存:256 GB ECC
- 系统盘:2×1.92 TB NVMe(RAID1)
- 数据盘:6×3.84 TB NVMe(RAID10)
- 网卡:2×10 GbE(bonding LACP)
- OS:CentOS 7.9(3.10.0-1160 内核,cgroup v1)
- 容器运行时:Docker 24.0(overlay2),containerd 1.6
服务器 2(边缘节点,跑边车与边界服务)
- 型号:Dell R640
- CPU:Intel Xeon Gold 6130(2×16c)
- 内存:192 GB
- OS:Ubuntu 20.04 LTS(5.4 内核,cgroup v1→可切 v2)
- 容器运行时:containerd 1.7 + nerdctl
调度形态: 混合部署(Kubernetes 边缘小集群 + 少量独立 Docker 服务)。
关键约束: 业务包含多租户 CI 构建容器与客户隔离容器,必须防容器逃逸、防横向移动。
威胁模型与目标
攻击面:
恶意/被劫持的业务容器尝试提权(--privileged、滥用 CAP_SYS_ADMIN)。
通过 /var/run/docker.sock、hostPath、挂载敏感宿主目录实现逃逸。
滥用 proc/sysfs、内核接口(ptrace、bpf) 进行探测与突破。
目标:
在 CentOS 7 用 SELinux,Ubuntu 用 AppArmor,双轨并行,叠加 seccomp、capabilities、userns、no_new_privileges;
默认拒绝、最小授权,阻止容器访问宿主关键接口,阻止“合集式”逃逸技巧。
快速体检(上线前我做的三件事)
# 宿主机基本态(CentOS 7)
getenforce # 期望 Enforcing
rpm -qa | grep container-selinux
docker info | egrep 'Security Options|Cgroup Driver|Storage Driver'
# Ubuntu 节点
sudo aa-status # 期望 apparmor 是 enabled
containerd --version
nerdctl info | grep -i security
# 内核&隔离面
sysctl kernel.unprivileged_bpf_disabled
sysctl kernel.yama.ptrace_scope
基线标准(我的“红线”):
- CentOS 7:SELinux Enforcing、container-selinux 已装;Docker/Containerd 启用 SELinux;不允许 --privileged。
- Ubuntu:AppArmor enforce 模式、为关键工作负载启用自定义 profile;默认 docker-default。
- 全局:启用 seccomp 默认策略,no_new_privileges、userns-remap(或 rootless),禁止容器直接接触 docker.sock。
一、SELinux 在 CentOS 7 的落地与“御敌”
1)启用与依赖
# 启用 / 修复
sed -i 's/^SELINUX=.*/SELINUX=enforcing/' /etc/selinux/config
yum install -y policycoreutils policycoreutils-python \
selinux-policy-targeted container-selinux
setenforce 1
getenforce # 看到 Enforcing 才算数
经验:如果历史上关过 SELinux,建议 fixfiles onboot 或 touch /.autorelabel && reboot 做全盘 relabel。
2)让 Docker 真正“说人话”——与 SELinux 对齐
/etc/docker/daemon.json:
{
"selinux-enabled": true,
"live-restore": true,
"userns-remap": "default",
"no-new-privileges": true,
"log-driver": "json-file",
"log-opts": {"max-size": "100m", "max-file": "3"},
"features": {"buildkit": true},
"seccomp-profile": "/etc/docker/seccomp/default.json"
}
systemctl daemon-reload && systemctl restart docker
docker info | sed -n '/Security Options/,+5p'
# 期望看到: name=selinux, name=seccomp, name=rootless(如启用), name=no-new-privileges
3)卷挂载与标签(容器数据必须有“户口”)
在 RHEL/CentOS 家族,容器可访问的数据文件类型通常是 svirt_sandbox_file_t(新策略也会见到 container_file_t)。我现场这样做:
# 定义期望标签(以 /srv/containers/data 为例)
semanage fcontext -a -t svirt_sandbox_file_t "/srv/containers/data(/.*)?"
restorecon -Rv /srv/containers/data
# Docker 侧:用 :Z 或 :z
docker run -d --name web \
-v /srv/containers/data:/var/www/html:Z \
nginx:1.25
:Z(大写)为该容器独占 MCS 标签;:z(小写)允许多个容器共享。
不要随意 chcon -t container_file_t,请用 semanage fcontext 固化规则,防丢。
4)阻断典型逃逸路径(三个实战片段)
片段 A:阻断对 docker.sock 的访问
# 1) 把 docker.sock 明确标成 docker 运行时类型
semanage fcontext -a -t docker_var_run_t "/var/run/docker.sock"
restorecon -v /var/run/docker.sock
# 2) 验证:容器内尝试操作 docker API
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock:Z alpine sh -lc 'apk add curl; \
curl --unix-socket /var/run/docker.sock http://localhost/containers/json'
# 预期:Permission denied (SELinux AVC),宿主 /var/log/audit/audit.log 有记录
若应用确实需要 docker API(CI 构建机常见),改走 rootless buildkit 或远端 dind,不要与宿主同体。
片段 B:限制过度能力(capabilities)与 no_new_privileges
docker run -d --name ci \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--security-opt no-new-privileges \
--pids-limit=512 --memory=2g \
my-ci:stable
SELinux 把“能做什么”再框一层,配合 no_new_privileges,就算容器里有 setuid 程序也翻不起浪。
片段 C:误用 --privileged 的拦截与替代
某个供应商镜像“强烈建议 --privileged”,结果容器内能直接 mount,还在扫宿主 /proc/kcore。我的做法:
- 先恢复正常权限:坚决不用 --privileged。
用审计日志看它到底需要什么:
- ausearch -m avc -ts recent | audit2allow -w
精准给到需要的 capability(通常是 SYS_ADMIN 之外的某个小能力)或换成 sidecar 提供服务。
若确需读写特定设备,用 device cgroup 明确允许哪个 /dev/xxx,而不是整仓库放行。
5)调 SELinux 时我遇到的坑 & 复盘
| 症状 | 现场表现 | 根因 | 处理 |
|---|---|---|---|
| 容器挂载本地卷后 13 权限错误 | 应用报 Permission denied,但 Linux ACL 没问题 |
卷没有 svirt_sandbox_file_t 标签 |
semanage fcontext + restorecon,或者 :Z/:z |
CI 容器无法访问 /var/run/docker.sock |
curl 失败 | 我把 sock 标了 docker_var_run_t,容器 container_t 不允许 |
正确做法是不要走 sock,改成 rootless buildkit/远端 dind |
| 某 NFS 卷读不到 | 应用无权限 | SELinux 默认限制容器访问 NFS | setsebool -P container_use_nfs on(仅在确需时) |
| 日志里全是 AVC,看不懂 | 运维焦虑 | 未用好工具链 | ausearch -m avc -ts recent、audit2why、sealert -a /var/log/audit/audit.log |
二、AppArmor 在 Ubuntu 节点的落地
Ubuntu 自带 docker-default profile,能挡一部分“野路子”,但我给关键工作负载写了 定制 profile,专门拒绝 mount、ptrace、访问 docker.sock,并收紧文件系统。
1)准备与状态
sudo aa-status
# apparmor module is loaded
# docker-default profile is in enforce mode ...
2)自定义 profile:/etc/apparmor.d/docker-deny-escape
# /etc/apparmor.d/docker-deny-escape
# 关键点:deny mount/ptrace/docker.sock;只读 /proc /sys;限制可执行
#include <tunables/global>
profile docker-deny-escape flags=(attach_disconnected,mediate_deleted) {
network,
capability,
file,
deny mount, # 禁止 mount 一切
deny ptrace (trace,readby), # 禁止 ptrace
deny signal (send,receive), # 可按需放开
deny /var/run/docker.sock rw, # 禁止触 docker.sock
# 只读系统视图
/proc/** r,
/sys/** r,
# 业务二进制与数据(按需调整路径)
/usr/bin/** mr,
/usr/sbin/** mr,
/opt/app/** rix,
/var/app/** rwk,
# 默认拒绝其他未声明访问
deny /** w,
}
启用:
apparmor_parser -r -W /etc/apparmor.d/docker-deny-escape
aa-enforce docker-deny-escape
3)容器加载这个 profile
Docker:
docker run --rm -it \
--security-opt apparmor=docker-deny-escape \
ubuntu:22.04 bash
Kubernetes(1.25+)注解:
apiVersion: v1
kind: Pod
metadata:
name: web
annotations:
container.apparmor.security.beta.kubernetes.io/app: localhost/docker-deny-escape
spec:
containers:
- name: app
image: nginx:1.25
小贴士:localhost/ 前缀意味着 profile 文件存在于 每台节点 的 /etc/apparmor.d/ 中;DaemonSet 分发是个好办法。
4)常见坑
AppArmor 日志在 dmesg//var/log/kern.log,关键词 AUDIT、apparmor="DENIED"。
很多人忘了 enforce 与 complain 的区别(complain 只告警不拦)。
profile 一变更要 重新加载:apparmor_parser -r -W ...。
三、容器运行时的“安全缺省”统一
无论 SELinux 还是 AppArmor,运行时必须帮我们“站好第一班岗”。
1)Docker(CentOS 7 与 Ubuntu 同理)
禁用特权与危险挂载:不提供 --privileged,不默认 -v /:/host 这类玩笑。
seccomp:使用官方默认策略或自定义精简版。
用户命名空间:"userns-remap": "default";或干脆 rootless。
no_new_privileges:全局与容器双重启用。
限制设备访问:必要时 --device=/dev/xxx,而不是 --device-cgroup-rule 'a *:* rwm'。
2)containerd
/etc/containerd/config.toml 关键位:
version = 2
[plugins."io.containerd.grpc.v1.cri"]
enable_selinux = true
enable_apparmor = true
sandbox_image = "registry.k8s.io/pause:3.9"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true
四、在 Kubernetes 里把策略“说清楚”
Pod 安全上下文 + SELinux + AppArmor + seccomp 一起上:
apiVersion: v1
kind: Pod
metadata:
name: hardened-app
annotations:
# AppArmor
container.apparmor.security.beta.kubernetes.io/app: localhost/docker-deny-escape
# seccomp
container.seccomp.security.alpha.kubernetes.io/app: "runtime/default"
spec:
securityContext:
runAsNonRoot: true
fsGroup: 2000
containers:
- name: app
image: ghcr.io/acme/app:1.0
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]
seLinuxOptions:
user: "system_u"
role: "system_r"
type: "container_t" # 注意:不要用 spc_t(特权)
level: "s0:c123,c456" # MCS 分级
volumeMounts:
- name: data
mountPath: /var/app/data
volumes:
- name: data
hostPath:
path: /srv/containers/app-data # 请确保已被 restorecon 为 svirt_sandbox_file_t
type: Directory
经验:K8s 里一旦配了 seLinuxOptions.type: spc_t 就形同“半特权”,等于在给对手开门。坚持 container_t。
五、对抗性验证(我在现场做的 7 个小测)
| 序号 | 场景 | 命令/动作 | 期望 | 真实结果 |
|---|---|---|---|---|
| 1 | 访问 docker.sock | -v /var/run/docker.sock:/var/run/docker.sock + curl |
Permission denied | SELinux AVC,阻断 |
| 2 | mount 尝试 | 容器内 mount -t proc proc /mnt |
不允许 | AppArmor deny |
| 3 | 读宿主 /etc/shadow | -v /etc:/host/etc:ro |
不允许敏感路径 | 手工审计拒绝此卷,CI 扫描拦截 |
| 4 | ptrace 附加 | strace -p 1 |
不允许 | AppArmor ptrace deny |
| 5 | BPF 滥用 | 加载 eBPF 程序 | 不允许 | 宿主 kernel.unprivileged_bpf_disabled=1 |
| 6 | 超限 fork | 压测创建大量进程 | 受控 | --pids-limit 生效 |
| 7 | 未打标卷访问 | 普通目录 bind mount | 失败 | SELinux 13 权限,restorecon 后恢复 |
六、我踩过的 10 个坑(以及我当时怎么补)
- CentOS 7 overlay2 与内核:太旧的 3.10 内核不稳定,我统一升到 3.10.0-1160 系列,Docker 24 + overlay2 明显稳。
- NFS/CEPH 卷标签:容器访问远端存储需开布尔值(例 container_use_nfs),只在确需场景打开。
- CI 镜像“必须 privileged”:与供应商扯皮两天,改为 rootless buildkit + --device 精准授权,最终不需要 SYS_ADMIN。
- K8s 节点不一致:AppArmor profile 要 每台节点 都有,DaemonSet 分发,避免 “works on my node”。
- SELinux 模块误开口子:有人图省事 audit2allow -M all && semodule -i,结果把洞越挖越大。策略:只 allow 你理解的。
- 日志没看懂:学会 ausearch -m avc -ts recent、audit2why,再看 AVC 的 tclass/perm/scontext。
- userns-remap 影响卷权限:容器 UID/GID 映射到宿主的 “dockremap” 用户,卷目录要配合 chown 或 :z/:Z。
- seccomp 误杀:一些老业务用奇怪 syscall(如 keyctl),我为这类容器指定更宽松 profile,而不放宽全局。
- spc_t 滥用:有人把 spc_t 当“修 bug 神器”,我直接禁用该选项的使用权限。
- 误把 AppArmor 放在 complain:一度“看上去都正常”,其实没拦住。上线前逐一核验 enforce。
七、强化清单(上线就照抄的 Checklist)
宿主基线
- SELinux Enforcing(CentOS 7),container-selinux 已装
- AppArmor enforce(Ubuntu),自定义 profile 已加载
- kernel.unprivileged_bpf_disabled=1,kernel.yama.ptrace_scope=2
- Docker:selinux-enabled=true、userns-remap=default、no-new-privileges=true
- Containerd:enable_selinux=true、enable_apparmor=true
- seccomp:全局默认 runtime/default,特殊容器单独文件
- 镜像扫描(CVE、恶意二进制)与 CI 阶段策略(不得使用 --privileged)
存储与卷
- 业务卷标记为 svirt_sandbox_file_t(或 container_file_t),restorecon -Rv
- 禁止默认挂载宿主根路径,/var/run/docker.sock 一律不挂
- NFS/CEPH 场景按需开启布尔值
Kubernetes
- allowPrivilegeEscalation=false、readOnlyRootFilesystem=true
- capabilities.drop: ["ALL"]、只加必要的
- seLinuxOptions.type=container_t,level 使用 MCS 分类
- AppArmor 注解 localhost/<profile>
- PodSecurity/PSA 至少 baseline,建议 restricted 命名空间
八、附:常用命令速查
# SELinux
getenforce && setenforce 1
semanage fcontext -a -t svirt_sandbox_file_t "/path(/.*)?"
restorecon -Rv /path
ausearch -m avc -ts recent | audit2why
# AppArmor
aa-status
apparmor_parser -r -W /etc/apparmor.d/<profile>
aa-enforce <profile>
dmesg | grep -i apparmor
# Docker / Containerd
docker info | sed -n '/Security Options/,+5p'
nerdctl run --security-opt apparmor=docker-deny-escape ...
九、为什么是“SELinux + AppArmor + 运行时”三件套
SELinux(CentOS/RHEL 系):强制访问控制 + 多类别隔离(MCS),对卷与进程有极细粒度建模。
AppArmor(Ubuntu 系):Profile 语义直观,拦截 mount/ptrace 等“逃逸核心手法”非常直接。
容器运行时策略(seccomp、capabilities、userns、no_new_privileges):把“常识”变成“缺省”,不给错误配置机会。
我在香港这套混合环境里把两者结合,外加运行时的“安全缺省”,不仅挡住了日常的越界需求,也承受住了我们红队的多轮压力测试。安全没有银弹,但叠加的最小授权,就是我们在生产机房最可靠的“墙”。
十、深夜里的防线:我的复盘与感悟
现在回过头来看,那几天在机房连续熬夜加固策略的经历,就像是跟一个随时会撕开防线的对手赛跑。SELinux 给了我在 CentOS 节点上最坚实的底线,AppArmor 在 Ubuntu 节点上把漏洞点一一封死,而容器运行时的安全缺省策略让整个体系更像一个有序的防御矩阵。记得完成最后一次对抗性测试时,容器内的攻击脚本一次次被拒之门外,audit 日志刷屏,但宿主稳如泰山。那一刻,我才真正体会到“安全不是喊口号”,而是一次次在嘈杂机房里、在冷风呼啸的深夜里,把每条策略敲进系统的汗水和坚持。对我来说,这不只是一次技术实践,更是一份责任感的落地。