近期针对Linux内核的CVE漏洞频出,CVE-2022-0185、CVE-2022-0185、CVE-2022-0847是威胁评分较高且热度较高的几个典型漏洞,相关的POC/EXP利用代码也已经在互联网上公开披露。对于容器场景来说,攻击者的攻击路径也比较相似,都是利用unshare等高危系统调用在新的usernamespace拿到CAP_SYS_ADMIN等高权限capabilities后利用漏洞代码实施越权逃逸。
从安全角度出发,K8s应用部署默认配置的seccomp=unconfined会放开所有系统调用,无疑给攻击者提供了很大的攻击面,尤其对于共享主机内核的非安全沙箱容器,攻击者可以直接利用主机内核能力实现逃逸。而seccomp作为Linux内核中提供的安全机制,可以通过策略定制的方式帮助约束应用侧对内核syscall的访问范围,是限制攻击者利用漏洞的上佳工具。尽管如此,如何用好seccomp一直是困扰开发者的头疼问题,虽然相较于SELinux,AppArmor等前辈特性,seccomp已经在易用性上作出了一定提升,但是即使是对安全专家来说,真正在生产环境上去精细化配置部署seccomp也不是一件容易的事情。
本文尝试在云原生K8s场景下说明seccomp的使用方式,介绍一些能够帮助开发运维人员简化seccomp策略自定义配置的方法论和开源工具,同时基于前人经验总结使用seccomp的最佳实践。
Seccomp是什么?
Seccomp(Secure Computing 安全计算模式的简称)是Linux内核中的一个安全特性,它可以限制用户态程序对syscall的使用,最早是2005年引入的,当时引入的目的是为了租借服务器上空闲的CPU资源跑一些非授信程序,所以当时的设计只允许了4个系统调用:read, write, sigreturn (或者 rt_sigreturn) 和 exit,这样的设计几乎让seccomp无法在实际的生产环境应用,比如一个应用需要socket通讯,那么socket必需在seccomp使用前规划开启完成。。。直到2012年Linux3.5内核版本引入了基于BPF的seccomp过滤模式(SECCOMP_MODE_FILTER),在该模式下用户可以通过定制化的策略细粒度过滤应用程序中可以使用哪些syscall。
一个seccomp profile示例如下:
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": [
"SCMP_ARCH_X86_64",
"SCMP_ARCH_X86",
"SCMP_ARCH_X32"
],
"syscalls": [{
"names": [
"accept4",
"epoll_wait"
],
"action": "SCMP_ACT_ALLOW",
"args": [],
"includes": {},
"excludes": {}
}]
}
集群管理运维人员可以在策略中定义系统调用集合和对应处理动作的映射关系,规则定义一般包含:
-
defaultAction中定义一个默认的过滤动作,比如可以在安全模式下可以默认拦截所有syscall
-
根据不同的OS体系结构指定不同的syscall集合
-
以白名单形式放行的syscall列表
-
4.14以上内核可以通过ACT_LOG模式打底配合ERROR拦截syscall的黑名单实现相对柔和的 complain-mode模式。
Seccomp in Kubernetes
由于docker从1.10版本加入了对seccomp的自定义参数配置,K8s社区为了兼容性,早在16年1.2版本就去掉了默认的seccomp profile配置,参见#21790。与此同时,社区也陷入了对如何支持seccomp的长时间拉锯,参见#24602,#135,#1747, 目前seccomp特性已经在1.19版本GA,我们可以通过如下方式在应用中完成配置:
通过指定的annotation标签配置:
早在1.3版本社区就已经支持通过annotations标签的方式向应用pod中添加seccomp配置,用户可以通过如下的annotations标签在pod或container维度配置seccomp策略:
pod维度:
annotations:
seccomp.security.alpha.kubernetes.io/pod: "localhost/profile.json"
container维度:
annotations:
container.security.alpha.kubernetes.io/<container-name>: "localhost/profile.json"
这里runtime会默认从节点上配置的seccomp策略文件目录(ACK集群上默认为/var/lib/kubelet/seccomp)中加载名称为profile.json的配置文件,这里的配置value支持以下三种策略:
-
unconfined - 不使用seccomp过滤
-
localhost/<profile-name> - 使用节点本地seccomp profile root路径下自定义的配置文件
注意:当前该annotations标签已经为废弃状态,预计社区1.25版本会删除该标签,后续统一使用下面介绍的securityContext配置。
通过security-context配置:
社区从1.19版本seccomp特性GA开始在securityContext配置中增加了seccompProfile的字段,用户可以直接在此配置Pod和Container维度的seccomp策略,由于annotations配置的方式已经是废弃状态,为了今后不必要的兼容性调整,推荐使用该方式配置:
securityContext:
seccompProfile:
type: Localhost
localhostProfile: my-seccomp-profile.json
这里的配置类型同样是如下三种:
-
Unconfined : 不启用seccomp
-
Localhost : 节点本地seccomp配置根目录 (ACK集群上默认为 /var/lib/kubelet/seccomp ) 下配置文件的相对路径
详细的配置说明参见官方文档
使用RuntimeDefault特性:
只适用于1.22集群,当前还是kubelet alpha特性,在生产环境需谨慎使用,开启的方式为:
-
在节点kubelet配置中添加feature-gates参数,指定 SeccompDefault=true enable特性;
-
在节点kubelet配置中添加--seccomp-default启动参数开启RuntimeDefault特性;
-
重启kubelet服务
在ACK集群节点上的配置修改如下:
在Kubelet参数配置重启完成后,我们通过kubectl启动一个正常的pod,并且通过amicontainerd工具查看默认的seccomp filter是否生效,我们可以看到pod中已经默认禁用了60+的syscall,其中包含了unshare、 uselib、mount这些经常被恶意攻击者青睐的高危syscall:
kubectl run -it bash --image=bash --restart=Never bash
apk add curl
curl -LO k8s.work/amicontained
chmod +x amicontained
./amicontained
在一个没有开启RuntimeDefault特性的节点上,我们部署一个pod作为对比,可以看到在默认状态下,pod中只禁用了20个syscall,和使用runtime default seccomp过滤保护的pod相比无疑暴露了更大的攻击面:
实践Seccomp的挑战:
如何自定义seccomp profile?这应该是阻碍大家使用seccomp最直接也是最普遍的一个问题。确实,无论是资深的K8s玩家还是一个专业的安全运维人员,似乎都很难完成seccomp策略文件的编写。虽然看上去seccomp策略文件就是一堆系统调用的黑白名单配置,但是在生产环境中,我们会遇到各种各样的问题,比如:
-
如果我们尝试使用的是SCMP_ACT_LOG审计模式记录应用中的系统调用,很有可能因为流控等原因丢失很多审计日志,导致配置不全;
-
虽然有时候应用pod能够在一个seccomp配置下正常启动,我们却不能断言这个配置一定是生产可用的,因为首先要保证我们测试了百分之百的应用场景,任何一个边界case都可能遗漏导致配置策略过紧;另外有时候虽然程序看起来运行正常,但是背后其实有些业务逻辑是处在被拦截的状态,此时如果我们没有通过日志等细节发现问题,很可能就遗漏了部分配置;
-
应用可能依赖了很多包,尤其层层嵌套的依赖关系让开发者很难掌握全部syscall清单,需要一个很长的监控和整理过程;
-
需要引入额外的安全配置维护成本,随着应用版本不断迭代,要求我们预置的seccomp配置也随之更新。
以上的困难看起来都要求seccomp的编写定义者一定要是对业务逻辑非常熟悉的应用开发人员自身,如果是专门的安全运维人员去完成编写配置,很可能因为对业务的不熟悉导致配置遗漏从而引发线上生产问题,这也给企业内部安全运维流程中去真正实施seccomp带来了额外的挑战。
其实这也是诸多通用安全特性在落地过程中面临的通用问题,如何平衡应用安全和生产环境的稳定性是经常困扰企业安全运维的核心问题。对于云服务商来说,在为企业提供应用侧安全能力的同时,也需要兼顾稳定性要素,如何在提升应用侧安全水位的同时,能够降低安全特性的使用门槛,以尽可能自动化的治理方式帮助企业安全运维部署实施安全特性,同时尽可能保证生产稳定和兼容性,也是对云服务商自身的巨大挑战。
就Seccomp配置这个问题本身,如何能够以自适应的方式适配应用行为,并且自动化的帮助企业客户生成和配置seccomp profile是整个问题的关键,下面这个部分我们来具体看下一些可行的选择。
自定义Seccomp profile:
参考社区seccomp特性作者Paulo Gomes的文章,这里列举一些可选方案:
通过内核tracing或BPF等手段:
对于逻辑比较简单明确的应用pod,我们可以使用如strace这样的tracing工具,简单有效的就能list逻辑用到的syscall列表,该方式对指定的逻辑命令下的系统调用收集比较方便,对于复杂一些的应用场景就不太适用了;另外在host模式下的测试可能会引入额外的高危syscall(setuid、capset等),还有就是性能开销的问题;由于以上原因该方式适用于应用程序的开发构建阶段,借助tracing工具初步收集syscall列表:
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ------------------
55.66 0.222638 147 1514 219 futex
17.77 0.071094 179 396 nanosleep
10.99 0.043972 154 284 epoll_pwait
5.46 0.021844 3640 6 wait4
。。。。
0.02 0.000093 46 2 sysinfo
0.02 0.000090 22 4 readlinkat
0.02 0.000067 6 11 set_robust_list
0.01 0.000056 14 4 1 connect
0.01 0.000047 2 20 brk
0.01 0.000044 11 4 madvise
0.01 0.000035 5 7 socket
0.01 0.000032 2 15 13 access
0.01 0.000025 3 7 uname
0.01 0.000022 3 6 getsockname
0.01 0.000021 7 3 epoll_create1
0.00 0.000014 7 2 getpeername
0.00 0.000013 0 18 dup2
0.00 0.000011 3 3 sched_getaffinity
0.00 0.000008 2 3 prlimit64
0.00 0.000008 1 6 getrandom
0.00 0.000007 2 3 bind
0.00 0.000006 2 3 set_tid_address
0.00 0.000003 3 1 getuid
0.00 0.000000 0 1 writev
0.00 0.000000 0 2 sendmsg
0.00 0.000000 0 12 3 recvmsg
0.00 0.000000 0 2 setsockopt
0.00 0.000000 0 7 1 getsockopt
0.00 0.000000 0 2 geteuid
0.00 0.000000 0 1 capget
0.00 0.000000 0 3 ppoll
------ ----------- ----------- --------- --------- ------------------
100.00 0.399984 67 5954 425 total
除了系统tracing工具,一个有意思的项目是oci-seccomp-bpf-hook,首先它是一个基于oci规范的prestart hook,可以在容器启动前通过hook的方式注册一个raw_syscalls:sys_enter类型的eBPF tracepoint,用来记录和目标应用容器共享同一个PID namespace的系统调用,并且自动生成seccomp配置文件,对于批量启动的测试场景这种自动化dump seccomp的文件是比较理想的,另外也比较利于集成企业CI/CD流程,让开发人员免于在应用逻辑更新的同时还需要兼顾seccomp策略修改的苦恼。
但是该方式也有一定的局限性,首先ACK集群默认的containerd运行时还不支持oci规范的hooks,参考#6645,另外就是这种最小化收敛的方式可能由于采集方式的原因遗漏配置,同时该hook依赖CAP_SYS_ADMIN特性,同样会破坏生产节点的安全模型。
另一个选择是zaz,是Paulo Gomes本人开发的小工具。该工具支持在指定的docker镜像中收集指定命令的seccomp配置,该模式下采集会首先去掉所有可能的syscall限制,然后遍历所有syscall查找可能阻碍程序指令成功运行的syscall,以这种“暴力破解”的方式尝试生成最终的配置文件如下:
对于GO语言编译的程序,工具还支持通过分析objdump文件的方式解析最终的seccomp syscall列表,应用对应的objdump可以通过如下指令完成,该流程已经自动集成在zaz指令中:
go tool objdump goapp > goapp.dump
审计模式:
不要忘记seccomp策略自身就支持审计模式,通过将seccomp的defaultAction配置为SCMP_ACT_LOG ,在该模式下seccomp过滤器会放行syscall,仅记录syscall的执行,在生产环境可以allow和deny规则配合使用,完成相对安全的生产策略。
通过使用上面介绍的tracing工具我们已经可以初步获取go binary和应用镜像中的seccomp配置,在此基础上我们可以结合seccomp自身的审计过滤模式,尽可能多的覆盖测试面,通过对syscall的审计日志分析进一步补充seccomp的配置输出。为了避免日志限流可能导致的syscall丢失,这里首先要放开流控配置:
# 通过下面命令查看log流控配置:
$ sudo sysctl -a | grep net.core.message
net.core.message_burst = 10
net.core.message_cost = 5
将burst和cost设置为0
sudo sysctl -w net.core.message_burst=0
sudo sysctl -w net.core.message_cost=0
安装syslog并启用系统auditd服务用于系统日志收集:
kubectl apply -k github.com/chrisns/syslog-auditd
kubectl --namespace kube-system wait --for condition=ready pods -l name=syslog
service auditd start
#相关配置在/etc/rsyslog.conf和/etc/audit目录下
启动应用后可以让测试团队运行尽可能多的用例,此时我们可以在节点系统日志上看到相关的syscall审计,通过zaz等audit->syscall->seccomp开源工具可以方便的将审计转换为对应的seccomp配置,通过一段时间的用例覆盖,可以帮助我们尽可能多的添加一些应用边界条件可能触发的系统调用。
最后,别忘了将之前收集阶段修改的系统参数复位:
sudo sysctl -w net.core.message_burst=10
sudo sysctl -w net.core.message_cost=5
注:测试在alilinux2节点上SCMP_ACT_LOG会有问题,虽然内核版本已大于4.14,alilinux3节点可以正常工作
使用security-profiles-operator:
security-profiles-operator是K8s sigs社区的开源项目,可以帮助用户通过k8s operator的原生方式自定义配置和部署seccomp配置,同时支持多种方式自动化适配采集应用当前运行状态下的seccomp配置。下图是SPO的产品能力图,可以看到不光是seccomp,SPO还涵盖了对AppArmor,Selinux,PSP和RBAC的等多种Linux和K8s安全策略语言的支持。
SPO的安装和使用可参考官方文档,安装命令如下:
#依赖cert-manager自签发证书
kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.8.0/cert-manager.yaml
kubectl --namespace cert-manager wait --for condition=ready pod -l app.kubernetes.io/instance=cert-manager
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/security-profiles-operator/main/deploy/operator.yaml
安装成功后除了会在集群中部署spo对应的operator外,还会在集群节点上部署对应的ds agent,用于在节点上同步下发配置seccomp并且根据系统日志或bpf能力收集记录(recording)应用侧的syscall行为;另外还会在集群中部署一个mutating webhook用于根据用户定义的ProfileBindings自动绑定应用对应的seccomp配置。
使用上,首先我们需要通过创建SeccompProfile的CR实例定义应用所需的seccomp配置,比如审计模式下的基础模板:
apiVersion: security-profiles-operator.x-k8s.io/v1beta1
kind: SeccompProfile
metadata:
namespace: my-namespace
name: profile1
spec:
defaultAction: SCMP_ACT_LOG
对于SeccompProfile的配置,SPO支持继承机制,即可以在sp的配置中指定一个base模板,这样我们就可以首先根据运行时启动运行容器所需要的syscall最小集配置一个,比如runc的一个sp示例,在sp基线的基础上,用户可以根据应用需求扩展策略,比如如下模板,在spec的baseProfileName字段中指定了sp基线的名称:
apiVersion: security-profiles-operator.x-k8s.io/v1beta1
kind: SeccompProfile
metadata:
namespace: my-namespace
name: profile1
spec:
defaultAction: SCMP_ACT_ERRNO
baseProfileName: runc-v1.0.0
syscalls:
- action: SCMP_ACT_ALLOW
names:
- exit_group
另外我们需要知道系统中常见的高危和需要加白的syscall,项目中也有对应的示例,sample配置如下,其中包含了部分攻击者经常利用的高危syscall:
architectures:
- SCMP_ARCH_X86_64
syscalls:
- action: SCMP_ACT_ALLOW
names:
- exit
- exit_group
- futex
- nanosleep
- action: SCMP_ACT_ERRNO
names:
- acct
- add_key
- bpf
- clock_adjtime
- clock_settime
- create_module
- delete_module
- finit_module
- get_kernel_syms
- get_mempolicy
- init_module
- ioperm
- iopl
- kcmp
- kexec_file_load
- kexec_load
- keyctl
- lookup_dcookie
- mbind
- mount
- move_pages
- name_to_handle_at
- nfsservctl
- open_by_handle_at
- perf_event_open
- personality
- pivot_root
- process_vm_readv
- process_vm_writev
- ptrace
- query_module
- quotactl
- reboot
- request_key
- set_mempolicy
- setns
- settimeofday
- stime
- swapoff
- swapon
- _sysctl
- sysfs
- umount2
- umount
- unshare
- uselib
- userfaultfd
- ustat
- vm86old
- vm86
部署成功的sp示例会由spod agent同步分发到每个节点的/var/lib/kubelet/seccomp/operator/<namespace>目录下以json文件形式保存,之后我们可以使用ProfileBinding将sp实例中定义的seccomp配置绑定给一个具体的应用,一个ProfileBinding示例如下,该绑定会将以nginx:1.19.1镜像启动的应用容器自动绑定到profile-complain sp实例中定义的seccomp配置
apiVersion: security-profiles-operator.x-k8s.io/v1alpha1
kind: ProfileBinding
metadata:
name: nginx-binding
spec:
profileRef:
kind: SeccompProfile
name: profile-complain
image: nginx:1.19.1
以nginx:1.19.1镜像启动测试pod,可以看到容器的securityContext配置中已经自动添加了profile-complain.json的配置:
最后我们再来看下ProfileRecording这个CRD,通过它可以根据指定label的应用Pod内的行为自动记录并生成seccomp配置文件,一个ProfileRecording示例如下:
apiVersion: security-profiles-operator.x-k8s.io/v1alpha1
kind: ProfileRecording
metadata:
name: my-recording
spec:
kind: SeccompProfile
recorder: bpf
podSelector:
matchLabels:
app: my-app
这里的recorder字段可以使用不同的采集类型,包括hook,log和bpf三种类型,其中hook类型基于上文介绍的oci-seccomp-bpf-hook,节点运行时需要符合oci规范,containerd暂时无法支持;log模式需要节点启用auditd和syslog服务,同时需要agent开启特权模式;bpf模式基于eBPF,依赖节点内核暴露的/sys/kernel/btf/vmlinux文件,可以通过如下开关开启特性:
> kubectl -n security-profiles-operator patch spod spod --type=merge -p '{"spec":{"enableBpfRecorder":true}}'
securityprofilesoperatordaemon.security-profiles-operator.x-k8s.io/spod patched
开启后如果看到如下agent日志说明关于节点内核的前置校验成功,可以开始测试:
在部署了上面提到的bpf类型的ProfileRecording实例后,我们来部署一个测试应用,看下是否能够成功dump下它的syscall行为,应用启动后,agent就会开始采集工作,同时可以看到在应用的ns下一个对应的sp实例被创建出来,包含了测试应用必需的syscall白名单:
由于采集依赖开启一些特权配置,建议只在测试场景下使用记录模式,通过自动采集的方式可以基于应用迭代很好的补充sp基线的配置内容。
Seccomp最佳实践:
虽然通过上面介绍的自定义seccomp profile方法我们已经可以使用一些相对自动化的方式帮助我们自动发现并部署应用侧seccomp配置,但这并不意味着我们已经掌握了生产可用的安全配置。请记住seccomp profile的构建不是一蹴而就的单次任务,它是一个长期迭代的精细化工作。需要在配置测试的过程中考虑多种方法的配合使用,除了依赖审计模式的补充外,采用SCMP_ACT_ERRNO模式在测试流程中直接返回错误也是一个直观有效的补充方法;由于拦截的目标是系统调用,对应用逻辑再熟悉的开发人员也有可能遗漏某些底层的syscall调用,更何况随着应用逻辑和内核版本的不断迭代,都会造成当前seccomp配置的失效。
这里基于Paulo Gomes的7 things you should know before you even start!给出几点建议,算是最佳实践吧:
-
给容器的security context配置 allowPrivilegeEscalation=false
默认情况下这个配置是true,该配置置为false会禁止容器创建新的子进程获取更高的特权权限,由于运行时中关于seccomp profile的配置非常靠前,这就需要针对该配置增加很多像setuid,capset等不必要的高危syscall,同时运行时的修改也会影响seccomp的配置
-
以容器维度细粒度配置seccomp
权限最小化原则,pod维度的配置会带来更大的攻击面,同时有已知的社区问题可能会影响较早版本的集群
-
使用runtime/default作为配置兜底
有时候我们无法做到应用的精细化secomp配置,此时可以使用K8s原生的runtime/default配置兜底
-
不要配置seccomp=unconfined
虽然这是K8s的默认选择,但是seccomp能够给应用到节点内核间增加新的隔离屏障,从安全角度触发强烈建议开启
-
使用审计模式细化配置
在编写seccomp配置时,首先可以将我们清楚的syscall黑白名单明确下来,一些容器中常用syscall的说明可以参考docker文档,在此基础上善用SCMP_ACT_LOG审计方式收集应用运行过程中的异常审计进一步细化配置(可能遇到内核不支持的问题。。)
-
优先使用白名单配置,这是更安全的做法
-
保证必要的权限, exit 、 close 、 exit_group 、 futex 等都是一些基本且必要的基础syscall;在配置上生产前要保证足够的迭代测试,strace等系统tracing工具可以很好的帮助我们发现问题,另外可以将zaz、SPO ProfileRecording这样的自动化记录并dump应用seccomp配置的工具集成到企业应用开发的CI/CD流程中,将非常有助于seccomp配置的更新迭代
-
在生产环境中使用seccomp,在带来安全的同时也要注意其引入的潜在风险
-
有时候无法保证seccomp的白名单已经覆盖了应用逻辑所有的边界条件,这对上线前的端到端测试也是很大的挑战
-
除了应用的迭代,我们在应用中调用的底层库函数也是不断迭代的,比如glibc 2.26中open函数调用的syscall从open变为openat,而这些修改是应用开发人员很难同步发现的, 这里 能看到更多的关于seccomp的吐槽
因此在配置seccomp引入对系统调用的安全屏障的同时,也请在收敛力度上兼顾稳定性,保持一定的平衡,毕竟为了追求绝对的安全而影响了生产稳定是得不偿失的事情。
Reference:
https://spinscale.de/posts/2020-10-27-seccomp-making-applications-more-secure.html
https://lwn.net/Articles/656307/
https://lwn.net/Articles/738694/
https://www.appvia.io/blog/how-to-use-the-security-profiles-operator
https://blog.aquasec.com/aqua-3.2-preventing-container-breakouts-with-dynamic-system-call-profiling
https://man7.org/conf/elce2018/seccomp-ELCE-2018-Kerrisk.pdf