vArmor
vArmor是字节跳动推出的一款容器安全解决方案,旨在通过强制访问控制来保护容器的安全。vArmor的实现依赖于Linux的eBPF和LSM(Linux Security Module)技术。其核心目标是为容器提供一种高效的安全模型,允许用户通过配置规则来控制容器的行为。它可以用于增强容器隔离性、减少内核攻击面、增加容器逃逸或横行移动攻击的难度与成本。
vArmor 的特色
- Cloud-Native. vArmor 遵循 Kubernetes Operator 设计模式,用户可通过操作 CRD API对特定的 Workloads 进行加固。从而以更贴近业务的视角,实现对容器化微服务的沙箱加固。
- Multiple Enforcers. vArmor 将 AppArmor、BPF、Seccomp 抽象为 Enforcer,并支持单独或组合使用,从而对容器的文件访问、进程执行、网络外联、系统调用等进行访问控制。
- Allow-by-Default. vArmor 当前重点支持此安全模型。即只有显式声明的行为会被阻断,从而减少性能损失和增加易用性。
- Built-in Rules. vArmor 提供了一系列开箱即用的内置规则。这些规则为 Allow-by-Default 安全模型设计,从而极大降低对用户专业知识的要求。
- Behavior Modeling. vArmor 支持对工作负载进行行为建模。这可用于开发白名单安全策略、分析哪些内置规则可用于加固应用、指导工作负载的配置遵循权限最小化原则。
- Deny-by-Default. vArmor 可以基于行为模型创建白名单安全策略,从而确保仅显式声明的行为被允许。
技术实现
vArmor通过以下技术实现云原生容器沙箱
1. 强制访问控制
借助 Linux 的 AppArmor或 BPF LSM(Linux Security Module),vArmor 在内核中对容器进程实施强制访问控制。这包括对文件、程序、网络外联等的管理。通过这种方式,vArmor 能够有效地防止未授权访问,确保容器的安全性。
2. 安全模型
为了减少性能损失并提高易用性,vArmor 采用 Allow by Default的安全模型。这意味着只有显式声明的行为会被阻断,而其他所有行为默认被允许。这种设计降低了误报率,使用户体验更加流畅。
3. 用户操作与沙箱策略
用户可以通过操作 CRD(Custom Resource Definitions)实现对指定工作负载中容器的沙箱加固。用户能够选择和配置沙箱策略,包括预置策略和自定义策略。预置策略包含一些常见的提权阻断和渗透入侵防御策略,使用户能够快速部署有效的安全措施。
4. vArmor 的实现
本部分主要关注 vArmor 如何利用 eBPF 中的 LSM 技术实现对容器的加固。vArmor 的内核代码存放在一个单独的仓库 vArmor-ebpf中。
在 vArmor-ebpf中,存在两个主要目录:behavior和 bpfenforcer。
behavior
behavior就是观察模式,主要用于观察和分析容器的行为,而不对其进行任何阻断。其设计目标是收集运行时信息,以便进行审计和安全监控。
behavior
模块的核心功能包括:
- 事件捕获:通过raw tracepoints捕获内核中的特定事件,例如进程创建和执行。
- 数据记录:将捕获到的事件数据记录下来,以便进行后续分析和审计。
- 实时监控:支持实时监控容器的行为,方便运维人员及时发现异常。
behavior
模块的核心入口文件是tracer.c
,在此文件中定义了多个raw tracepoint事件。以下是对tracer.c
中sched_process_exec
事件的详细分析。
SEC("raw_tracepoint/sched_process_exec") int tracepoint__sched__sched_process_exec(struct bpf_raw_tracepoint_args *ctx) { 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结构体 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; } env_start += len; event.env[0] = 0; // 清空env数组 i++; } event.num = i; bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &event, sizeof(event)); return 0; }
在这个函数中,sched_process_exec
事件会在进程执行新的可执行文件时触发。其主要逻辑包括:
- 获取当前进程和父进程信息:利用
ctx->args
获取当前执行的进程及其父进程的信息。 - 填充事件结构体:将相关的进程信息(如PID、TGID、进程名称)以及执行的文件名填充到自定义的
event
结构体中。 - 环境变量处理:读取当前进程的环境变量,并将其存储在事件结构体中。
- 将事件输出到用户空间:通过
bpf_perf_event_output
将事件信息传递到用户空间的事件处理程序。
在用户空间,behavior
模块会创建一个事件读取器,用于读取从内核空间传递来的事件数据。以下是事件处理的示例代码:
func (tracer *Tracer) createBpfEventsReader() error { reader, err := perf.NewReader(tracer.objs.Events, 8192*128) if err != nil { return err } tracer.reader = reader return nil } func (tracer *Tracer) handleTraceEvents() { var event bpfEvent for { record, err := tracer.reader.Read() if err != nil { continue } if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, &event); err != nil { continue } // 将解析后的事件发送到其他goroutine进行处理 for _, eventCh := range tracer.bpfEventChs { eventCh <- event } } }
在这个代码示例中,createBpfEventsReader
函数创建了一个事件读取器,而handleTraceEvents
则是一个循环,持续读取事件,解析后将其传递给其他处理逻辑。
bpfenforcer
bpfenforcer负责将eBPF程序加载到内核中并执行安全策略。以下是该模块的详细分析。
在bpfenforcer中,eBPF程序的加载主要通过initBPF()
函数实现。该函数会调用loadBpfObjects()
来加载预编译的eBPF程序和映射。
func (enforcer *Enforcer) initBPF() error { tracer.log.Info("load bpf program and maps into the kernel") if err := loadBpfObjects(&enforcer.objs, nil); err != nil { return fmt.Errorf("loadBpfObjects() failed: %v", err) } return nil }
AttachLSM()
函数用于将eBPF程序附加到LSM钩子点。以VarmorSocketConnect
为例,代码如下:
sockConnLink, err := link.AttachLSM(link.LSMOptions{ Program: enforcer.objs.VarmorSocketConnect, }) if err != nil { return err } enforcer.sockConnLink = sockConnLink
此代码段将VarmorSocketConnect
程序与内核的socket连接事件关联,使得该程序能够在相应的事件发生时被调用。
netInnerMap
是用于保存网络规则的内存结构,其定义如下:
netInnerMap := ebpf.MapSpec{ Name: "v_net_inner_", Type: ebpf.Hash, KeySize: 4, ValueSize: 4*2 + 16*2, MaxEntries: uint32(varmortypes.MaxBpfNetworkRuleCount), } collectionSpec.Maps["v_net_outer"].InnerMap = &netInnerMap
在内核中,规则通过v_net_outer
和v_net_inner
两个BPF映射进行管理。v_net_outer
是一个哈希表,存储了与namespace相关的规则集合。
struct { __uint(type, BPF_MAP_TYPE_HASH_OF_MAPS); __uint(max_entries, OUTER_MAP_ENTRIES_MAX); __type(key, u32); __type(value, u32); } v_net_outer SEC(".maps");
在用户态,通过enforcer
模块设置规则,并将其放入v_net_inner
中。
func (tracer *Tracer) createBpfEventsReader() error {
reader, err := perf.NewReader(tracer.objs.Events, 8192*128)
if err != nil {
return err
}
tracer.reader = reader
return nil
}
func (tracer *Tracer) handleTraceEvents() {
var event bpfEvent
for {
record, err := tracer.reader.Read()
if err != nil {
continue
}
if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, &event); err != nil {
continue
}
// 将解析后的事件发送到其他goroutine进行处理
for _, eventCh := range tracer.bpfEventChs {
eventCh <- event
}
}
}
behavior与bpfenforcer的关系
- 观察与控制:
behavior
模块主要用于观察和记录容器的行为,而bpfenforcer
模块则负责实施安全策略,对特定行为进行阻断。两者结合实现了全面的安全监控和控制。 - 数据反馈:
behavior
模块所收集的事件数据可以用来分析和优化bpfenforcer
模块的规则设置,提高安全性和性能。
总结
vArmor通过eBPF
的LSM
机制,实现了对容器的加固。通过behavior
和bpfenforcer
两种模式,可以实现观察模式和阻断模式。bpfenforcer
模块负责加载eBPF程序并实施强制访问控制,而behavior
模块则用于监控和记录事件。这种架构使得vArmor能够高效地对容器行为进行管理和控制。