我是怎么理解 eBPF、AppArmor 和 Seccomp 的?
先说结论:不要把 eBPF 当成 AppArmor,也不要把 Seccomp 当成完整的沙箱。我的理解是:Seccomp 缩小系统调用面,AppArmor 限制文件和能力,eBPF 负责把运行时发生的事情记录下来。
写在前面:这不是一篇"看完就会逃逸"的文章
我最开始接触这几个东西,是因为一次很普通的容器故障:服务在宿主机上运行正常,放进容器后却突然报 Permission denied。当时第一反应是把权限放开,结果服务是起来了,但自己也说不清到底放开了什么。
后来我才慢慢把这件事拆开:先看进程调用了什么,再看 syscall 有没有被拦,最后看文件、能力和 profile。这个过程里,eBPF、Seccomp 和 AppArmor 才从几个容易混在一起的名词,变成了三个不同位置的工具。
目录
- 一、为什么要同时理解这三项技术?
- 二、eBPF:在内核事件上构建可观测性与安全能力
- 三、AppArmor:以配置文件约束进程行为
- 四、Seccomp:用系统调用过滤缩小内核攻击面
- 五、三者放在一起:分层防御模型
- 六、容器安全基线示例
- 七、总结
- 参考资料
一、为什么要同时理解这三项技术?
容器共享宿主机内核。容器里的进程虽然拥有独立的文件系统、网络命名空间和进程视图,但最终仍然会通过系统调用进入同一个 Linux 内核。
一次典型的攻击链可能是:
- 应用依赖存在漏洞,攻击者获得容器内代码执行能力;
- 进程尝试读取敏感文件、访问宿主机设备,或调用高风险系统调用;
- 攻击者继续利用内核或配置错误,扩大权限;
- 运行时没有审计与告警,直到数据泄露才被发现。
三项技术分别解决不同问题:
| 技术 | 核心问题 | 典型能力 | 不能替代什么 |
|---|---|---|---|
| eBPF | 发生了什么?是否异常? | 低侵入观测、审计、网络处理、运行时检测 | 不是默认的访问控制策略,也不是自动沙箱 |
| AppArmor | 进程可以访问哪些对象? | 文件路径、能力、网络、信号等强制访问控制 | 不能完整限制所有系统调用 |
| Seccomp | 进程可以调用哪些系统调用? | 允许/拒绝 syscall,过滤参数,降低内核攻击面 | 不能表达复杂的文件路径权限 |
一句话记忆:
Seccomp 管入口,AppArmor 管资源,eBPF 管观测和动态策略。
二、eBPF:在内核事件上构建可观测性与安全能力
2.1 eBPF 到底是什么?
eBPF(extended Berkeley Packet Filter)是一套运行在 Linux 内核中的安全、可验证、事件驱动的程序执行机制。它允许开发者把小型程序加载到内核中的特定挂载点,在不修改内核源码、通常也不需要重启内核的情况下,观测或处理系统行为。
常见挂载点包括:
- Tracepoint:内核预定义的稳定事件,例如进程执行、系统调用、网络事件;
- kprobe/kretprobe:动态探测内核函数的进入和返回;
- Uprobe/uretprobe:探测用户态程序或共享库函数;
- LSM BPF:参与 Linux Security Module 决策;
- TC/XDP:在网络协议栈较早阶段处理数据包;
- cgroup hooks:围绕容器或 cgroup 进行网络、套接字和设备相关控制。
一个 eBPF 程序通常由以下部分组成:
text
用户态加载器
│ 通过 bpf() 系统调用加载
▼
内核 verifier(验证器)
│ 检查边界、循环、内存访问和可达性
▼
eBPF 程序 + map + ring buffer
│
├── 采集内核事件
├── 更新状态
└── 将事件送回用户态
2.2 eBPF 的安全性来自哪里?
eBPF 并不是"任意内核代码注入"。程序加载前通常会经过 verifier 检查,重点包括:
- 是否存在越界内存访问;
- 指针类型和生命周期是否正确;
- 是否可能执行不可控的无限循环;
- 是否调用了当前程序类型不允许的 helper;
- 是否满足权限和内核配置要求。
但这不意味着 eBPF 没有风险。生产环境仍然要关注:
- 谁拥有加载 eBPF 程序所需的权限;
- 内核版本、BTF、helper 能力是否一致;
- 运行时探针是否引入过高开销;
- 探针采集的数据是否包含敏感信息;
- 容器是否被授予了不必要的
CAP_BPF、CAP_SYS_ADMIN等能力。
2.3 用 bpftrace 观察进程执行
下面的示例用于观察系统中执行过的程序。它适合在测试环境快速定位"谁启动了什么",不建议直接作为生产审计方案。
bash
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_execve
{
printf("pid=%d comm=%s file=%s\\n", pid, comm, str(args->filename));
}
'
在容器环境中,可以进一步结合 PID、cgroup、容器元数据进行归属判断。实际落地时,建议使用成熟工具或自研 agent,统一处理事件去重、采样、脱敏和告警。
我自己排查问题时,一般先从 execve 这种事件开始看。因为"容器里到底启动了什么"往往比一上来研究一大堆 profile 更容易得到线索。比如服务偷偷启动了 shell、健康检查脚本路径不对,通常几分钟就能看出来。
2.4 eBPF 适合做什么?
- 进程启动、文件访问、网络连接、提权行为审计;
- 容器运行时检测,例如在容器中执行 shell、写入敏感路径;
- 网络可观测性和高性能数据面处理;
- 性能分析,例如 CPU、I/O、锁竞争和调度延迟;
- 与 LSM、cgroup、网络 hook 结合,构建动态安全策略。
2.5 eBPF 的边界
eBPF 首先是一种内核扩展和事件处理机制,而不是"一键安全开关"。
- 只部署观测程序,并不会自动阻止危险行为;
- 事件采集不等于完整审计,丢事件、采样和权限都会影响结论;
- 直接使用 kprobe 依赖内核实现细节,跨版本稳定性不如 tracepoint;
- 复杂策略需要清晰的失败模式,否则可能出现误杀或性能问题;
- eBPF 程序本身也必须纳入版本管理、权限管理和发布审计。
三、AppArmor:以配置文件约束进程行为
3.1 AppArmor 的基本模型
AppArmor 是 Linux Security Module(LSM)之一,采用路径为中心的强制访问控制模型。管理员为一个程序定义 profile,指定它可以读取、写入、执行哪些路径,以及允许使用哪些能力、网络类型和信号。
与传统 Unix 权限相比,AppArmor 的关键差异是:
- Unix 权限回答"文件所有者和权限位是否允许";
- AppArmor 进一步回答"这个进程即使拥有 Unix 权限,是否仍被 profile 允许"。
常见 profile 规则:
text
/usr/bin/my-service {
# 只读配置
/etc/my-service/** r,
# 允许写入运行时目录
/var/lib/my-service/** rwk,
# 允许执行指定程序
/usr/bin/helper ix,
# 拒绝访问密钥目录
deny /root/.ssh/** r,
# 允许建立网络连接
network inet stream,
}
权限字符常见含义:
| 权限 | 含义 |
|---|---|
r |
读取 |
w |
写入 |
k |
文件锁 |
m |
映射到内存并执行 |
x |
执行,具体继承方式由规则决定 |
ix |
执行后继承当前 profile |
px |
执行后切换到指定 profile |
一个我觉得比较实用的排查顺序
遇到权限问题时,我现在基本不再直接给容器加 privileged。先确认实际行为,再一点点加规则,最后用完整链路回归验证。这样慢一点,但出了问题还能解释,也方便后续收紧权限。
3.2 Complain 模式与 Enforce 模式
开发和上线前建议采用两阶段:
- Complain:记录违反规则的行为,但不阻止;
- 根据审计日志补充最小权限规则;
- Enforce:正式阻止未授权行为;
- 持续观察误报和业务变更。
bash
# 查看 profile 状态
sudo aa-status
# 将 profile 切换为观察模式
sudo aa-complain /etc/apparmor.d/usr.sbin.my-service
# 将 profile 切换为强制模式
sudo aa-enforce /etc/apparmor.d/usr.sbin.my-service
3.3 Docker 中使用 AppArmor
宿主机已加载 profile 后,可以在启动容器时指定:
bash
docker run --rm \
--security-opt apparmor=my-container-profile \
--read-only \
--cap-drop=ALL \
nginx:stable
Kubernetes 中可以通过运行时类相关配置或安全上下文使用 AppArmor。不同 Kubernetes 版本和容器运行时的配置方式存在差异,生产环境应以集群版本文档和节点实际 profile 状态为准。
3.4 AppArmor 的优势与局限
优势:
- 规则以路径为中心,对应用运维人员相对直观;
- 可以精细限制配置、密钥、设备和执行文件;
- 能与容器运行时结合,形成应用级隔离;
- 不需要改造应用代码。
局限:
- 路径模型不等于文件对象模型,硬链接、挂载和命名空间场景需要谨慎验证;
- profile 过于宽松时,攻击者仍可能利用允许路径;
- profile 过于严格时,容易因业务升级产生启动失败;
- AppArmor 不是系统调用过滤器,不能替代 Seccomp;
- 宿主机必须启用并正确配置 AppArmor,容器内写 profile 并不会自动获得宿主机强制效果。
四、Seccomp:用系统调用过滤缩小内核攻击面
4.1 为什么要限制系统调用?
Linux 用户态程序通过系统调用使用内核能力。一个普通 Web 服务可能只需要文件、网络、内存和线程相关系统调用,但如果它同时拥有挂载、加载内核模块、调试任意进程等能力,漏洞被利用后的攻击面会显著扩大。
Seccomp(secure computing)允许进程设置系统调用过滤器。当进程发起 syscall 时,过滤器可以根据:
- 系统调用编号;
- 架构;
- syscall 参数;
- 当前过滤器动作;
决定允许、拒绝、返回错误、触发审计或终止进程。
4.2 Seccomp 的两种主要模式
- Strict mode:只允许极少数系统调用,适用范围非常有限;
- Filter mode:通过 BPF 过滤器定义更灵活的规则,容器场景主要使用这一模式。
Seccomp 使用的是经典 BPF 过滤逻辑,和 eBPF 有关联但不是同一个东西。不要因为都出现 BPF 就把两者当成同一套技术:Seccomp 的目标是 syscall 过滤,eBPF 的目标是可验证的内核程序与事件处理生态。
4.3 Docker 使用自定义 Seccomp profile
我第一次写 Seccomp 规则时,犯过一个很典型的错误:看到某个 syscall 可疑,就直接拒绝,结果服务启动阶段就挂了。后来改成先用默认 profile 跑完整测试,再针对确实不需要的调用做限制,排障成本低很多。
下面是一个简化示例,表示拒绝 mount 系统调用。完整 profile 还需要根据应用实际调用情况设计,不建议直接拿示例当生产白名单。
json
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{
"names": ["mount"],
"action": "SCMP_ACT_ERRNO",
"errnoRet": 1
}
]
}
启动容器:
bash
docker run --rm \
--security-opt seccomp=./seccomp.json \
nginx:stable
生产环境更推荐从"默认拒绝、按需放行"的思路设计,但必须先通过测试或运行时观测收集应用调用集,否则很容易误伤正常功能。
4.4 Seccomp 与 capability 的关系
两者控制层次不同:
- Capability 控制进程是否拥有某类特权;
- Seccomp 控制进程能否调用某些系统调用;
- 某个 syscall 被允许,也不代表它一定有权限成功执行;
- 某个 capability 被删除,也不代表相关 syscall 不会被调用。
因此,安全基线通常是同时做:
bash
docker run --rm \
--cap-drop=ALL \
--security-opt no-new-privileges:true \
--security-opt seccomp=./seccomp.json \
image:tag
4.5 Seccomp 的局限
- 它看见的是 syscall,不擅长表达"只能访问某个目录";
- syscall 参数过滤复杂,架构差异和兼容性需要测试;
- 某些高层行为会通过多个 syscall 完成,只过滤一个 syscall 可能不够;
- 容器运行时默认 profile 不是万能的,不能替代应用自身的最小权限设计;
- 过滤规则错误可能导致应用启动失败,甚至让故障排查变得困难。
五、三者放在一起:分层防御模型
一个更准确的关系图如下:
text
应用进程
│
├── 发起系统调用 ──> Seccomp:这个 syscall 能不能进内核?
│
├── 访问文件/网络/能力 ──> AppArmor:这个资源是否允许访问?
│
└── 行为产生事件 ──> eBPF:谁做了什么?是否需要告警或处置?
推荐组合
| 场景 | Seccomp | AppArmor | eBPF |
|---|---|---|---|
| 互联网暴露的 Web 服务 | 必选 | 推荐 | 推荐 |
| 构建任务/CI Runner | 必选 | 推荐 | 推荐 |
| 多租户平台 | 严格配置 | 严格配置 | 强烈推荐 |
| 低风险内部工具 | 使用默认 profile | 按需 | 按需 |
| 需要高性能网络处理 | 保持 syscall 最小化 | 保护宿主资源 | 可用于 XDP/TC |
一个现实的安全闭环
- 用 eBPF 或审计工具收集应用真实行为;
- 根据行为生成初始 syscall 和文件访问清单;
- 用 Seccomp 删除不需要的内核入口;
- 用 AppArmor 限制配置、密钥、设备和执行路径;
- 再用 eBPF 持续检测 profile 绕过、异常执行和横向行为;
- 将事件接入日志、告警和应急响应系统。
六、容器安全基线示例
下面的命令展示一组常见的加固参数。参数是否适用要以应用测试结果为准:
bash
docker run --rm \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--cap-drop=ALL \
--security-opt no-new-privileges:true \
--security-opt seccomp=./seccomp.json \
--security-opt apparmor=my-container-profile \
--pids-limit=256 \
--memory=512m \
--cpus=1 \
my-service:1.0
检查重点:
- 是否真的需要写入根文件系统;
- 是否真的需要某个 capability;
- 是否存在必须使用的 setuid/setgid 程序;
- 是否需要调试、挂载、加载模块或访问设备;
- profile 被拒绝后,应用是否能输出可定位的错误日志;
- 规则是否经过升级、回滚和灾备演练。
在 Kubernetes 中,还应结合:
allowPrivilegeEscalation: false;readOnlyRootFilesystem: true;runAsNonRoot: true;- 删除不必要的 Linux capabilities;
- Pod Security Standards;
- 节点级 AppArmor/Seccomp 配置;
- 运行时检测与审计平台。
七、总结
- Seccomp 通过过滤系统调用,减少进程进入内核的攻击面;
- AppArmor 通过 profile 限制进程访问文件、网络、能力和其他资源;
- eBPF 通过内核 hook 提供高性能观测,并可扩展到网络、运行时检测和安全策略;
- 三者不是互相替代,而是位于不同层次的防御能力;
- 真正可靠的容器安全,依赖最小权限、分层控制、持续观测和可回滚的工程流程。
最终建议:把安全策略当成代码来维护。每次镜像、内核、运行时或业务依赖升级,都重新验证 Seccomp、AppArmor 和 eBPF 规则,而不是把一次成功启动当成安全证明。
参考资料
- Linux Kernel Documentation - eBPF:https://docs.kernel.org/bpf/index.html
- eBPF 官方文档:https://ebpf.io/what-is-ebpf/
- Linux Kernel Documentation - Seccomp:https://docs.kernel.org/userspace-api/seccomp_filter.html
- Docker Seccomp 安全配置:https://docs.docker.com/engine/security/seccomp/
- Ubuntu AppArmor 文档:https://documentation.ubuntu.com/server/how-to/security/apparmor/
- Kubernetes Security Context:https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
- Linux capabilities 手册:https://man7.org/linux/man-pages/man7/capabilities.7.html
CSDN 标签 :Linux 云原生 容器安全 eBPF AppArmor Seccomp Docker Kubernetes 内核安全 DevSecOps