说明最近字节开源了vArmor
,刚好最近在研究eBPF
,所以就顺便看了一下vArmor
的实现,发现vArmor
的实现也是基于eBPF
的,所以就顺便记录一下。
vArmor 通过以下技术实现云原生容器沙箱
借助 Linux 的 AppArmor 或 BPF LSM,在内核中对容器进程进行强制访问控制(文件、程序、网络外联等)
为减少性能损失和增加易用性,vArmor 的安全模型为 Allow by Default,即只有显式声明的行为会被阻断
用户通过操作 CRD 实现对指定 Workload 中的容器进行沙箱加固
用户可以通过选择和配置沙箱策略(预置策略、自定义策略)来对容器进行强制访问控制。预置策略包含一些常见的提权阻断、渗透入侵防御策略。
vArmor的实现本文主要是关注vArmor
如何借用eBPF
中的LSM
技术实现对容器加固的。vArmor
的内核代码是在一个单独仓库 vArmor-ebpf
在vArmor-ebpf
中存在两个主要目录,分别是behavior
和bpfenforcer
。
behavior
就是观察模式,不会对容器的行为进行任何阻断。
bpfenforcer
,按照官方的说法,就是强制访问控制器。通过对某些行为进行阻断达到加固的目的。
behaviorbehavior
中的核心入口文件是tracer.c
。在这个文件中定义了两个raw_tracepoint
事件。
raw_tracepoint/sched_process_fork
raw_tracepoint/sched_process_exec
以其中的sched_process_exec
代码为例分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 // https://elixir.bootlin.com/linux/v5.4.196/source/fs/exec.c#L1722 SEC("raw_tracepoint/sched_process_exec" ) int tracepoint__sched__sched_process_exec (struct bpf_raw_tracepoint_args *ctx) { // TP_PROTO(struct task_struct *p, pid_t old_pid, struct linux_binprm *bprm) struct task_struct *current = (struct task_struct *)ctx ->args [0]; struct linux_binprm *bprm = (struct linux_binprm *)ctx ->args [2]; struct task_struct *parent = BPF_CORE_READ (current , parent ); struct event event = { }; event.type = 2 ; BPF_CORE_READ_INTO(&event.parent_pid, parent, pid); BPF_CORE_READ_INTO(&event.parent_tgid, parent, tgid); BPF_CORE_READ_STR_INTO(&event.parent_task, parent, comm); BPF_CORE_READ_INTO(&event.child_pid, current, pid); BPF_CORE_READ_INTO(&event.child_tgid, current, tgid); BPF_CORE_READ_STR_INTO(&event.child_task, current, comm); bpf_probe_read_kernel_str(&event.filename, sizeof (event.filename), BPF_CORE_READ(bprm, filename)); u64 env_start = 0 ; u64 env_end = 0 ; int i = 0 ; int len = 0 ; BPF_CORE_READ_INTO(&env_start, current, mm, env_start); BPF_CORE_READ_INTO(&env_end, current, mm, env_end); while (i < MAX_ENV_EXTRACT_LOOP_COUNT && env_start < env_end ) { len = bpf_probe_read_user_str(&event.env, sizeof (event.env), (void *)env_start); if ( len <= 0 ) { break ; } else if ( event.env[0 ] == 'V' && event.env[1 ] == 'A' && event.env[2 ] == 'R' && event.env[3 ] == 'M' && event.env[4 ] == 'O' && event.env[5 ] == 'R' && event.env[6 ] == '=' ) { break ; } else { env_start = env_start + len; event.env[0 ] = 0 ; i++; } } event.num = i; bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &event, sizeof (event)); return 0 ; }
通过注释,可以看到主要是基于内核5.4.196
版本开发的。
有关rawtracepoint
的原理和机制,可以参考之前写的文章rawtracepoint机制介绍 .
当一个进程执行新的可执行文件(例如通过 execve 系统调用)时,内核会发出 sched_process_exec
跟踪事件,以便跟踪和记录进程执行的相关信息。这个跟踪事件提供了以下信息:
common_type:跟踪事件的类型标识符。
common_flags:跟踪事件的标志位。
common_preempt_count:跟踪事件发生时的抢占计数。
common_pid:触发事件的进程 ID。
filename:新可执行文件的文件名。
tracepoint__sched__sched_process_exec
整体的逻辑也比较简单,通过task_struct
获得子父进程的pid
、tgid
、comm
等信息,然后通过bpf_perf_event_output
将这些信息传递给用户态。
整体来说,就是一个观察模式,不会对容器的行为进行任何阻断。
bpfenforcerenforcer
入口文件是enforcer.c
,在这个文件中定义了多个lsm
事件。包括:
capable
file_open
path_symlink
path_link
path_rename
bprm_check_security
socket_connect
具体的函数逻辑是封装在capability.h
、file.h
、process.h
、network.h
中。
具体以lsm/socket_connect
为例,分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 SEC("lsm/socket_connect" ) int BPF_PROG (varmor_socket_connect, struct socket *sock, struct sockaddr *address, int addrlen) { // Only care about ipv4 and ipv6 for now if (address->sa_family != AF_INET && address->sa_family != AF_INET6) return 0 ; // Retrieve the current task struct task_struct *current = (struct task_struct *)bpf_get_current_task (); // Whether the current task has network access control rules u32 mnt_ns = get_task_mnt_ns_id(current); u32 *vnet_inner = get_net_inner_map(mnt_ns); if (vnet_inner == NULL ) return 0 ; DEBUG_PRINT("================ lsm/socket_connect ================" ); DEBUG_PRINT("socket status: 0x%x" , sock->state); DEBUG_PRINT("socket type: 0x%x" , sock->type); DEBUG_PRINT("socket flags: 0x%x" , sock->flags); // Iterate all rules in the inner map return iterate_net_inner_map(vnet_inner, address); }
通过address->sa_family != AF_INET && address->sa_family != AF_INET6
,只关注ipv4
和ipv6
的连接。
1 2 3 4 u32 mnt_ns = get_task_mnt_ns_id(current); u32 *vnet_inner = get_net_inner_map(mnt_ns); if (vnet_inner == NULL )return 0 ;
获得当前进程的mnt_ns
,然后通过mnt_ns
获得vnet_inner
,vnet_inner
是一个bpf map
,存储了当前进程的网络访问控制规则。
整个代码的核心关键是iterate_net_inner_map(vnet_inner, address)
,iterate_net_inner_map
的实现是在network.h
中。
由于整个函数体较长,逐步分析。
1 2 3 4 5 6 7 8 9 10 for (inner_id=0 ; inner_id<NET_INNER_MAP_ENTRIES_MAX; inner_id++) { // The key of the inner map must start from 0 struct net_rule *rule = get_net_rule (vnet_inner , inner_id ); if (rule == NULL ) { DEBUG_PRINT("" ); DEBUG_PRINT("access allowed" ); return 0 ; } .... }
通过for
循环,配合get_net_rule(vnet_inner, inner_id)
获得vnet_inner
中的每一条规则。
针对每条规则,匹配address
是否符合规则,检查条件包括IP和端口信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // Check if the address matches the rule if (rule->flags & CIDR_MATCH) { for (i = 0 ; i < 4 ; i++) { ip = (addr4->sin_addr.s_addr >> (8 * i)) & 0xff ; if ((ip & rule->mask[i]) != rule->address[i]) { match = false ; break ; } } } // Check if the port matches the rule if (match && (rule->flags & PORT_MATCH) && (rule->port != bpf_ntohs(addr4->sin_port))) { match = false ; }
执行动作,如果发现匹配的规则,执行规则中定义的动作:
1 2 3 4 5 if (match) { DEBUG_PRINT("" ); DEBUG_PRINT("access denied" ); return -EPERM; }
通过返回 -EPERM
,LSM 程序可以告知内核或调用者,当前的操作被拒绝,并且可能会触发相应的权限拒绝处理逻辑。至此整个处理流程结束。
其他类型的lsm
事件,处理逻辑也是类似的,只是针对的对象不同。
说明整体来说,vArmor-ebpf
代码逻辑是很清晰的,通过eBPF
的LSM
机制,实现了对容器的加固。通过behavior
和bpfenforcer
两种模式,可以实现观察模式和阻断模式。
vArmor-ebpf
也是很好的eBPF
学习资料,可以参考和学习,后续如果有机会,也会继续深入学习。
参考https://mp.weixin.qq.com/s/5rmkALNMhA1cVsk5A14wbA https://github.com/bytedance/vArmor https://github.com/bytedance/vArmor-ebpf