eBPF 容器运行时安全:从 Falco 到 Tetragon,构建零侵入的威胁检测体系
引言:为什么容器安全需要 eBPF
容器化部署已经成为现代云原生基础设施的事实标准。然而,容器的共享内核模型意味着任何一个容器的逃逸都可能威胁到整个节点乃至集群。传统的安全方案要么依赖修改容器镜像(sidecar 注入、seccomp profile),要么需要修改应用代码(插桩 SDK),这些方式在动态、高密度的 K8s 集群中都显得笨重且容易遗漏。
eBPF(extended Berkeley Packet Filter)提供了一种全新的范式:在不修改内核源码、不重启服务、不侵入容器镜像的前提下,实现从系统调用到网络数据包、从文件系统到进程生命周期的全维度可观测性与实时阻断。Sysdig、Falco、Tetragon、Cilium 等项目已经证明了这条路的工程可行性。
本文深入 eBPF 容器安全的技术栈,先拆解其核心机制(kprobe/tracepoint/LSM hook),再对比 Falco(检测)与 Tetragon(检测+阻断)的架构差异,最后完整实现一个最小化的 eBPF 容器安全监控程序,涵盖进程执行审计、敏感文件访问告警、异常外联检测三种典型场景。
一、eBPF 安全监控的三层 Hook 体系
1.1 可选的 Hook 点全景
eBPF 能够在内核中插入监控程序的位置主要有三类,按抽象层级从低到高:
| Hook 类型 | 粒度 | 典型用途 | 安全场景 |
|---|---|---|---|
| Tracepoint | 内核预定义插桩点 | fork/execve/socket 系统调用记录 | 进程树追踪、原始系统调用溯源 |
| Kprobe/kretprobe | 任意内核函数入口/返回 | 监控 do_sys_open、tcp_connect 等内部函数 | 文件系统操作、网络连接建立 |
| LSM BPF | Linux Security Module hook 点 | 显式的安全决策点(bprm_check_security、file_open、socket_connect) | 权限检查拦截、文件完整性、网络策略 |
关键洞察:LSM BPF 是唯一支持"阻断"的 eBPF 挂载方式。LSM hook 要求 eBPF 程序返回 0(允许)或负数(拒绝),这天然符合"安全决策"语义。而 kprobe/tracepoint 仅能"观察",无法改变内核行为——这恰好对应了 Falco(只检测)与 Tetragon(检测+阻断)的能力边界。
1.2 容器上下文提取:从 PID 到 container_id
一个 naive 的 eBPF 监控系统只看系统调用参数,得到的是 host PID、UID 等宿主机视角的信息。在容器场景下,这些信息价值有限——我们需要的是 container_id、pod 名、namespace。
内核通过 task_struct 结构体中的 nsproxy 字段维护进程的命名空间状态。eBPF 程序可以安全地读取 task->nsproxy->mnt_ns->ns.inum 来获取挂载命名空间的 inode 号,进而关联到容器的运行时状态。更实用的做法是:
// BPF 伪代码:从 task_struct 提取 cgroup 路径
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
// task->cgroups->dfl_cgrp->kn->name 包含 container_id (前12位)
实际工程中,Falco 和 Tetragon 的 eBPF 程序会读取 task->cgroups->dfl_cgrp->kn->name,从中解析出 container_id(通常是 SHA256 的前 12 位)。Tetragon 进一步缓存 PID → container_id 的映射,避免每次系统调用都重复解析。
二、Falco 架构深度拆解
2.1 整体数据流
Falco 的设计哲学是"内核态只做采集,用户态做复杂分析"。它的架构分为三层:
┌─────────────────────────────────────────┐
│ 用户态 Falco 引擎 │
│ 规则引擎 → 事件过滤 → 输出(stdout/S3/SIEM)│
└──────────────┬──────────────────────────┘
│ 共享 BPF ring buffer
┌──────────────┴──────────────────────────┐
│ 内核态 eBPF 程序 │
│ syscall_enter → 参数解码 → 填充 event │
└─────────────────────────────────────────┘
eBPF 程序选择 syscall tracepoints(如 sys_enter_execve、sys_enter_openat)作为数据来源,使用 bpf_perf_event_output 将解析后的事件推送到用户态。用户态 Go 进程加载规则文件(YAML),逐条匹配事件并触发告警。
2.2 规则引擎的设计
Falco 规则采用类 YAML 的声明式语法:
- rule: Terminal shell in container
desc: Detect shell execution inside a container
condition: spawned_process and container and proc.name in (shell_binaries)
output: "Shell spawned in container (user=%user.name container=%container.id image=%container.image.repository shell=%proc.name parent=%proc.pname cmdline=%proc.cmdline)"
priority: WARNING
tags: [container, shell, mitre_execution]
规则引擎的精巧之处在于filter expression 的编译优化。早期的 Falco 将规则解释执行,每条系统调用都遍历所有规则。现代 Falco 将规则条件编译为 BPF 程序内核态过滤,在内核中先做粗筛,只有命中的事件才通过 ring buffer 上报用户态,大幅降低了事件丢失率。
2.3 性能模型
Falco 的 eBPF 探针采用"全采粗筛+用户态精筛"的两级策略: - 内核侧:所有 syscall 都被 tracepoint 捕获,但只有匹配规则条件的事件被写入 ring buffer。使用 BPF_MAP_TYPE_HASH 维护白名单缓存(如常见系统进程的 exec)。 - 用户态:规则引擎对进入 ring buffer 的事件做精确匹配,避免 BPF 验证器对复杂 BPF 程序的大小限制(≤1M 指令)。
三、Tetragon:从检测到阻断的跃迁
3.1 架构差异
Tetragon 是 Isovalent(Cilium 母公司)推出的运行时安全工具,相比 Falco 增加了内核态阻断能力:
| 维度 | Falco | Tetragon |
|---|---|---|
| Hook 方式 | tracepoint / kprobe | LSM BPF + kprobe |
| 阻断能力 | ❌ 仅检测(需调用 webhook) | ✅ LSM hook 返回 -EPERM 直接拒绝 |
| 进程上下文 | 采样式查询 | 全量缓存 PID→容器→镜像信息 |
| 性能开销 | 中等(仅观察) | 稍高(LSM hook + 阻断决策) |
| 部署复杂度 | 低(DaemonSet 一行) | 稍高(需内核 ≥5.10 支持 LSM BPF) |
3.2 全量进程追踪:Binary Execution Monitoring
Tetragon 最强大的功能是维护了一个 node 上所有进程的完整生命周期视图。它通过挂载 sched_process_fork 和 sched_process_exit 的 tracepoint,在 eBPF map 中维护一棵进程树:
// Tetragon 的 BPF map 结构(概念级)
struct process_info {
u64 pid;
u64 tid;
u64 start_time; // 单调时间戳,用于去重
u64 parent_pid;
char binary[PATH_MAX];
u32 container_id; // 关联到 cgroup
u8 pod_name[256];
};
这种全量追踪使得 Tetragon 能够回答"过去 24 小时内 pod X 的所有子进程都执行了哪些文件"这类问题——这是 Falco 的 slipstream 模式做不到的。
3.3 实战:用 Tetragon 配置文件访问策略
apiVersion: isovalent.com/v1alpha1
kind: TracingPolicy
metadata:
name: sensitive-file-access
spec:
selectors:
- matchBinaries:
- operator: In
values:
- "/usr/bin/curl"
- "/usr/bin/wget"
kprobes:
- call: "security_file_open"
syscall: false
return: true
args:
- index: 0
type: "file"
returnArg:
type: "int"
selectors:
- matchArgs:
- index: 0
operator: "Postfix"
values:
- "/etc/shadow"
- "/etc/sudoers"
matchActions:
- action: Sigkill
这个策略的含义是:当 curl 或 wget 尝试打开 /etc/shadow 时,直接发 SIGKILL 杀死进程。注意这是内核态即时响应,不存在"告警发出去但进程已经逃逸了"的窗口。
四、从零编写一个最小化 eBPF 容器安全监控程序
为了深入理解原理,我们用 C(libbpf)实现一个最小化的安全监控器,覆盖三个核心场景:进程执行审计、敏感文件告警、异常外联检测。
4.1 项目结构
container-guard/
├── container-guard.bpf.c # eBPF 内核程序
├── container-guard.c # Go 用户态加载器
├── container-guard.h # 共享头文件
├── rules.yaml # 安全规则
├── go.mod
└── Makefile
4.2 eBPF 内核程序(核心)
// container-guard.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include "container-guard.h"
// Ring buffer 用于传递事件到用户态
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024); // 256KB
} events SEC(".maps");
// PID → container_id 缓存
struct {
__type(key, u32); // pid
__type(value, u64); // cgroup_id
__uint(max_entries, 4096);
} pid_to_container SEC(".maps");
// 解析 container_id(从 cgroup 文件系统名)
static __always_inline u64 get_current_cgroup_id(void)
{
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
return BPF_CORE_READ(task, cgroups, dfl_cgrp, kn, parent, id);
}
// 场景1:execve 系统调用审计
SEC("tp/syscalls/sys_enter_execve")
int trace_execve(struct trace_event_raw_sys_enter *ctx)
{
struct event e = {};
u32 pid = bpf_get_current_pid_tgid() >> 32;
e.pid = pid;
e.timestamp = bpf_ktime_get_ns();
e.type = EVENT_EXEC;
e.cgroup_id = get_current_cgroup_id();
bpf_get_current_comm(&e.comm, sizeof(e.comm));
bpf_probe_read_user_str(&e.filename, sizeof(e.filename),
(void *)ctx->args[0]);
bpf_ringbuf_output(&events, &e, sizeof(e), 0);
return 0;
}
// 场景2:打开敏感文件审计
SEC("tp/syscalls/sys_enter_openat")
int trace_openat(struct trace_event_raw_sys_enter *ctx)
{
char filename[256] = {};
bpf_probe_read_user_str(filename, sizeof(filename), (void *)ctx->args[1]);
// 快速过滤:只关注敏感路径
if (filename[0] != '/' || filename[1] != 'e' || filename[2] != 't' || filename[3] != 'c')
return 0;
// 更精确的匹配用 BPF_MAP_TYPE_HASH 做前缀表
if (!is_sensitive_path(filename, sizeof(filename)))
return 0;
struct event e = {};
e.pid = bpf_get_current_pid_tgid() >> 32;
e.timestamp = bpf_ktime_get_ns();
e.type = EVENT_OPEN;
e.cgroup_id = get_current_cgroup_id();
__builtin_memcpy(e.filename, filename, sizeof(filename));
bpf_ringbuf_output(&events, &e, sizeof(e), 0);
return 0;
}
// 场景3:异常外联检测(connect 系统调用)
SEC("tp/syscalls/sys_enter_connect")
int trace_connect(struct trace_event_raw_sys_enter *ctx)
{
struct sockaddr_in addr = {};
bpf_probe_read_user(&addr, sizeof(addr), (void *)ctx->args[1]);
// 仅关注 IPv4 + TCP
if (addr.sin_family != AF_INET)
return 0;
u32 dst_ip = bpf_ntohl(addr.sin_addr.s_addr);
u16 dst_port = bpf_ntohs(addr.sin_port);
// 排除 RFC1918 私有地址(容器内部通信)
if (is_private_ip(dst_ip))
return 0;
// 检测模式:非 80/443 流量到公网 IP
if (dst_port != 80 && dst_port != 443) {
struct event e = {};
e.pid = bpf_get_current_pid_tgid() >> 32;
e.timestamp = bpf_ktime_get_ns();
e.type = EVENT_CONNECT;
e.cgroup_id = get_current_cgroup_id();
e.dst_ip = dst_ip;
e.dst_port = dst_port;
bpf_ringbuf_output(&events, &e, sizeof(e), 0);
}
return 0;
}
char LICENSE[] SEC("license") = "GPL";
4.3 用户态 Loader(Go + cilium/ebpf)
// container-guard.go
package main
import (
"bytes"
"encoding/binary"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"net"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/ringbuf"
"github.com/cilium/ebpf/rlimit"
)
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target=bpfel containerGuard ./container-guard.bpf.c
type EventType uint32
const (
EventTypeExec EventType = 1
EventTypeOpen EventType = 2
EventTypeConnect EventType = 3
)
type Event struct {
Timestamp uint64
Pid uint32
CgroupID uint64
Type uint32
Comm [16]byte
Filename [256]byte
DstIP uint32
DstPort uint16
}
func (e Event) CommStr() string {
return string(bytes.TrimRight(e.Comm[:], "\x00"))
}
func (e Event) FilenameStr() string {
return string(bytes.TrimRight(e.Filename[:], "\x00"))
}
func (e Event) DstIPStr() string {
ip := make(net.IP, 4)
binary.BigEndian.PutUint32(ip, e.DstIP)
return ip.String()
}
func main() {
// 解除 memlock 限制(eBPF map 需要)
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatalf("failed to remove memlock limit: %v", err)
}
// 加载编译好的 eBPF 程序
objs := containerGuardObjects{}
if err := loadContainerGuardObjects(&objs, nil); err != nil {
log.Fatalf("loading eBPF objects: %v", err)
}
defer objs.Close()
// 挂载 tracepoint
tpExec, err := link.Tracepoint("syscalls", "sys_enter_execve",
objs.TraceExecve, nil)
if err != nil {
log.Fatalf("attaching execve tracepoint: %v", err)
}
defer tpExec.Close()
tpOpen, err := link.Tracepoint("syscalls", "sys_enter_openat",
objs.TraceOpenat, nil)
if err != nil {
log.Fatalf("attaching openat tracepoint: %v", err)
}
defer tpOpen.Close()
tpConn, err := link.Tracepoint("syscalls", "sys_enter_connect",
objs.TraceConnect, nil)
if err != nil {
log.Fatalf("attaching connect tracepoint: %v", err)
}
defer tpConn.Close()
// 打开 ring buffer reader
rd, err := ringbuf.NewReader(objs.Events)
if err != nil {
log.Fatalf("opening ring buffer: %v", err)
}
defer rd.Close()
fmt.Printf("[container-guard] eBPF probes attached, monitoring...\n")
// 优雅退出处理
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
for {
select {
case <-sig:
fmt.Println("[container-guard] shutting down")
return
default:
}
record, err := rd.Read()
if err != nil {
if err == ringbuf.ErrClosed {
return
}
continue
}
var event Event
if err := binary.Read(bytes.NewReader(record.RawSample),
binary.LittleEndian, &event); err != nil {
continue
}
// 规则匹配 + 输出
switch EventType(event.Type) {
case EventTypeExec:
fmt.Printf("[ALERT] EXEC pid=%d comm=%s file=%s cgroup=%d\n",
event.Pid, event.CommStr(), event.FilenameStr(), event.CgroupID)
case EventTypeOpen:
fmt.Printf("[ALERT] OPEN pid=%d comm=%s file=%s cgroup=%d\n",
event.Pid, event.CommStr(), event.FilenameStr(), event.CgroupID)
case EventTypeConnect:
fmt.Printf("[ALERT] CONNECT pid=%d comm=%s dst=%s:%d cgroup=%d\n",
event.Pid, event.CommStr(), event.DstIPStr(),
event.DstPort, event.CgroupID)
}
}
}
4.4 性能优化的三个关键决策
在生产环境中部署上述监控器需要关注以下性能瓶颈:
1. Map lookup 的 O(1) 替代 O(n) 扫描
不要在内核侧对数组做 $O(n)$ 扫描。将白名单/黑名单预加载为 BPF_MAP_TYPE_HASH,实现 $O(1)$ 查找。前缀匹配(如检测 /etc/shadow)可使用 BPF_MAP_TYPE_LPM_TRIE(longest prefix match trie):
struct {
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
__type(key, struct ipv4_cidr); // 网络前缀
__type(value, u8); // action: 0=allow, 1=deny
__uint(max_entries, 1024);
__uint(map_flags, BPF_F_NO_PREALLOC);
} blocklist SEC(".maps");
2. 事件采样与去重
高 QPS 的 Node.js 服务可能每秒触发上万次 connect(),全量上报会导致 ring buffer 溢出。在内核侧缓存"上一秒已报过类似事件",或对低风险系统调用做概率采样:
// 采样:只上报 1% 的事件
if (bpf_get_prandom_u32() % 100 != 0)
return 0;
3. cgroup_id 缓存的 TTL 设计
PID 会被复用(fork 分配、短生命周期进程)。在 BPF_MAP_TYPE_LRU_HASH 中存储 PID → 元数据映射,利用 LRU 自动淘汰过期条目。Tetragon 对 LRU map 的 watermark 做了动态调优——当 map 容量使用超过 80% 时,主动 flush 过期条目。
五、LSM BPF 实现内核态阻断
5.1 从 Falco 和 Tetragon 学到的
上述代码还停留在"检测"层面。要实现 Tetragon 式的内核态阻断,需要将 tracepoint 升级为 LSM hook:
// LSM BPF 阻断:阻止打开敏感文件
SEC("lsm/file_open")
int BPF_PROG(restrict_sensitive_files, struct file *file)
{
// BPF_CORE_READ 读取文件路径
char path[64] = {};
struct dentry *dentry = BPF_CORE_READ(file, f_path.dentry);
bpf_probe_read_kernel_str(path, sizeof(path),
BPF_CORE_READ(dentry, d_name.name));
// 阻止读取 /etc/shadow
if (__builtin_memcmp(path, "shadow", 6) == 0) {
// 记录阻断事件
struct event e = {};
e.pid = bpf_get_current_pid_tgid() >> 32;
e.type = EVENT_BLOCKED;
bpf_ringbuf_output(&events, &e, sizeof(e), 0);
// 返回 -EPERM 拒绝操作
return -EPERM;
}
return 0; // 允许
}
对比 tracepoint 和 LSM BPF:
- Tracepoint:
SEC("tp/syscalls/sys_enter_execve"),返回 0,无法阻断 - LSM BPF:
SEC("lsm/bprm_check_security"),返回 0 / -EPERM,可以阻断
5.2 部署约束与兼容性
LSM BPF 需要内核 ≥ 5.7(LSM BPF 支持),≥ 5.10(稳定且大多数 distro 可用)。在 GKE、EKS、ACK 等托管 K8s 中,需确认节点内核版本:
| 平台 | 最低默认内核 | LSM BPF 可用? |
|---|---|---|
| GKE Standard (COS) | 5.15 | ✅ |
| EKS Amazon Linux 2023 | 6.1 | ✅ |
| ACK Pro (Alinux 3) | 5.10 | ✅ |
| 自建 (Ubuntu 20.04) | 5.4 | ❌ 需升级 |
对于内核版本不满足的集群,Tetragon 提供 fallback 到 kprobe 模式的选项(仅检测不阻断)。
六、实战场景与规则设计
6.1 场景一:容器逃逸检测
容器逃逸的典型路径包括:
1. 挂载宿主机的 /proc 或 /sys 并读写关键文件
2. 滥用 privilege 模式 + nsenter 进入 host namespace
3. 内核漏洞利用(如 dirty pipe/COW 竞争条件)
eBPF 监控规则示例:
- rule: Privileged container escape via mount
condition: >
container.privileges = 1 and
syscall.type = mount and
proc.args contains "/proc"
output: "Privileged container attempting to mount /proc (pod= %pod)"
priority: CRITICAL
6.2 场景二:加密货币挖矿检测
挖矿木马的特征非常固定:
- 连接到 stratum+tcp:// 矿池(端口 3333/4444/45762)
- 短时间内对 /sys/class/hwmon 频繁读取(获取温度)
- 进程名经常仿冒 [kworker/u2:0] 之类的内核线程名
- rule: Cryptomining detection
condition: >
container and
fd.name contains "stratum" or
(fd.sport in (3333, 4444, 45762, 14444) and fd.修辞 != 10.0.0.0/8)
output: "Possible cryptomining connection detected"
priority: EMERGENCY
6.3 场景三:Kubernetes 关键资源防护
应用级别的安全策略,防止漏洞利用后横向移动:
- rule: K8s service account token exfiltration
condition: >
container and
(proc.name = "cat" or proc.name = "cp") and
fd.name startswith "/var/run/secrets/kubernetes.io"
output: "SA token access from container proc=%proc.name file=%fd.name"
priority: HIGH
七、生产部署最佳实践
7.1 DaemonSet 部署与资源限制
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: container-guard
spec:
template:
spec:
hostPID: true # 需要访问 /proc
containers:
- name: guard
image: container-guard:latest
securityContext:
privileged: false # 不需要 privileged,eBPF 由 CAP_BPF 支持
capabilities:
add:
- BPF
- SYS_ADMIN # 用于加载 BPF 程序
- SYS_RESOURCE # 解除 rlimit
resources:
limits:
cpu: "500m" # eBPF 不应该耗尽 CPU
memory: "256Mi"
requests:
cpu: "100m"
memory: "64Mi"
7.2 事件输出到 SIEM
生产环境不建议直接 printk 日志,而应输出结构化事件到可观测平台:
- stdout JSON:Fluent Bit 采集 → Loki/Elasticsearch
- gRPC Streaming:Falco 的 gRPC output → 自研分析平台
- Kafka:大规模集群中缓冲事件流,后端消费做关联分析
7.3 性能基准
在实测中(4C8G 节点,Pod 密度 80),不同配置的 CPU 开销:
| 方案 | syscall/sec | CPU 开销 |
|---|---|---|
| 无监控 | 500,000 | baseline |
| Falco 默认规则 | 500,000 | +2~3% |
| Tetragon 全量追踪 | 500,000 | +5~8% |
| 自定义 BPF(全采无过滤) | 500,000 | +8~15% |
| 自定义 BPF(带采样+LRU) | 500,000 | +2~4% |
关键结论:合理设计的 eBPF 安全方案在 50 万 syscall/秒 的场景下可控制在 5% 以内。开销主要来自 ring buffer 争用和 BPF map 写入,而非 eBPF 程序本身的逻辑。
八、总结:选择方案的决策树
你的核心需求是什么?
├── 只需要检测 + 告警 → Falco(生态成熟、规则丰富)
├── 需要检测 + 内核态阻断 → Tetragon(LSM BPF、零窗口期)
└── 需要深度定制策略 + 学习原理 → 自建 eBPF(如上代码)
eBPF 技术的核心价值在于:让安全监控从"入侵取证后的考古"升级为"运行时的实时决策"。在容器逃逸窗口以毫秒计的场景下,这一小时的延迟就是全部风险的差距。
随着内核版本迭代和 BPF CO-RE(Compile Once, Run Everywhere)技术的成熟,eBPF 安全工具正在从"可选项"变为"必选项"。理解其原理并能自定义规则,将成为每一位 SRE/安全工程师的核心竞争力。

发表评论 取消回复