善攻者,动于九天之上。你的容器安全吗?

k8s技术圈

共 5241字,需浏览 11分钟

 · 2021-11-16

大家好,我是二哥。从今天开始的几篇(我也不知道会有几篇),我会集中聊聊与容器安全相关的内容。我粗略估计会涉及到user account、capability、container image、container isolation的加固、container isolation的破防、container network security(涉及到网路安全,不是网络方案)、container runtime security等方面的内容。
这是一个大的领域,我慢慢写,慢慢聊,你慢慢看。
说到容器安全,有些同学会想起来CIS Benchmark(Center for Internet Security 基线), 它提供的指导手册包含详细的检测和评估方法,已经被广泛接受为保障容器和虚拟机安全的最佳实践。但这些只是操作手册,它只是告诉我们要做些什么,我们只有近距离的查看系统底层的设计细节,才会真切地感到害怕,也就理解了Benchmark为什么这么建议。另外不会有一个手册能包含所有规避风险的建议,只有知道了这些本质上的东西,才能让我们时刻保有警惕的心。本立而道生,欲求正道,当先务本。
这篇我们先来看看容器为什么不安全。本文涉及到不少容器的基础概念,建议复习阅读我的上一篇文章“画地为牢,细谈VM和容器”。
估计有不少朋友会疑惑:容器不是被禁锢在namespace里面吗?不是说一个容器不能触碰到namespace之外的数据吗?不是说容器看到的文件系统以及配置都是自己独有的吗?
是的,以上的问题的答案都是YES。但事实情况远比这个来得残酷。通常我们只会把焦点集中在容器自身,但如果我们把视角从它身上挪开,将镜头拉长,从更广的角度来看的话,会发现处处存在风险。

图 1:容器和它周边环境风险示意图
我们来看看图1。它分为两部分,上面的部分是面向云原生应用的部署环境,下面是开发环境。在这里,容器是运行在虚拟机之上的,而虚拟机是由基于HyperVisiors的VMM管理,图中用箭头标出了与容器相关的各种可能产生安全事件的地方。下面的部分是一个面向云原生环境开发应用的典型流程,我在图中标注了“安全薄弱点环生”字样,具体内容详见后文介绍。

1. 攻击面

《孙子兵法》说:善攻者,动于九天之上。意思是说攻击像是从天上来的,很突然,降维打击,对手难以招架。我们结合图1展开来聊聊,看看坏人是多么的狡猾,简直是无孔不入。限于篇幅,本文只列举风险(risk),不给出解决方案,实际上只要遵循业界的最佳实践能很大程度上避开此类风险。坏人固然狡猾,但平时工程中有意无意抄近路的做法也为坏人提供了可乘之机。
risk和threat是两个不同的概念,前者是风险,后者是与安全相关的威胁。当风险成真即为威胁,而攻击者可以通过各种手段将风险变成事实。
下文中docker container指docker image的运行实例,image是静态的,而docker container是动态的,它是一个进程。
不安全的应用和库无论是宿主机OS,container runtime,我们开发的应用还是应用所依赖的第三方库,都是工程师写的,它们或许自身就包含了如栈溢出(如CVE2012-0158)、buffer长度未检查(如标志性的OpenSSL HeartBleed漏洞)之类的漏洞,而Linux内核著名的脏牛漏洞CVE-2016-5195,可以让低权限用户在全版本Linux系统上实现本地提权。想想Linux kernel有2000万行之巨,类似的漏洞估计还有不少,连Linus本人也曾说:Perfect Security in Linux is Impossible。
错误的容器镜像配置。当代码写好后,我们会将其编译成docker image。在这一步各种不小心都会引入问题,比如Dockerfile内没有通过USER指令指定container应以何种账号运行,那么大概率Docker runtime会默认以root身份运行你的容器。再比如在Dockerfile里面用如下所示的方式不小心将密码封存在了docker image里面,虽然在docker container里面无法直接看到password.txt,但是当通过docker save xxx > xxx.tar,再解压tar包后,就可以非常容易拿到这个密码。
FROM alpineRUN echo "lance-top-secret" > /password.txtRUN rm /password.txt
不安全的rootfs。我们都知道Dockerfile需要以FROM开始(ARG不算在build流程之中)。FROM意味着我们会在目标镜像里引入rootfs。rootfs中包含了我们的应用正常运行所必须的库和基本的工具集。这些库和工具本身自然会出现漏洞,也就为容器逃逸提供了可能。比如在下面的示例中,即使运行docker时有意地去掉所有的capability,且将user设置成了UID 1000,但因为这个rootfs中的bash有setuid,使得最终euid成功地变成了UID为0的root,后果你懂的。
# docker run --rm -ti --name setuid_test --cap-drop=all --user 1000:1000 cajga/evil_nginx_image bash# I have no name!@c2836c61dd38:/$ ls -l /bash-rwsrwxrwx. 1 root root 1099016 Dec  7 20:10 /bash# I have no name!@c2836c61dd38:/$ iduid=1000 gid=1000 groups=1000# I have no name!@c2836c61dd38:/$ /bash -p# bash-4.4# iduid=1000 gid=1000 euid=0(root) groups=1000
此处强行插入一条广告:接下来的一篇,二哥会给大家介绍Linux capability,file permission,user namespace,uid/gid,process 五者之间错综复杂的关系。因为复杂,所以容易出错,也就更容易因为使用不当而打开了漏洞的大门。
在容器里安装应用在前几年确实流行过当容器起来后,通过npm, apt等方式动态更新应用的方式来达到升级版本的目的。这样做可以避免重新build。不过这已经被公认为是所谓的anti-pattern。现在的最佳实践都劝大家把镜像做成immutable的,也就是镜像运行时,rootfs应该都是只读的,不应该发生安装应用到/bin目录之类的写操作,当然写数据到临时目录除外。
这样做的后果很容易想到。一是版本不可控,进而带来无法预知的行为,并且增加了troubleshooting的难度,二来动态升级的版本没有经过镜像安全扫描,可能含有新的漏洞。
但这都只是建议,如果不通过类似drift prevention这样的强制措施,谁也阻挡不了运行时升级这样的骚操作。二哥丰富的工(踩)程(坑)经验反复提醒我:但凡技术或实现上有操作的空间,它总会在某种借口下被使用,进而被滥用,直到出问题的那天,最后一个倒霉蛋会红着眼睛,挠着发乱的头发,发疯地吼道:当初是哪个混蛋想起来这样干的?不知道你怎么想,这种时候,我可不希望自己的名字出现在git log里面。
错误地拉取了不正确的镜像。比如从未知的Image registry拉取镜像,从docker hub拉取未标记"Official Image"的镜像,拉取镜像时未指定精确的版本。你懂我的意思,尤其是当你喜欢在FROM指令后面直接使用tag为latest的方式来pull base image的时候。
不安全的网络。容器网络和宿主机网络是两个独立的网络方案,但同时容器网络又是对宿主机环境开放的。Kubernetes要求Pod之间可以相互自由地访问,但如果不加约束,就意味着如果一个Pod被恶意利用了,攻击者可以随意在K8s环境横向移动。零信任(Zero-trust)理念告诉我们:neither external networks (the internet and SaaS services) nor the internal network (enterprise network & WAN) should ever be trusted!(无论外部的网络或是内部的网络都不能被信任)。
容器逃逸。容器逃逸是指攻击者通过漏洞,劫持容器化内应用,获得了在容器内执行某种权限下命令的能力,进而利用这种命令执行能力,借助一些手段进一步获得该容器所在的宿主机上的某种权限下的命令执行能力。
containerd、CRI-O是广泛使用的container runtime。理论上它们应该会被很好地进行安全方面的加固,但实际上不断有漏洞被研究人员发现,这些漏洞使得运行于容器内部的恶意代码可以触碰到容器外面的环境。
如2019年发现的漏洞CVE-2019-5736,它导致18.09.2版本之前的Docker允许恶意容器覆盖宿主机上的runC二进制文件,由此使攻击者能够以root身份在宿主机上执行任意命令。恶意容器需满足以下两个条件之一: (1)由一个攻击者控制的恶意镜像创建、(2)攻击者具有某已存在容器的写权限,且可通过docker exec进入。哦,我们又提到了一次镜像。
镜像编译环境所引入的风险。如图2所示。这是一个非常典型的RD日常工作流程。写好的代码提交到Git,然后提交CI build,build好的image会push到某一个Image Registry,供CD使用。规模稍大的公司会有单独的部门负责Git、CI和Image Registry服务的运维。这就意味着当代码离开了我们的工作机,就进入了一个会有若干种风险的环境。图中画出了各种各样的有风险的地方,如所依赖的第三方库或base image包含漏洞、build machine上安装的编译环境有漏洞等等。可以用“防不胜防”来形容。
这可不是耸人听闻。还记得2020年12月报道的那起著名的SolarWinds攻击事件吗?此次网络攻击事件最晚开始于2020年3月,它导致了美国联邦政府数据泄露,还给成千上万的SolarWinds客户带来了巨大的麻烦。攻击者破坏了 CI/CD 管道的“心脏”,即管道中对代码进行测试、包装、装箱和签名的部位,然后成功地更改了 SolarWinds 的源代码。攻击者还部署了恶意软件,即如今臭名昭著的“SunSpot”,它以高权限运行,扫描 Orion 构建。

图 2:编译镜像时涉及到的攻击风险
其它。此处省略1万字。容我调皮一下。

2. 从安全边界到零信任

基于容器的特征,我们很自然地会将其作为安全边界(security boundary),安全边界又被叫做信任边界(trust boundary)。namespace、rootfs和容器网络构成了边界的围墙。“边界”的潜台词就是说以它为界,有inside和outside之分。回到文首朋友们的疑惑:我都让应用跑在容器里了,为什么还不安全呢?
这样的疑惑部分来源于传统的边界意识。我们工作所涉及到传统IT安全环境被戏称为“城-池安全防护模式”。城里的人可以自由行动,而进城得经过护城河。

图 3:城-池安全防护模式示意图
如今云计算的浪潮已经彻底改变了它当初的模样。Blissfully在他们“2020 Annual SaaS Trends”中评估到:一个中等大小的企业平均会用到137个SaaS应用,而一个大型企业会用到288个。
这样的巨大改变使得划个边界来设防的IT环境被撕开了许多的口子,慢慢被切割成了若干个分散的子环境。使用者(员工、合作伙伴、顾问、供应商);他们所使用的设备(手机、笔记本、台式机);他们所访问的服务(SaaS 应用、内部应用、云设施、自行搭建的服务器)交织在一起进一步使得这样的设防作用削弱不少。
多处设防是兵家大忌。《孙子兵法》有言:故备前则后寡,备后则前寡,备左则右寡,备右则左寡,无所不备,则无所不寡。
一句话,谁都不可信。忘掉边界,以身份标识(认证)和访问权限(授权)为支点和起点,让我们拥抱零信任吧。
以上就是本文的全部内容。码字不易,喜欢本文的话请帮忙转发或点击"在看"。您的举手之劳是对二哥莫大的鼓励。谢谢!
浏览 31
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报