土法炼钢 · 系统与基础设施

【Tetragon / eBPF】事件导出路径:JSON、gRPC、tetra 与过滤边界

文章导航

分类入口
kubernetessecurity
标签入口
#tetragon#ebpf#grpc#tetra#export-filter#redaction#field-filter#v1.7.0

目录

第 3 篇 把事件送进 ring;本篇钉 过滤分层轴在用户态的一半。读者最常见的误判是:Helm 里配了 denylist,于是认定「集群里已经没有这类事件」。v1.7.0 Events 文档用 Caution 把这句话钉死——allowlist / denylist 只作用于 JSON 文件 sink;gRPC 与默认的 tetra getevents 仍看到未按这两份名单过滤的事件。

本篇在系列中的位置

篇目 核心内容
第 3 篇 · Sensor 与 Hook 事件从哪类 hook 来
第 4 篇 · 事件导出路径 JSON / gRPC / tetra;allow·deny、field filter、redaction
第 5 篇 · Agent 生命周期 BTF 加载与三条策略路径
系列目录 全部篇目

版本锚定:Tetragon v1.7.0(docs/.../concepts/events.md;pkg/exporter/exporter.go;pkg/filters/;pkg/fieldfilters/;api/v1/tetragon/events.proto)。JSON 默认路径以 tag 文档为准:/var/run/cilium/tetragon/tetragon.log。gRPC 默认:unix:///var/run/tetragon/tetragon.sock。不以 live 站点的迁仓说明覆盖这两处。无真实节点则不粘贴伪造导出;配置 JSON 来自官方文档示例。

内核 selector(matchArgs 等)在 hook 内丢事件,属于第 7 篇。本篇只讨论 已经通过内核、到达 agent 之后 的分流。


一、三条出口

flowchart LR
  ring["Ring to agent"] --> split{"Sink"}
  split -->|"JSON allow deny"| json["tetragon.log"]
  split -->|"gRPC unfiltered"| grpc["tetra getevents"]
  json --> pipe["tetra reading JSON stdin"]

图:与第 1 篇五轴图中的 sink 分流一致。第三条路不是独立内核出口,而是 JSON 文件的消费者。

v1.7.0 Events 文档把观察方式分成 JSON、tetra CLI、gRPC。工程上要拆成三条可见集合不同的路:

出口 文档中的用法 过滤从哪来 默认落点
文件 JSON 容器 export-stdout 日志,或读 agent 写出的文件 agent 为 exporter 构造的 GetEventsRequest(allow/deny、可选 field filter) /var/run/cilium/tetragon/tetragon.log,默认轮转压缩
gRPC 监听地址 --server-address;Helm tetragon.grpc.address 该次 GetEventsRequest 自己的 allow/deny/field_filters 默认 unix:///var/run/tetragon/tetragon.sock
把 JSON 再喂给 tetra kubectl logs ... -c export-stdout -f \| tetra getevents 已经过文件 sink 过滤;tetra 只做二次显示过滤(进程名、pod 等 CLI 旗标) 标准输入

pkg/exporter/exporter.go 的 Start 调用 server.GetEventsWG,把自身当作 gRPC 流的接收端。NewExporter 从 option.Config.ExportFilename 拆出文件名与目录;rotationInterval < 0 直接报错。Send 里做可选速率限制,再 encoder.Encode 写成 JSON:

// project: cilium/tetragon, tag: v1.7.0
// path: pkg/exporter/exporter.go
// function: (*Exporter).Send
func (e *Exporter) Send(event *tetragon.GetEventsResponse) error {
    if e.rateLimiter != nil && !e.rateLimiter.Allow() {
        e.rateLimiter.Drop()
        rateLimitDropped.Inc()
        return nil
    }
    if err := e.encoder.Encode(event); err != nil {
        logger.GetLogger().Warn("Failed to JSON encode", logfields.Error, err)
    }
    eventsExportedTotal.Inc()
    eventsExportTimestamp.Set(float64(event.GetTime().GetSeconds()))
    return nil
}

因此「JSON 导出」在实现上是 同一套 GetEvents API 的一个客户端,配置来自 daemon 的 export 旗标,而不是另一套 BPF。编码失败只打日志,函数仍返回 nil——文件里缺行时不要只查 denylist。

文档给出的对照命令语义(未在本仓库执行):

# 消费已经按 JSON export 规则过滤过的行
kubectl logs -n kube-system -l app.kubernetes.io/name=tetragon -c export-stdout -f | tetra getevents -o compact

# 容器内直接跑 tetra:走 gRPC,不经过文件 allow/deny
kubectl exec -it -n kube-system ds/tetragon -c tetragon -- tetra getevents -o compact

文档在 gRPC 节写明:tetra 没有把 JSON piped 进来时,就是 gRPC 客户端。Helm 示例里的 localhost:54321 是改地址的方法,不是 v1.7.0 的默认监听。默认 Unix socket 属于 Upgrade notes 与 events.md 共同钉死的行为;第三方客户端若仍连旧 TCP 端口,会表现为「agent 正常、订阅为空」。第 5、12 篇回收地址变更。

EventLogService(eventlogservice.proto)只调整 JSON 文件的 lumberjack 参数(max_size、rotation_interval、max_backups),不改变 gRPC 可见集合。


二、allow / deny 只作用于 file sink

Export filter 是一行一个 JSON 对象。行与行之间 OR,同一对象内字段 AND。仅配置 allowlist 时导出是默认拒绝;denylist 与 allowlist 同时命中时 denylist 优先。两边都不配则 JSON 导出全部事件。

官方文档示例(配置语义,不是事件样本):

{"event_set": ["PROCESS_EXEC", "PROCESS_EXIT"], "namespace": ["foo"]}
{"event_set": ["PROCESS_KPROBE"]}

匹配含义:(PROCESS_EXEC|PROCESS_EXIT) ∧ namespace=foo 或 PROCESS_KPROBE。

实现:pkg/filters/filters.go 的 Apply 为 whitelist.MatchOne(ev) && blacklist.MatchNone(ev);空名单视为通过。ParseFilterList 按 JSON 流解码为 tetragon.Filter。

// project: cilium/tetragon, tag: v1.7.0
// path: pkg/filters/filters.go
// function: Apply
func Apply(whitelist, blacklist FilterFuncs, ev *event.Event) bool {
    return whitelist.MatchOne(ev) && blacklist.MatchNone(ev)
}

events.proto 的 GetEventsRequest 把这两份名单叫做 allow_list / deny_list,并写明结果是集合差 allow_list - deny_list。同一 Filter 对象内部多个字段是 AND(BuildFilterList 里每行一个 MatchAll);多行之间是 OR(MatchOne)。这与文档「行 OR、行内 AND」一致。

Caution(v1.7.0 events.md 原文语义):上述 denylist / allowlist 只应用于导出成 JSON 文件的事件(file sink)。它们 不影响经 gRPC API 流出、被 tetra CLI 消费的事件。即使某事件会被 JSON denylist 丢掉,使用 tetra CLI 时你仍会看到未按该名单过滤的事件。

这不是 CLI 的 bug。JSON exporter 与交互式 tetra 是两个 GetEvents 客户端:前者带 daemon 配置的名单,后者默认不带那份名单。把 JSON 管道进 tetra 时,你看到的是已经按文件规则滤过的子集,再叠加 CLI 自己的 --processes / --pod。排障时必须先问「这条 tetra 命令有没有 stdin JSON」。

Filter 字段(events.proto @ v1.7.0)与文档表对照如下。写过滤器以 proto 为准:文档表未列全 PROCESS_THROTTLE / PROCESS_LSM / PROCESS_USDT。

字段 匹配对象
event_set EventType 枚举
binary_regex / parent_binary_regex / ancestor_binary_regex RE2,后者要求该事件类型开启 ancestors
namespace / namespace_regex Pod 命名空间;空字符串匹配非 Pod 进程
pod_regex / labels / container_id / container_name_regex K8s / 容器元数据;labels 永不匹配无 pod 的主机事件
in_init_tree 容器 init 子树,见 第 2 篇
cel_expression 用户态 CEL(可带 k8s IP/CIDR 扩展)
health_check 二进制是否像对应 Pod 的 liveness/readiness exec
pid_set PID 及其子孙;注释写明面向测试,生产需 --enable-pid-set-filter

CEL 这里不是第 7 篇的 CEL-in-BPF 选择器。ancestor_binary_regex 在未开启 ancestors 时,CheckAncestorsEnabled 会让构建过滤器失败。

内核已经 NoPost 的事件不会到达本层。allow/deny 不能把内核丢掉的事件找回来,只能再砍 agent 已经拿到的集合。


三、field filter

Field filter 限制留下哪些 protobuf 字段,不决定事件是否存在。写法同样是一行一个 JSON,fields 用 protobuf field mask(点分路径,逗号分隔多路径)。action 为 INCLUDE 或 EXCLUDE。可按 event_set 限定类型;invert_event_set 反转该集合。

默认:某事件类型没有任何 field filter 时导出全部字段。一旦对该类型定义了 INCLUDE,未列入的字段默认不再出现。EXCLUDE 与 INCLUDE 冲突时,GetEventsRequest 注释写 exclusion always takes precedence。

文档示例:除 PROCESS_EXEC 外,只保留 process.exec_id 与 process.parent_exec_id:

{"fields":"process.exec_id,process.parent_exec_id", "event_set": ["PROCESS_EXEC"], "invert_event_set": true, "action": "INCLUDE"}

pkg/fieldfilters/fields.go 在 JSON 反序列化前把 snake_case 改成 protobuf JSON 期望的 camelCase(fixupFieldFilterString)。Filter 方法只改 event oneof 内部,node_name / time 等公共字段保留。

Field filter 活在 GetEventsRequest.field_filters 里,因此:

这与 allow/deny 同构:都是 每个 GetEvents 客户端一份,不是「内核里已经删了字段」。SIEM 映射失败时先看 field filter 是否把 pod 砍掉,再看策略。


四、redaction 与环境变量

Redaction 针对参数和环境变量里的敏感子串,避免它们经 JSON 导出被带出集群——文档这一节的标题如此。实现却比 allow/deny 更早:initProcessInternalExec(pkg/process/process.go)在写入 process cache 之前调用 fieldfilters.RedactionFilters.Redact(binary, args, envs)。因此 gRPC 客户端拿到的 Process.arguments 同样可能已被打码。排障时不要假设「tetra 看见明文、文件里是 *****」——对 redaction 而言两套 sink 共享 cache 里那份已经改过的字符串。

配置:--redaction-filters 或 Helm redactionFilters。JSON 对象含 redact(RE2 列表)与可选 binary_regex。只有捕获组被替换成 *****(常量 REDACTION_STR)。文档 Warning:JSON 里反斜杠要再转义。RE2 语法以 Google RE2 为准,不要按 PCRE 去写回溯。

文档示例(配置,非本机事件):

{"redact": ["--password(?:\\s+|=)(\\S*)"]}

含义:参数里 --password=foo 变为 --password=*****。按二进制限制:

{"binary_regex": ["(?:^|/)foo$"], "redact": ["-p(?:\\s+|=)(\\S*)"]}

环境变量同理,例如 {"redact": ["(?:SSHPASS=)+(\\S+)"]}。

--filter-environment-variables VAR1[,VAR2..] 在 redaction 之前把未点名的环境变量删掉(slices.DeleteFunc)。这是白名单保留,不是打码。未开此旗标且未配 redaction 时,开启 env 采集会把机密送进所有 sink——采集开关在 __base__ 的 ENV_VARS_ENABLED(第 3 篇),本篇管的是已经采集之后如何裁剪。

Redact 对每个 redaction 对象:若配置了 binary_regex 且全部不匹配,则跳过;否则对 argv 与每个 env 字符串做 ReplaceAllStringFunc,只替换捕获组区间。嵌套捕获组若起点落在已打码区间则跳过,避免把 ***** 再切碎。RE2 不支持回溯引用的写法在 Go regexp 上会编译失败,agent 启动时应直接报错,而不是静默不过滤。

RedactionFilter.match 在 proto 里已 deprecated,不要写进新配置。

Redaction 在用户态、在 hook 返回之后。它不阻止内核日志或审计的其他通道看见同一 argv;它只约束 Tetragon 事件对象。TOCTOU 与「参数在用户态被改写」仍属 hook 轴(第 3、10 篇)。


五、失败表象

表象 先点哪一层 不要先做什么
SIEM(读 tetragon.log)没有,节点上 tetra getevents(无管道)有 JSON allow/deny;官方 Caution 重装 DaemonSet;改 TracingPolicy 符号
管道 logs \| tetra 与无管道 tetra 不一致 你在比较 file sink 与 gRPC 认定 tetra 解析 JSON 坏了
事件在,但没有 pod / parent field filter INCLUDE;或血缘轴 cache miss 加大 denylist
arguments 里是 *****,策略却按明文写的 redaction 已改 cache 在选择器里匹配打码后的字符串却期望明文
两边都没有事件 内核 selector / hook / 加载轴(第 3、6–7 篇) 只调 export Helm 值
配置了 CEL export filter 无效果 是否配在 JSON exporter 的名单里;gRPC 请求是否带同一 cel_expression 把它当成内核 CEL-in-BPF
日志文件找不到 tag 文档路径 /var/run/cilium/tetragon/tetragon.log;Helm 是否改了位置 用 live 文档的另一默认目录当 v1.7.0

Exporter.Send 在 rateLimiter 不允许时丢弃并计数,返回 nil(不报错给调用方)。这是用户态第二层丢事件,表象与 denylist 相似,指标不同。节流与 cgroup-rate 见第 11 篇。

谱系:McCanne & Jacobson(1993)把过滤放进内核 VM——对应本系列的 selector,不对应 JSON denylist。Garfinkel(NDSS 2003)关心拦截完整性:用户态再滤一层会让「监控面」与「强制执行面」分叉。Tetragon 把分叉写成文档 Caution,而不是藏起来。Höiland-Jørgensen et al.(2018)的 XDP 丢包发生在驱动路径,与 file sink 丢 JSON 行没有同一套计数语义。

争论:导出采样与真实事件完备性。A 侧要 SIEM 体积可控,用 allow/deny 与 field filter 砍字段。B 侧取证要求 gRPC 所见与落盘所见可对齐。v1.7.0 明确选择不对齐:同一 agent、两套客户端配置。可检验命题是:runbook 是否强制「对账必须点名 sink」。若合规审计把 tetra 屏幕当证据,却用 denylist 证明「未发生」,结论不成立。

开放问题:

  1. 能否在不改变 Caution 语义的前提下,让两个 sink 的差异可被单一 metric 证明——第 12 篇看 exporter 指标,不在本篇编数字。
  2. redaction 发生在 cache 构造时,对「只想打码 JSON、保留 gRPC 明文」没有开关——这是 v1.7.0 实现边界。
  3. 内核过滤与用户态过滤的集合差,如何在排障坐标系里变成检查清单——第 13 篇。

六、小结

  1. 三条出口:JSON 文件、gRPC、把 JSON 喂给 tetra。后一条是文件的消费者。
  2. allow/deny 只作用于 file sink;官方 Caution 覆盖 tetra 默认 gRPC 路径。
  3. field filter 按 GetEvents 客户端裁字段;管道 JSON 不可恢复。
  4. redaction 在写入 process cache 时改 argv/env,因此进入所有 sink。
  5. 默认 JSON 路径是 /var/run/cilium/tetragon/tetragon.log。下一篇回到 agent:程序没挂上时,导出层再干净也没有事件。

七、参考资料

规范 / 官方文档(A)

源码(A)

核心论文

实验 / 工具

站内对照


→ 上一篇:Sensor 与 Hook · 系列目录 · 下一篇:Agent 生命周期

读完这篇,下一步读什么

优先读同系列或同问题的下一篇,把单篇消费变成主题集群。


By .