上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2025-08-17 10:21 阅读量:816


那天是凌晨两点,我还在香港葵涌的机房里守着屏幕。空调的冷风呼呼直吹,机柜里的风扇呼啸得像飞机起飞一样,监控屏幕上不断跳出容器告警。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 recentaudit2whysealert -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 个坑(以及我当时怎么补)

  1. CentOS 7 overlay2 与内核:太旧的 3.10 内核不稳定,我统一升到 3.10.0-1160 系列,Docker 24 + overlay2 明显稳。
  2. NFS/CEPH 卷标签:容器访问远端存储需开布尔值(例 container_use_nfs),只在确需场景打开。
  3. CI 镜像“必须 privileged”:与供应商扯皮两天,改为 rootless buildkit + --device 精准授权,最终不需要 SYS_ADMIN。
  4. K8s 节点不一致:AppArmor profile 要 每台节点 都有,DaemonSet 分发,避免 “works on my node”。
  5. SELinux 模块误开口子:有人图省事 audit2allow -M all && semodule -i,结果把洞越挖越大。策略:只 allow 你理解的。
  6. 日志没看懂:学会 ausearch -m avc -ts recent、audit2why,再看 AVC 的 tclass/perm/scontext。
  7. userns-remap 影响卷权限:容器 UID/GID 映射到宿主的 “dockremap” 用户,卷目录要配合 chown 或 :z/:Z。
  8. seccomp 误杀:一些老业务用奇怪 syscall(如 keyctl),我为这类容器指定更宽松 profile,而不放宽全局。
  9. spc_t 滥用:有人把 spc_t 当“修 bug 神器”,我直接禁用该选项的使用权限。
  10. 误把 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 日志刷屏,但宿主稳如泰山。那一刻,我才真正体会到“安全不是喊口号”,而是一次次在嘈杂机房里、在冷风呼啸的深夜里,把每条策略敲进系统的汗水和坚持。对我来说,这不只是一次技术实践,更是一份责任感的落地。

目录结构
全文