security-collector-exporter v0.3.0:eBPF 加持的实时安全监控

从静态到实时

上一篇文章介绍了 security-collector-exporter v0.1.0——把 Linux 安全配置状态变成 Prometheus 指标。但 v0.1.0 本质上还是"快照式"的:定时读 /etc/proc,抓的是某一时刻的静态配置。

安全运维里有一块是快照覆盖不了的:实时发生的安全事件。有人在跑反弹 shell、有进程在提权、有异常的网络连接、有人在加载内核模块——这些事发生了就过去了,你下次 scrape 的时候根本看不到。

eBPF 是解决这个问题的天然工具。它能挂载到内核的各种 hook 点上(tracepoint、kprobe),在内核态就完成数据聚合,零拷贝、低开销。从 v0.2.0 开始,security-collector-exporter 加入了完整的 eBPF 支持,经过 v0.3.0 的修 bug 和稳定性打磨,终于算是能用了。

eBPF 采集了什么

五个独立的 BPF 程序,分别挂载到不同的内核 hook 点,覆盖五类安全事件:

BPF 程序挂载点监控内容
process.csys_enter_execve, sched_process_exit进程执行、退出、活跃进程数
network.cinet_sock_set_stateTCP/UDP 连接建立、活跃连接、连接错误
file.csys_enter_openat敏感文件访问操作(按严重级别分类)
privilege.csys_enter_setuid/setgid/capset提权行为(setuid/setgid/capset)
kernel.csys_enter_init_module/finit_module内核模块加载/卸载

暴露的 Prometheus 指标:

text
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 进程指标(标签: type=system/user/container/suspicious)
security_ebpf_process_exec_total
security_ebpf_process_exit_total
security_ebpf_process_active_count

# 网络指标(标签: direction=in/out, protocol=tcp/udp)
security_ebpf_connect_total
security_ebpf_connect_active
security_ebpf_connect_error_total{type="timeout|refused|reset"}

# 文件访问指标(标签: severity=critical/warning/info, operation=read/write)
security_ebpf_file_access_total

# 提权指标(标签: type=setuid/setgid/capset, result=success/failure)
security_ebpf_privilege_escalation_total

# 内核模块指标(标签: action=load/load_file)
security_ebpf_kernel_module_total

# 元信息
security_ebpf_up{status="active|degraded|disabled"}
security_ebpf_sample_rate

其中进程分类挺有意思——不是简单地区分 system/user,而是四类:system(系统路径下的进程)、user(用户进程)、container(通过 cgroup ID 检测容器进程)、suspicious(可疑的解释器调用)。这样在 PromQL 里就能精准过滤:

promql
1
2
3
4
5
# 查容器里执行的进程数
security_ebpf_process_exec_total{type="container"}

# 查可疑进程执行
security_ebpf_process_exec_total{type="suspicious"}

整体架构

架构分两大层:内核态负责采集和预聚合,用户态负责读取和暴露指标。

内核态的 5 个 BPF 程序挂载到 14 个 tracepoint 上,事件发生时直接在 percpu_array map 里递增计数器,零锁竞争:

mermaid
flowchart LR

    classDef tp fill:#E3F2FD,stroke:#1565C0,color:#1565C0
    classDef bpf fill:#FFF3E0,stroke:#E65100,color:#BF360C
    classDef map fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20

    subgraph tp["Tracepoints"]
        t1@{ shape: rounded, label: "execve / exit" }
        t2@{ shape: rounded, label: "tcp state change" }
        t3@{ shape: rounded, label: "openat" }
        t4@{ shape: rounded, label: "setuid / setgid / capset" }
        t5@{ shape: rounded, label: "init_module" }
    end

    subgraph bpf["BPF 程序"]
        p@{ shape: proc, label: "process.c\n进程追踪" }
        n@{ shape: proc, label: "network.c\n网络连接" }
        f@{ shape: proc, label: "file.c\n文件访问" }
        v@{ shape: proc, label: "privilege.c\n提权检测" }
        k@{ shape: proc, label: "kernel.c\n内核模块" }
    end

    subgraph maps["percpu_array Maps"]
        m1@{ shape: cyl, label: "exec / exit / active" }
        m2@{ shape: cyl, label: "connect / error" }
        m3@{ shape: cyl, label: "access by severity" }
        m4@{ shape: cyl, label: "escalation by type" }
        m5@{ shape: cyl, label: "module by action" }
    end

    t1 --> p --> m1
    t2 --> n --> m2
    t3 --> f --> m3
    t4 --> v --> m4
    t5 --> k --> m5

    class t1,t2,t3,t4,t5 tp
    class p,n,f,v,k bpf
    class m1,m2,m3,m4,m5 map

用户态的 Manager 加载 BPF 程序并挂载 tracepoint,Aggregator 每 5 秒从 percpu_array 读取聚合数据(带 delta 追踪算 RPS),最终由 EbpfCollector 暴露为 Prometheus 指标:

mermaid
flowchart LR

    classDef datasource fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20
    classDef component fill:#EDE7F6,stroke:#4527A0,color:#311B92
    classDef endpoint fill:#FCE4EC,stroke:#C62828,color:#B71C1C

    maps@{ shape: cyl, label: "BPF percpu_array Maps" }

    subgraph go["Go 用户态"]
        mgr@{ shape: notch-rect, label: "Manager\n加载 & 挂载" }
        agg@{ shape: proc, label: "Aggregator\n读取 + delta 追踪" }
        smp@{ shape: notch-rect, label: "AdaptiveSampler\n自适应采样率" }
        col@{ shape: proc, label: "EbpfCollector" }
    end

    prom@{ shape: stadium, label: "Prometheus :9102/metrics" }

    maps -->|"每 5s 读取"| agg
    mgr -.->|"生命周期"| maps
    agg --> col
    smp -.->|"采样率"| col
    col -->|"/metrics"| prom

    class maps datasource
    class mgr,agg,smp,col component
    class prom endpoint

中间的自适应采样器(AdaptiveSampler)每 10 秒评估一次实际事件量,超过目标 RPS 的 2 倍就加倍采样率(采得更稀疏),低于一半就恢复(采得更密集),防止高负载下 eBPF 事件量压垮用户态。

v0.2.0 踩的坑

v0.2.0 是 eBPF 功能的首个版本,代码量不小(新增 ~4000 行),但发布后跑起来就发现了不少问题。

BPF 程序本身的 bug

网络 BPF 的结构体字段错误inet_sock_set_state 这个 tracepoint 的内核结构体里没有 skbaddr 字段,但 v0.2.0 的 network.c 里引用了它。BPF verifier 直接拒绝加载,网络监控整个挂掉。

UDP kprobe 不存在。v0.2.0 尝试用 kprobe 来追踪 UDP 连接,但目标内核上根本没有对应的 kprobe tracer。加载失败但没有优雅处理。

进程退出分类不准sched_process_exit 触发时,拿不到进程当初被 execve 分类时的类别。v0.2.0 没有做 PID 到类别的映射,导致 exit 事件的分类和 exec 不一致。

提权检测的 map 类型错误。privilege.c 用的是 PERCPU_ARRAY,但在多 CPU 并发写入同一个 key 时存在竞争条件。换成 PERCPU_HASH 才解决。

ARM 设备上的阻塞问题

这是一个比较隐蔽的 bug。在 ARM 设备(树莓派之类)上,端口版本探测(process_info.go 里的 Java 版本检测逻辑)会卡住 Collect() 周期。原因是某些进程(比如 mediamtx)的版本命令会 hang 住,没有超时机制,整个 Prometheus scrape 就超时了。

CI 构建问题

BPF 程序需要用 clang 编译 C 代码生成字节码(*_bpf.o),然后通过 go:embed 嵌入到 Go 二进制里。v0.2.0 把 .o 文件加到了 .gitignore,CI 环境(ubuntu-latest)没有 clang,构建直接失败。

v0.3.0 怎么修的

BPF 程序修复

c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// process.c: 新增 PID hash map,execve 时存类别,exit 时取回
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 65536);
    __type(key, __u64);    /* pid_tgid */
    __type(value, __u32);  /* category */
} pid_category SEC(".maps");

// execve 时存入
__u64 pid_tgid = bpf_get_current_pid_tgid();
bpf_map_update_elem(&pid_category, &pid_tgid, &category, BPF_ANY);

// exit 时取出
__u32 *cat_ptr = bpf_map_lookup_elem(&pid_category, &pid_tgid);
if (cat_ptr) {
    category = *cat_ptr;
    bpf_map_delete_elem(&pid_category, &pid_tgid);
} else {
    // BPF 启动前已存在的进程,用 comm 名猜测分类
    category = classify_by_comm();
}

classify_by_comm() 是一个 fallback——对于 eBPF 加载之前就已经跑着的进程,拿不到 execve 事件,就通过进程名的前四个字节做粗粒度匹配(syst → systemd、sshd → sshd 等等)。BPF verifier 不允许循环,所以只能硬编码逐字节比较。

聚合器重写

v0.2.0 的 Aggregator 是用内存数组模拟 percpu 数据的(simulation 模式)。v0.3.0 彻底重写,直接读取真实的 BPF percpu_array maps,并加了 delta 追踪:

go
1
2
3
4
5
6
7
// ReadAndUpdateFromMaps 读取所有 COUNTER 类型的 BPF maps,计算增量
func (a *Aggregator) ReadAndUpdateFromMaps() uint64 {
    // 对每个 counter map 的每个 key:
    //   当前值 - 上一次的值 = delta
    //   累加 delta 得到总事件数,喂给 AdaptiveSampler
    // processActive 和 connectActive 是 GAUGE,不参与 delta
}

ARM 阻塞修复

三个改动一起解决了这个问题:

  • executeVersionCommand 加了 3 秒超时(context.WithTimeout
  • 版本探测结果为空时缓存 5 分钟(避免反复探测已知不响应的进程)
  • getVersionFromBinaryio.LimitReader 只读前 1MB(之前会读整个二进制文件,对大文件很慢)

CI 修复

把编译好的 *_bpf.o 文件提交到 git。这些是架构无关的 BPF ELF 字节码(不是 x86 或 ARM 机器码),所以不存在跨平台问题。CI 不再需要 clang 工具链。

两个有意思的设计

自适应采样

高负载场景下 eBPF 事件量可能非常大(几万到几十万 events/sec)。AdaptiveSampler 的思路是:设定一个目标 RPS(默认 5000),每 10 秒评估一次实际 RPS——超过目标的 2 倍就加倍采样率(采得更稀疏),低于目标的一半就减半采样率(采得更密集)。采样率范围限制在 1 ~ 10000 之间。

这个采样率会作为 security_ebpf_sample_rate 指标暴露出来,查询时可以据此还原真实值:

promql
1
2
# 近似还原真实的进程执行速率
security_ebpf_process_exec_total * security_ebpf_sample_rate

Space-Saving Top-N

代码里还实现了一个 Space-Saving 算法,用于在高基数数据流中维护 Top-K 频繁项。固定数量的计数器(默认 100 个),新 key 来了如果还有空位就占一个,没空位就替换当前计数最小的那个并继承其计数。O(1) 更新,O(K log K) 查询 Top-N。这个组件为后续的"谁在频繁执行 / 谁在频繁连网"这类查询做了准备。

部署方式

启用 eBPF 需要额外参数:

bash
1
2
3
4
5
6
7
8
docker run -d \
  --name security-exporter \
  --privileged \
  -p 9102:9102 \
  ghcr.io/mickeyzzc/security-collector-exporter:0.3.0 \
  --ebpf.enabled \
  --ebpf.sample-rate=1 \
  --ebpf.max-events-per-second=5000

不传 --ebpf.enabled 的话就只跑 v0.1.0 的静态采集器,完全不影响。

内核要求 Linux 5.4+(需要 BTF 支持)。跑不了 eBPF 的环境会自动降级到 degraded 状态,security_ebpf_up{status="degraded"} 为 0,只跑传统采集器。

几个有用的 eBPF 告警规则:

yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# eBPF 监控降级
- alert: EbpfMonitoringDegraded
  expr: security_ebpf_up{status="degraded"} == 1
  labels:
    severity: warning

# 可疑进程执行
- alert: SuspiciousProcessExecuted
  expr: increase(security_ebpf_process_exec_total{type="suspicious"}[5m]) > 0
  labels:
    severity: critical

# 提权失败次数突增(可能在暴力提权)
- alert: PrivilegeEscalationAttempts
  expr: increase(security_ebpf_privilege_escalation_total{result="failure"}[10m]) > 5
  labels:
    severity: critical

# 内核模块加载
- alert: KernelModuleLoaded
  expr: increase(security_ebpf_kernel_module_total{action="load"}[5m]) > 0
  labels:
    severity: warning

版本回顾

text
1
2
3
v0.1.0 (2026-05-14)  静态安全配置采集,15 类指标
v0.2.0 (2026-05-18)  eBPF 实时监控,5 类 BPF 程序,~4000 行新代码
v0.3.0 (2026-05-19)  BPF bug 修复、ARM 兼容、CI 修复、文档清理

项目地址:github.com/mickeyzzc/security-collector-exporter