容器的安全本质是内核边界的收敛。仅靠 Namespace 和 Cgroup 无法阻挡内核提权与容器逃逸。标准的企业级防御范式是:通过 Seccomp 白名单严格限制系统调用(Syscall),使用 AppArmor 收紧文件与 Capabilities 权限,最后引入 Falco 基于 eBPF 的规则引擎进行运行时审计。本文直接拆解这三层防御体系的底层机制与核心落地配置。
第一道防线:基于 BPF 的 Seccomp 系统调用拦截
容器共享宿主机内核,Linux 内核拥有 300+ 个系统调用,而一个普通的 Web 服务在运行期通常只需用到 40-50 个。多暴露一个 Syscall,就多一分提权逃逸的攻击面(例如著名的 CVE-2022-0185 依赖 unshare系统调用)。
Seccomp(Secure Computing Mode)的底层本质,是将用户态配置的过滤规则,编译为经典的 cBPF(Classic BPF)指令,注入到内核的 task_struct 中。当进程触发系统调用时,内核会执行这套 BPF 字节码,根据返回值(如 SECCOMP_RET_KILL_PROCESS 或 SECCOMP_RET_ERRNO)决定放行还是阻断。
为什么原生的 RuntimeDefault 策略在核心业务中往往不够用?
Kubernetes 1.22+ 默认启用了 RuntimeDefault seccomp profile(由 containerd/Docker 提供)。这套默认策略禁用了约 44 个高危系统调用(如 kexec_load、bpf、ptrace),在兼容性和安全性之间做了妥协。
但在实际生产环境中,RuntimeDefault 存在两个极端问题:
-
防不住高级逃逸:它默认允许了
perf_event_open、userfaultfd等复杂且频繁爆出漏洞的系统调用。 -
误杀特定底层组件:对于需要精细内存管理的组件(如使用 DPDK 的网络服务,或者绑定 NUMA 节点的数据库),
mbind和set_mempolicy会被默认规则拦截,导致Operation not permitted。
实战:自定义 Seccomp 白名单阻断逃逸链
对于无状态的微服务,我们推崇“默认拒绝(Default Deny)”的白名单模式。以下是一个基于 K8s v1.28 的严格 Seccomp Profile 示例(需提前放置于 Kubelet 的 /var/lib/kubelet/seccomp/ 目录下):
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": [
"read", "write", "openat", "close", "fstat", "mmap", "mprotect", "munmap",
"brk", "rt_sigaction", "rt_sigprocmask", "rt_sigreturn", "ioctl", "pread64",
"pwrite64", "readv", "writev", "sched_yield", "futex", "epoll_wait", "epoll_ctl"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
在 Pod 定义中引用该 Profile:
apiVersion: v1
kind: Pod
metadata:
name: strict-secure-pod
spec:
securityContext:
seccompProfile:
type: Localhost
localhostProfile: custom-microservice.json
当被劫持的容器尝试调用不在白名单中的 execve 获取 Shell 时,内核会直接返回 EPERM,进程甚至没有机会进入内核态的主逻辑。
第二道防线:AppArmor 的 MAC(强制访问控制)隔离
Seccomp 控制的是“进程能对内核做什么”,而 AppArmor 控制的是“进程能对特定文件/网络做什么”。
在容器场景下,我们通常使用 AppArmor 解决以下痛点:
-
防止修改容器内的关键文件(即使是以 root 用户运行)。
-
限制在容器内使用
apt-get、curl等工具下载恶意 Payload。 -
封锁
/proc和/sys目录下的敏感写权限。
在 K8s 中落地 AppArmor 需要将 Profile 加载到宿主机(使用 apparmor_parser -r),并通过注解绑定到 Pod。
核心配置文件片段:针对 Nginx 容器的加固
# /etc/apparmor.d/k8s-nginx-hardened
#include <tunables/global>
profile k8s-nginx-hardened flags=(attach_disconnected, mediate_deleted) {
#include <abstractions/base>
#include <abstractions/nameservice>
# 基础文件读写
/ r,
/** r,
# 允许 Nginx 写入自身日志和 PID
/var/log/nginx/** w,
/run/nginx.pid w,
# 严禁执行 shell 和提权相关的工具
deny /bin/bash x,
deny /bin/sh x,
deny /usr/bin/curl x,
deny /usr/bin/wget x,
# 严禁向特殊文件系统写入
deny /sys/** w,
deny /proc/sys/** w,
}
将该策略应用于 Pod(注意:K8s v1.30 开始引入了原生的 apparmorProfile 字段,但在 v1.28 及以下仍需使用 Annotation):
metadata:
annotations:
container.apparmor.security.beta.kubernetes.io/nginx: localhost/k8s-nginx-hardened
第三道防线:Falco eBPF 规则引擎与异常行为审计
静态防御(Seccomp/AppArmor)再严密,也总有遗漏(或由于业务过度妥协而开的后门)。此时我们需要 Falco(基于 eBPF 的云原生安全威胁检测引擎)作为兜底的监控探针。
Falco 底层通过 eBPF(在 Kernel 5.15+ 环境推荐使用 modern_bpf 探针,无需编译内核模块)挂载到内核的 tracepoints(如 sys_enter_execve)。事件经过 Ring Buffer 推送到用户态引擎,匹配 YAML 定义的规则树。由于只读取不拦截,eBPF 对业务 QPS 的影响微乎其微(通常<2%)。
落地实战:捕捉容器内的敏感凭据读取与 Shell 派生
Falco (v0.36.1) 的精髓在于其强大的上下文关联能力。以下规则用于实时捕捉:非标准二进制在容器内读取 .kube/config 或 /etc/shadow 的行为。
- macro: sensitive_files
condition: fd.name in (/etc/shadow, /root/.kube/config, /var/run/secrets/kubernetes.io/serviceaccount/token)
- macro: allowed_readers
condition: proc.name in (kubelet, containerd)
- rule: Read Sensitive File by Unauthorized Process
desc: Detect unauthorized access to sensitive secrets within containers
condition: open_read and sensitive_files and container and not allowed_readers
output: "Sensitive file opened for reading (user=%user.name file=%fd.name command=%proc.cmdline container_id=%container.id image=%container.image.repository)"
priority: WARNING
tags: [filesystem, mitre_credential_access]
当告警触发时,Falco 可以将日志推送到 stdout、Webhook 或 Kafka。此时配合排查日志,你能看到精确的 container_id 和导致告警的 proc.cmdline。
常见问题 (FAQ)
Q1:应用接入自定义 Seccomp 后崩溃,日志只报 Operation not permitted,如何快速定位是哪个系统调用被拦截了?
A:两种方案。第一种是查看宿主机的 dmesg 日志或 /var/log/audit/audit.log,搜索 SECCOMP 关键字,日志里的 syscall= 后面跟的数字就是被拦截的系统调用编号,对照内核源码的 unistd.h 即可查出具体调用。第二种是在测试环境中,将 Seccomp Profile 的 defaultAction 临时改为 SCMP_ACT_LOG,此时内核只记录日志不拦截,方便你在日志中一次性收集完整的系统调用基线。
Q2:Falco 在高并发集群下频繁出现 Events dropped(丢弃事件)的告警,该如何调优?
A:丢弃事件通常意味着系统调用产生速率超过了 eBPF Ring Buffer 到用户态的消费速率。首先,通过修改 Helm values 提升 Ring Buffer 大小:falco.modern_bpf.cpus_for_each_buffer=1(让每个 CPU 拥有独立 buffer)并增加 falco.syscall_buf_size_preset=8。其次,深度优化 Falco 规则集,移除或禁用高频但低价值的宏条件(比如极其频繁的 openat 且过滤条件不严格的规则)。
Q3:我更新了宿主机上的 AppArmor 配置文件并重新 apparmor_parser -r 载入了,为什么正在运行的 Pod 行为没有受限制?
A:AppArmor 策略是在容器创建(由 containerd 调用 runc)时,通过 prctl(PR_SET_APPARMOR) 将进程与特定的 Profile 绑定的。一旦进程启动,它的安全上下文就已固定。你需要彻底删除并重建该 Pod(让它走完整的拉起生命周期),新的 AppArmor 策略才能对该容器内的进程生效。