说明
最近字节开源了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
,按照官方的说法,就是强制访问控制器。通过对某些行为进行阻断达到加固的目的。
behavior
behavior
中的核心入口文件是tracer.c
。在这个文件中定义了两个raw_tracepoint
事件。
raw_tracepoint/sched_process_fork
raw_tracepoint/sched_process_exec
以其中的sched_process_exec
代码为例分析:
1 | // https://elixir.bootlin.com/linux/v5.4.196/source/fs/exec.c#L1722 |
通过注释,可以看到主要是基于内核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
将这些信息传递给用户态。
整体来说,就是一个观察模式,不会对容器的行为进行任何阻断。
bpfenforcer
enforcer
入口文件是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
24SEC("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 | u32 mnt_ns = get_task_mnt_ns_id(current); |
获得当前进程的mnt_ns
,然后通过mnt_ns
获得vnet_inner
,vnet_inner
是一个bpf map
,存储了当前进程的网络访问控制规则。
整个代码的核心关键是iterate_net_inner_map(vnet_inner, address)
,iterate_net_inner_map
的实现是在network.h
中。
由于整个函数体较长,逐步分析。
1 | for(inner_id=0; inner_id<NET_INNER_MAP_ENTRIES_MAX; inner_id++) { |
通过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
5if (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