云原生运行时防护系统Tetragon介绍

tl;dr 文章较长,代码很多,可直接拖到文末看讲解视频。
背景 在云原生领域中,cilium是容器管理上最著名的网络编排、可观察性、网络安全的开源软件。基于革命性技术ebpf实现,并用xdp、tc等功能实现了l3、l4层的防火墙、负载均衡软件,具备优秀的网络安全处理能力,但在运行时安全上,cilium一直是缺失的。
2022年5月,在欧洲举行的kubecon技术峰会期间,cilium的母公司isovalent发布了云原生运行时防护系统tetragon[1],填补这一空缺。
tetragon的面世,意味着与falco、tracee、kubearmor、datadog-agent等几款产品正面竞争,ebpf运行时防护领域愈加内卷。
tetragon介绍 摘自tetragon官方仓库[2]的产品介绍。
ebpf实时性 tetragon 是一个运行时安全实时和可观察性工具。它直接在内核中对事件做相应动作,比如执行过滤、阻止,无需再将事件发送到用户空间处理。
对于可观察性用例,直接在内核中应用过滤器会大大减少观察开销。避免昂贵的上下文切换,尤其是对于高频事件,例如发送、读取或写入操作,减少了大量的内存、cpu等资源。
同时,tetragon提供了丰富的过滤器(文件、套接字、二进制名称、命名空间等),允许用户筛选重要且相关的事件,并传递给用户空间。
ebpf灵活性 tetragon 可以挂载到linux kernel中的任何函数,并过滤其参数、返回值、进程元数据(例如,可执行文件名称)、文件和其他属性。
跟踪策略通过编写跟踪策略,用户可以解决各种安全性和可观察性用例。tetragon社区中,提供了一些常见的跟踪策略,可以解决大部分的可观察性和安全用例。
用户也可以创建新的规则策略部署,自定义配置文件,以满足业务需求。
ebpf 内核感知 tetragon 通过ebpf钩子感知linux kernel状态,并将状态与kubernetes用户策略相结合,以创建由内核实时执行的规则,来增强云原生场景的安全防护功能。例如,当容器内恶意程序更改其权限时,我们可以创建一个策略来触发警报,甚至在进程有机会完成系统调用并可能运行其他系统调用之前终止该进程。
tetragon阻断实现原理 以上是tetragon官方的介绍,提到具备阻断能力,并在技术峰会上,展示了相关阻断的截图,有必要了解一下其实现原理。
tetragon的运行原理会在下篇详细介绍,本篇主要讲实时阻断原理。本文分析的代码版本为首次发布的tag v0.8.0[3] ,commit id:75e49ab。
业界常见方式 lkm的内核模块、ld_preload的动态链接库修改、基于lsm的selinux、seccomp技术等都是常见做内核态/用户态运行时阻断的技术方案,而缺点就比较明显,系统稳定性、规则灵活性、变更周期等问题比较突出。
当然,也有使用内核模块方式。方法是把ebpf特性,封装在内核模块里,再安装到老版本的内核上,这样,就可以覆盖更多内核版本了。但backport新特性的做法在社区里很不推荐,维护成本特别高,需要比较大的内核研发团队与深厚的技术功底。。
云原生生态中,cncf的项目falco具备内核模块与ebpf探针两套驱动引擎,提供数据收集能力。同类产品tracee也是,还基于lsm接口,实现了一定的防御阻断能力,同时支持使用者自定义语法配置文件,进行检测、判断、阻断规则的修改快速更新,以达到更好的防御能力。
作为云原生领域的容器管理软件领头羊,cilium也会落后,但linux security module[4]钩子(以下简称lsm)需要linux kernel 5.7以上版本,而业界多数内核版本都不会这么新。cilium有没有使用lsm类hook进行阻断呢?我们一起来看一下。
配置文件 前面提到,tetragon灵活性更高,可以读取配置文件规则,应用到内核态。以代码仓库的crds/examples/open_kill.yaml为例,语法规则分为如下几部分
kprobe函数名 函数原型参数 进程过滤配置 参数过滤配置 执行动作 其中,matchactions字段为匹配后的执行动作,比如这里的sigkill
 - call: __x64_sys_write    syscall: true    args:    - index: 0      type: fd    - index: 1      type: char_buf      sizeargindex: 3    - index: 2      type: size_t    selectors:    - matchpids:      - operator: notin        followforks: true        isnamespacepid: true        values:        - 0        - 1      matchargs:      - index: 0        operator: prefix        values:        - /etc/passwd      matchactions:      - action: sigkill yaml配置文件的解析在pkg/k8s/apis/cilium.io/v1alpha1/types.go中的tracingpolicyspec结构体中,包含kprobespec和tracepointspec, 对应json:kprobes和json:tracepoints两个json的结构。
type tracingpolicyspec struct { // +kubebuilderoptional // a list of kprobe specs. kprobes []kprobespec `json:kprobes` // +kubebuilderoptional // a list of tracepoint specs. tracepoints []tracepointspec `json:tracepoints`} 同时,tetragon还支持远程下发配置,配置结构与yaml结构是一样的。这里相比传统的内核模块等技术方案,灵活性更高。
用户空间 详细执行流程会在下篇分享,直入主题。
数据结构 在项目中,抽象出一些概念:
tetragon由多个sensors传感器构成 sensor由多个programs和maps构成 每个program对应ebpf代码的hook函数 每个map是相应program的bpf的数据交互map // program reprents a bpf program.type program struct { // name is the name of the bpf object file. name string // attach is the attachment point, e.g. the kernel function. attach string // label is the program section name to load from program. label string // pinpath is the pinned path to this program. note this is a relative path // based on the bpf directory fgs is running under. pinpath string // retprobe indicates whether a kprobe is a kretprobe. retprobe bool // errorfatal indicates whether a program must load and fatal otherwise. // most program will set this to true. for example, kernel functions hooks // may change across verions so different names are attempted, hence // avoiding fataling when the first attempt fails. errorfatal bool // needs override bpf program override bool // type is the type of bpf program. for example, tc, skb, tracepoint, // etc. type      string loadstate state // tracefd is needed because tracepoints are added different than kprobes // for example. the fd is to keep a reference to the tracepoint program in // order to delete it. todo: this can be moved into loaderdata for // tracepoints. tracefd int // loaderdata represents per-type specific fields. loaderdata interface{} // unloader for the program. nil if not loaded. unloader unloader.unloader}execvev53 = program.builder(    bpf_execve_event_v53.o,    sched/sched_process_exec,    tracepoint/sys_execve,    event_execve,    execve,) sensors传感器加载 默认传感器注册 pkg/sensors/tracing包下由两个文件对传感器进行默认注册,分别是generictracepoint.go与generickprobe.go,写入如下两个sensors到registeredtracingsensors中。
observerkprobesensor ,kprobe类型hook observertracepointsensor, tracepoint类型hook 同时,还会注册自定义ebpf probe加载器到registeredprobeload中。
// generickprobe.go#line=43func init() { kprobe := &observerkprobesensor{  name: kprobe sensor, } sensors.registerprobetype(generic_kprobe, kprobe) sensors.registertracingsensorsatinit(kprobe.name, kprobe) observer.registereventhandleratinit(ops.msg_op_generic_kprobe, handlegenerickprobe)} 策略文件解析 getsensorsfromparserpolicy方法遍历所有sensors,调用它的spechandler方法,解析yaml配置。解析配置后,生成新的传感器对象,作为整个应用启动、注入、监听的所有传感器。
// cmd/tetragon/main.go#line=209startsensors, err = sensors.getsensorsfromparserpolicy(&cnf.spec)// pkg/sensors/sensors.go#line=160for _, s := range registeredtracingsensors {    sensor, err := s.spechandler(spec)    if err != nil {        return nil, err    }    if sensor == nil {        continue    }    sensors = append(sensors, sensor)} 新增传感器 接上,yaml解析器读取格式后,根据当前传感器的类型(kprobe还是tracepoint),处理yaml配置文件对应的hook配置。
kprobe类型以kprobe类型的hook配置为例,调用 addgenerickprobesensors 详细处理每一个配置内容。
// pkg/sensors/tracing/generickprobe.go#line=242-516func addgenerickprobesensors(kprobes []v1alpha1.kprobespec, btfbasefile string) (*sensors.sensor, error) {    // 遍历所有kprobes的元素    for i := range kprobes {        f := &kprobes[i]        funcname := f.call        // 1. 验证配置文件中配置依赖,比如sigkill需要内核大于5.3        // 2. 解析匹配参数(进程名、namespace、路径、五元组等)写入btf对象        // 3. 解析returnarg 返回值参数,写入到btf对象        // 4. 过滤保留参数,写入btf对象        // 5. 解析filters逻辑,写入btf对象        // 6. 解析binary名字到内核数据结构体        // 7. 将属性写入btf指针,以便加载        // 8. 判断action是否为sigkill,并写入btf对象        kernelselectors, err := selectors.initkernelselectors(f)        kprobeentry := generickprobe{   loadargs: kprobeloadargs{    filters:  kernelselectors,    btf:      uintptr(btfobj),    retprobe: setretprobe,    syscall:  is_syscall,   },   argsigprinters:    argsigprinters,   argreturnprinters: argreturnprinters,   userreturnfilters: userreturnfilters,   funcname:          funcname,   pendingevents:     map[uint64]pendingevent{},   tableid:           idtable.uninitializedentryid,  }  generickprobetable.addentry(&kprobeentry)                // 9. 设定这个kprobe所需的ebpf字节码文件信息                loadprogname := bpf_generic_kprobe_v53.o        // 10. 利用如上信息,填充到prog的结构体中。        // 11. 在prog结构体中,label字段的值都是**kprobe/generic_kprobe**        load := program.builder(                path.join(option.config.hubblelib, loadprogname),                funcname,                kprobe/generic_kprobe,                kprobe+_+funcname,                generic_kprobe).                setloaderdata(kprobeentry.tableid)        }    // 创建生成新的sensor,并返回    return &sensors.sensor{  name:  __generic_kprobe_sensors__,  progs: progs,  maps:  []*program.map{}, }, nil} 解析过程如下:
验证配置文件中配置依赖,比如sigkill需要内核大于5.3 解析匹配参数(进程名、namespace、路径、五元组等)写入btf对象 解析returnarg 返回值参数,写入到btf对象 过滤保留参数,写入btf对象 解析filters逻辑,写入btf对象 解析binary名字到内核数据结构体 将属性写入btf指针,以便加载 判断action是否为sigkill,并写入btf对象 设定这个kprobe所需的ebpf字节码文件信息 利用如上信息,填充到prog的结构体中。 在prog结构体中,label字段的值都是kprobe/generic_kprobe 解析完成后,返回一个新的sensor,并添加到sensors传感器数组中。
至此,完成了配置文件的解析。在这里,阻断指令的配置,是保存在generickprobe.loadargs.filters这个byte数组中的。
动态更新 在tetragon项目中,还具备从远程下发新的规则,更新、添加sensor传感器功能,相应代码在pkg/observer/observer.go中,本篇不做过多展开,会在下篇分享。
// initsensormanager starts the sensor controller and stt manager.func (k *observer) initsensormanager() error { var err error sensormanager, err = sensors.startsensormanager(k.bpfdir, k.mapdir, k.ciliumdir) return err} ebpf加载与挂载 sensors启动 传感器启动加载,执行流程为obs.start(ctx, startsensors) -> config.loadconfig -> load.load 。
在load方法里,对每一个program元素,调用observerloadinstance方法进行加载。
// load.load
// load loads the sensor, by loading all the bpf programs and maps.func (s *sensor) load(stopctx context.context, bpfdir, mapdir, ciliumdir string) error {    for _, p := range s.progs {            // ...                        // 加载每个prog            if err := observerloadinstance(stopctx, bpfdir, mapdir, ciliumdir, p); err != nil {                return err            }           // ... }} ebpf program加载、挂载 在对每个ebpf program进行加载时,会判断hook的类型,针对tracepoint特殊判断处理。这里还是以kprobe为例。代码调用loadinstance函数,逻辑中判断是否存在自定义的加载器:
若有,则调用s.loadprobe加载; 若没有,则调用loader.loadkprobeprogram加载; // pkg/sensors/load.go#line=297if s, ok := registeredprobeload[load.type]; ok {   logger.getlogger().withfield(program, load.name).withfield(type, load.type).infof(load probe)   return s.loadprobe(loadprobeargs{    bpfdir:    bpfdir,    mapdir:    mapdir,    ciliumdir: ciliumdir,    load:      load,    version:   version,    verbose:   verbose,   })  }  return loader.loadkprobeprogram(   version, verbose,   btfobj,   load.name,   load.attach,   load.label,   filepath.join(bpfdir, load.pinpath),   mapdir,   load.retprobe) 同样,以前面提到的observerkprobesensor类型传感器,已经注册自己的probe加载器,那么会走s.loadprobe()逻辑,之后,调用loadgenerickprobesensor() -> loadgenerickprobe()进行加载。
这里的load.name、load.attach、load.label的值,来自前面的yaml配置文件读取部分,值分别为bpf_generic_kprobe_v53.o、 __x64_sys_write 、 kprobe/generic_kprobe,也就是说,不管是哪个kprobe函数,都会被挂载到kprobe/generic_kprobe上,都被generic_kprobe_event()这个ebpf 函数处理,起到统一管理的网关作用。
kprobe/generic_kprobe对应的ebpf代码在bpf/process/bpf_generic_kprobe.c文件里,我们在后面内核空间代码详细分析。
__attribute__((section((kprobe/generic_kprobe)), used)) intgeneric_kprobe_event(struct pt_regs *ctx){ return generic_kprobe_start_process_filter(ctx);} filters过滤器 loadgenerickprobe()函数最后一个参数filters过滤器参数,类型是[4096]byte
func loadgenerickprobe(bpfdir, mapdir string, version int, p *program.program, btf uintptr, genmapdir string, filters [4096]byte) error {} 其内容为前面yaml格式解析中的第5步,
kernelselectors, err := selectors.initkernelselectors(f) 格式构成为
filter := [length][matchpids][matchbinaries][matchargs][matchnamespaces][matchcapabilities][matchnamespacechanges][matchcapabilitychanges]
这些数据,也是在内核空间ebpf逻辑中,实现参数匹配,动作响应的判断依据。
cgo函数调用 在调用bpf syscall的实现上,tetragon没有使用母公司自己的cilium/ebpf库,而是使用cgo包装了libbpf进行加载。使用的版本还是0.2.0,当前社区最新版为0.7.0,比较老。(下一篇再讲原因)
loadgenerickprobe()函数调用bpf.loadgenerickprobeprogram(),并把filters传递给下一个cgo的函数c.generic_kprobe_loader(),函数定义在pkg/bpf/loader.go的476行。
int generic_kprobe_loader(const int version,    const int verbosity,    bool override,    void *btf,    const char *prog,    const char *attach,    const char *label,    const char *__prog,    const char *mapdir,    const char *genmapdir,    void *filters) { struct bpf_object *obj; int err; obj = generic_loader_args(version, verbosity, override, btf, prog, attach,      label, __prog, mapdir, filters, bpf_prog_type_kprobe); if (!obj) {  return -1; }    // ...} 之后,再调用cgo的c函数generic_loader_args()进行bpf syscall调用,加载ebpf程序,挂载到对应kprobe函数上。之后,再写入ebpf maps。
ebpf maps创建写入 tetragon使用ebpf maps进行用户空间与内核空间的配置数据交互,比如yaml配置文件中,各种过滤条件,匹配后的处理动作等。
还是以open_kill.yaml为例,涉及了两类ebpf map:
特征匹配规则,也就是配置的内容,比如需要保护的文件路径、ip黑名单等,称之为filters规则 路由分发规则,也就是tetragon程序内部,用于ebpf hook的函数网关处理各类参数的自用规则,用尾调用tail call类型的map实现。 filters规则map 在generic_loader_args()里,创建了filter_map ebpf map,并将判断规则、阻断规则filter bytes数组写入到这个map里,格式依旧是[4096]byte的字节流。
// ...char *filter_map = filter_map;// ...map_fd = bpf_object__find_map_fd_by_name(obj, filter_map); if (map_fd >= 0) {  err = bpf_map_update_elem(map_fd, &zero, filter, bpf_any);  if (err) {   printf(warning: map update elem %s error %d, filter_map, err);  } } tail call尾调用map 在ebpf program加载部分提到,ebpf kprobe函数kprobe/generic_kprobe(即generic_kprobe_event())作为统一的过滤处理网关,针对不同的kprobe,其参数个数、参数类型一定是不一样的,比如文件读写函数__x64_sys_write有三个参数,分别是
args:    - index: 0      type: fd    - index: 1      type: char_buf      sizeargindex: 3    - index: 2      type: size_t 而socket连接函数__x64_connect只有两个参数,分别是
args:  - index: 0    type: sockfd  - index: 1    type: sockaddr 并且,他们的参数类型也不一样,作为统一网关,且面对参数个数、参数类型都不一样的问题,tetragon在ebpf的解决方案是使用bpf_map_type_prog_array类型的ebpf map实现,用于尾调用tail call处理。,
kprobe_calls尾调用map
struct bpf_map_def __attribute__((section(maps), used)) kprobe_calls = { .type = bpf_map_type_prog_array, .key_size = sizeof(__u32), .value_size = sizeof(__u32), .max_entries = 11,}; kprobe_calls map写入
map_bpf = bpf_object__find_map_by_name(obj, kprobe_calls);// ...err = bpf_map__pin(map_bpf, map_name);// ...map_fd = bpf_map__fd(map_bpf);for (i = 0; i < 11; i++) {    struct bpf_program *prog;    char prog_name[20];    char pin_name[200];    int fd;    snprintf(prog_name, sizeof(prog_name), kprobe/%i, i);    prog = bpf_object__find_program_by_title(obj, prog_name);    if (!prog)        goto out;    fd = bpf_program__fd(prog);    if (fd act[i];  switch (action) { case action_unfollowfd: case action_followfd:    // ...  break; case action_sigkill:  if (bpf_core_enum_value(tetragon_args, sigkill))   send_signal(fgs_sigkill);  break; case action_override:   default:  break; } if (!err) {  e->action = action;  return ++i; } return -1;} 可以看到,针对类型,是调用了send_signal()函数进行下发fgs_sigkill指令给当前进程,完整阻断动作。send_signal()函数是ebpf的内置函数,在kernel 5.3版本[5]里增加。
阻断演示视频可以到cncf (cloud native computing foundation)的油管观看:real time security - ebpf for preventing attacks - liz rice, isovalent[6]
lsm hook比较lsm类hook是在kernel 5.7以后才添加。阻断功能的实现,tetragon选择send_signal()的方式,有着兼容更多内核版本的优势。并且其kprobe的hook点上,可以实现网关式通用处理,通过配置方式,更灵活地变更hook点,避免更新ebpf字节码的方式。
更灵活 网关式 内核版本覆盖多 总结 tetragon是一个实时识别阻断的运行时防护系统。具备网关式统一处理抓手,可以覆盖更多内核版本,通过配置文件方式灵活变更hook点。在ebpf技术支持下,还具备热挂载,系统稳定性高,程序可靠性高等特点。是主机运行时防护系统hids的最佳学习项目。
笔者水平有限,若有错误,欢迎指出,谢谢。
tetragon阻断争论 2022年5月,云原生安全公司isovalent的cto宣布开源了其内部开发了多年的基于ebpf安全监控和阻断的方案:tetragon。称可以防御容器逃逸的linux内核漏洞。
安全研究人员felix wilhelm的质疑,在tetragone: a lesson in security fundamentals[7]认为可以轻易绕过,并用cve-2021-22555[8]漏洞修改版演示。
这篇文章从标题上都充满了各种嘲讽,tetragon单词加了e,大概是gone的谐音吧。grsecurity是linux 内核安全经验非常深厚的大厂,对这个领域比较精通。但tetragon的优势并不是内核底层安全能力。
赛博堡垒(hardenedvault)也撰写一篇文章,云原生安全tetragon案例之安全产品自防护[9] 认为该产品必定失败。
随着云原生的流行,linux内核安全成为了一个无法绕开的问题,某个容器被攻陷后可以向linux内核发起攻击,一旦成功则整个主机都会被攻击者控制,如果你不想你的产品耗资上百万美金后攻击者两个小时就攻陷的话,那应该认真的考虑是否应该从一开始就建立正确的威胁模型。另外,ebpf机制更适合实现审计监控类的安全方案而非防护阻断类,ved的ebpf版本也仅仅是为审计而设计,剩下的事情你应该让siem和soc团队去做,在安全流程上我们也应该遵循kiss(keep it simple, stupid!)原则,不是吗?
我的看法 针对漏洞利用的方法(注:不是漏洞)的防御机制通常会针对三个阶段:
pre-exploitation(前漏洞利用阶段) exploitation(漏洞利用阶段) post-exploitation(后漏洞利用阶段) tetragon的阻断功能是在exploitation漏洞利用阶段生效的,因为是可以直接阻断,让此次漏洞攻击失败。其次,认可几位安全人员的关于tetragon适用场景说法,更适合阻断用户空间的内核逃逸漏洞。
否定的声音来自底层防御的传统厂商,其实他们没明白,tetragon的优势是可以动态、轻量、无感知的提升防御能力,并不是完全防御所有攻击方式。
业务优先于安全 在云原生领域,业务类型多数是web服务,安全级别不需要那么高,硬件宿主机重启成本较高,性能要求大,业务优先,安全其次。过于严格的安全检测,占用过多资源,影响业务运行速度,性价比低,成本高。这是云原生场景不能接受的。但偶尔的入侵行为是能容忍的。所以业务优先级大于安全是第一守则。
安全优先于业务 传统安全厂商的产品对系统稳定性、可用性、性能都有较大影响,且存在热更新的难题,哪怕解决了,依旧是特别重的方案。在轻量、动态、热更新的需求下,显得特别笨重。
机密数据库等保密程度较高的服务器,数据安全大于业务功能,愿意牺牲性能换取安全性,那么这种场景适合传统安全厂商的产品。这种场景的规则是安全优先级大于业务,更适合传统安全厂商发挥。
争议总结 当今互联网的服务器市场中,云原生业务占比越来越高,这将会是tetragon、falco、tracee、datadog等运行时安全产品愈加内卷的动力。
对于用户来说,根据自己的业务特性,选择相应的防御检测产品,满足自己业务需求。
原文标题:tetragon阻断争论
文章出处:【微信公众号:一口linux】欢迎添加关注!文章转载请注明出处。

比亚迪新能源市场下滑明显,国产特斯拉势头正劲
用二维材料制作有效的光吸收器
具有HART的完全隔离、单通道电压、4mA至20mA输出电路图
如何安装功能吸引人的tvOS 14?
2020最具性价比的半入耳蓝牙耳机,NANK南卡LITE新品发布!!
云原生运行时防护系统Tetragon介绍
STM32设置时钟的操作方法和步骤
联想WatchS体验 在智能与美观二者之间做了很好的协调
汽轮机内部损失有哪些?其意义如何?
Consensic感芯半导体推出MEMS电容式压力传感器CPS120
第三季度TCL电子逆势大增,带动行业智能化升级
ESD静电整改有什么基本思路?
NVIDIA天价收购ARM,最大挑战在于中国监管机构的审批
国网德州供电公司利用大数据平台,构建企业复工复产电力指数
RLAIF:一个不依赖人工的RLHF替代方案
智慧城市的到来我们可以拥有什么
想要分析网络变更会有什么影响
英特尔宣布放弃NUC业务!
浅谈服务机器人
中国芯片最新50强榜单发布!