云计算时代容器安全综述-容器的隔离性安全风险(下)

笔者在前边系列文章中详细的阐述过在Docker上创建容器实例的过程,当我们在命令行运行docker run的时候,背后的原理是客户端将创建容器实例的命令发送给docker deamon,deamon负责执行必要的系统调用(比如创建新的命名空间等)来具体的创建容器进程。执行创建命名空间这样的操作需要root权限,因此我们一般会简单的表述为创建容器实例需要root权限。

但是前边的表述不是太准确,特别是在安装了Docker的机器上,用户并不是必须持有root权限才能创建容器实例,本质上用户只要加入docker组(docker group)后就有权限给Docker socket发送命令,比如发送docker run命令给socket来驱动docker deamon来创建容器实例。笔者这里需要特别强调的是将用户加入到docker group等同于root权限,因为如果用户有权限启动容器实例,就意味着用户启动了以root权限运行的容器实例,并且这个容器实例可以在启动的时候通过参数:docker run -v /:/host <镜像ID> 来挂载操作系统的根目录,通过这点相信大家能体会到“等同”的含义。

容器实例默认情况下以root权限运行会造成重大的安全风险,因此最近几年Rootless容器模式开始出现。具体来说Rootless容器模式就是让非root用户能够在操作系统上创建容器实例。从技术原理的角度看,Rootless容器模式依赖于user命名空间机制来实现,非root权限的用户ID会被映射到容器中的root账号,特别是当出现容易逃逸攻击的时候,容器中的账号逃逸出来后,在宿主机上就是普通账号,因此能够造成的危害也非常有限,因此从这一点看,Rootless容器模式对安全有巨大的提升。

咱们在上篇文章中展示过如何通过把非root账户加入到docker组中来启动容器实例,也特别强调过如果运行时是podman,并且做过docker和podman的别名,那么非root账户无法启动容器实例,背后的原因是podman的具体实现机制叫deamonless。从deamonless这个单词读者也应该能够猜到podman没有Docker机制中的Deamon进程,由于创建容器实例需要创建命名空间,因此非root账户无法直接在podman运行时上启动容器实例。

虽说Rootless这种机制听起来很“安全”,但是它也不是银弹,并不是所有的容器镜像都可以成功在rootless容器模式下启动并运行起来,原因是操作系统权限(capabilities)的实现机制和命名空间机制有点微妙。如果读者查看Linux操作系统的文档,会发现user命名空间不仅仅隔离了用户(user)和组(group),还包括内核的权限(capabilities)。大白话说就是我们可以为运行在某个特性user命名空间的进程增加或者删除某项操作系统内核访问的能力,这个能力只限于这个命名空间。对于rootless容器实例来说,当我们给rootless容器实例增加了某项内核访问权限,这个权限只局限于容器进程内,并不等同于容器实例访问宿主机上的内核资源。

笔者承认前边的这句话并没有把rootless和user命名空间之间微妙的关系说清楚,咱们还是继续举个例子说明。CAP_NET_BIND_SERVICE这个能力在Linux操作系统内核中允许进程可以使用低于1024的端口号。假设我们在Docker环境中启动一个容器实例(默认情况下容器实例以root权限运行,因此持有CAP_NET_BIND_SERVICE内核权限),并且和宿主机共享网络命名空间,那么容器实例中运行的应用程序就可以使用1-65536所有的端口号。反过来如果我们运行rootless容器,显示的赋予CAP_NET_BIND_SERVICE权限并和宿主机共享host命名空间,那么运行在容器中的相同springboot应用是无法使用低于1024的端口号,这就证实了我们前边的逻辑,内核能力只限于容器内部,无法透出到宿主机。

虽说内核能力和user命名空间这种细微的设计看似罪恶之源,但是从安全的角色,这是健壮的设计,我们不用担心通过user命名空间隔离的进程修改宿主机操作系统内核的配置,造成系统运行或者重启等故障。笔者对rootless这种模式的使用经验显示,大部分容器镜像都可以成功运行,读者的场景如果对安全要求非常高,建议考虑rootless这种模式。

rootless容器实例从宿主机的角度看,userid为普通用户,虽然从容器内部看的确以root账户运行,这种不同角度看到不同的用户账户权限会造成容器内部访问文件系统数据的问题,因此需要操作系统的文件系统类型支持文件ownership和group ownership的remap,这部分内容要延展开会非常复杂,感兴趣的读取可以自行参考相关材料。

不过rootless容器模式相比于Docker容器,略显青涩,还处于非常初级的发展阶段,目前支持比较完整的运行时包括runc和podman,docker虽说也支持,但是还处于试验阶段。从Kubernetes的角度看,目前尚未支持rootless容器这种模式,相信社区已经在努力朝着这个方向发展,感兴趣的读者可以参考Akihiro Suda等人开发的一个POC的例子,读者可以自行寻找。

安全是个全局的话题,因此运行时的问题并不是全部,笔者的经验显示大部分安全问题都和配置有关,容器以root权限运行叠加配置的风险,这是容器安全领域风险的主要来源。因此在实际的项目中,大家需要仔细分析自己的业务场景,结合本文提到的user id重写以及rootless容器模式,来规避容器实例以root权限运行的问题。

说到容器实例以root权限运行,Docker和很多其他的容器运行时都提供了容器实例启动的时候指定--privileged参数,如果你不知道这个参数,请忽略后续的信息,因为这个--privileged参数被称作是“计算机历史上最臭名昭著的参数”。这么叫的原因有两个,其一是很强大,其二是很容易被滥用造成安全风险。

由于安全总是被误解为安全专家的职责范围,因此很多时候运维人员和系统开发人员对安全的意识并不强,导致很多人对--privileged这个参数的理解是:容器实例以root权限运行,相信大家应该清楚默认情况下Docker平台下启动的容器实例都是以root权限运行这个事实,因此读者读到这里的疑问是:这个--privileged参数到底有啥用?

在继续讨论之前,大家需要了解一个技术细节,虽说默认情况下容器实例以root账号运行,但是操作系统内核的设计人员关于虚拟化这套机制也考虑了安全因素,具体来说就是容器实例运行的这个root权限也只持有部分操作系统的内核调用能力(capabilities),咱们接下来实际的例子来看看,默认情况下容器实例持有的操作系统内核权限以及携带--privileged的场景下权限清单。

笔者在自己的环境中分别运行不带--privileged和带--privileged的场景,通过capsh输出(这个工具只有在Linux操作系统有)权限能力清单,详细信息如下:

➜  source docker run --rm -it alpine sh -c 'apk add -U libcap; capsh --print | grep Current'

...

Current: = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,

cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,

cap_audit_write,cap_setfcap+eip

➜  source docker run --rm -it --privileged alpine sh -c 'apk add -U libcap; capsh --print | grep Current'

...

Current: = cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,

cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,

cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,

cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,

cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,

cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,

cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,

cap_wake_alarm,cap_block_suspend,cap_audit_read+eip

从输出的清单大家应该很明显就能看出,携带--privileged的容器进程会持有几乎全部操作系统内核的访问权限,即便是这里边大部分容器进程都用不着。读者可能会问,既然这个--privileged参数如此具备杀伤力,为啥要引入这个参数呢?

具体来说--privileged用来实现Docker in Docker模式,自己搭建过流水线CICD的同学应该理解DID这种模式,Jenkins运行在容器中,但是我们需要在容器中来访问deamon构建容器镜像,虽然说这种模式很普遍,读者还是建议大家谨慎对待--privileged参数。如果读者的业务场景的确需要使用--privileged参数,建议做好日志记录和安全审计,并且对需要使用--privileged参数的场景要多次推敲讨论,降级这个参数造成的安全风险。

另一种思路就是给应用所需的权限,虽然说这个看起来很自然,但是由于大部分应用会依赖很多三方包,系统包,因此应用程序在静态的时候,很难完整确认程序运行所需要的所有内核权限。因此我们需要借助于Tracee这样的工具来追踪记录应用程序需要的内核权限。咱们还是通过下边的例子来说明这个工具:

在自己的Linux操作系统上首先安装这个工具(如果没有的话),接着打开两个终端,在终端1运行 docker run -it --rm nginx。

$ docker run -it --rm nginx

然后在终端2上运行tracee工具,并设置对应的参数,如下:

$ ./tracee.py -c -e cap_capable

TIME(s) UTS_NAME UID EVENT COMM PID PPID RET ARGS

125.000 c8520fe719e5 0 cap_capable nginx 6 1 0 CAP_SETGID

125.000 c8520fe719e5 0 cap_capable nginx 6 1 0 CAP_SETGID

125.000 c8520fe719e5 0 cap_capable nginx 6 1 0 CAP_SETUID

124.964 c8520fe719e5 0 cap_capable nginx 1 3500 0 CAP_SYS_ADMIN

124.964 c8520fe719e5 0 cap_capable nginx 1 3500 0 CAP_SYS_ADMIN

通过这种方式我们就能准确的知道应用程序需要的内核访问权限,然后就可以通过命令行$ docker run --cap-drop=all --cap-add=<cap1> --cap-add=<cap2> <image> ...来指定合适的权限集合。大家需要注意的是我们首先要cap-drop=all,然后再设定应用程序需要的操作系统权限。

容器默认情况下以root权限运行只是安全风险中的一环,我们在启动容器实例的使用,也可以使用-v选项来将宿主机的目录挂载到容器中,用来实现宿主机和容器之间进行数据文件的共享。虽然说这种方式很方便就可是实现数据共享,但是也存在巨大的安全隐患,并没有人阻止你把宿主机的根目录挂载到容器中,比如下边的例子。

笔者在本地启动了一个容器实例,并把宿主机(macOS的虚拟机)的根目录挂载到容器中,大家从输出的结果可以看到,从容器实例中可以看到宿主机的系统目录:

➜  source docker run -it -v /:/hostroot ubuntu bash

root@59c908045783:/# ls

bin  boot  dev  etc  home  hostroot  lib  lib32  lib64  libx32  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

root@59c908045783:/# cd hostroot

root@59c908045783:/hostroot# ls

A            C  F  I  L        N  Q  System  Users    W  Z  bin  cores  e    g    host_mnt  k    lib64  mnt  opt      proc  root  sbin  t    usr  w  z

Applications  D  G  J  Library  O  R  T      V        X  a  boot  d      etc  h    i        l    m      n    p        q    run  srv  tmp  v    x

B            E  H  K  M        P  S  U      Volumes  Y  b  c    dev    f    home  j        lib  media  o    private  r    s    sys  u    var  y

root@59c908045783:/hostroot#

从输出的信息可以看到,如果恶意攻击者攻破了容器实例,那么整个宿主机就直接暴露给黑客,他的确可以干任何想干的事情。虽然说在容器挂载宿主机的根目录属于这种场景比较牵强,但是这种的场景的变体我们日常的系统开发和运维中可能会遇到,包括:

- 在容器中挂载/etc会允许恶意攻击修改系统的用户信息,特别是/etc/passwd文件中包含了用户密码信息

- 挂载/bin文件夹会给恶意攻击者写入恶意可执行文件的机会

- 挂载宿主机的日志文件后,恶意攻击者可以通过篡改日志来抹掉攻击系统的证据

- 在Kubernetes场景下,将/var/log文件夹关在到容器中,所有用户都可以通过kubectl logs访问系统的日志信息

在Docker的场景下,docker deamon通过socket /var/run/docker.sock来接收客户端的命令,所有有权限写这个sock的客户端,理论上都可以创建容器实例。并且通过前文我们知道,deamon以root权限运行,会执行任何发过来的指令。因此我们可以将有权限访问Docker socket等同于宿主机上的root权限。

最后,我们也要对容器实例和宿主机共享命名空间的场景保持极高的警惕性,比如说我们希望容器实例访问宿主机上的进程信息,那么就需要在容器启动的时候,指定参数--pid=host。这种场景下从容器内部就可以看到所有的宿主机上的进程,plus所有的容器进程,因此我们就可以在容器实例中通过kill命令来结束某些进程,这会造成巨大的安全风险。

不过在容器之间,或者容器和宿主机之前共享某些命名空间并不总是bad idea,边车模式正是采用了共享网络命名空间的机制,来将网络相关的功能拆分到代理POD上。具体来说Service mesh边车代理负责应用程序的网络接入功能,比如HTTPS的证书管理和配置,这样就可以让应用程序更聚焦于业务逻辑处理,而边车POD负责设置TLS证书等。

另外我们也可以在边车上收集进入应用程序的请求信息,一次来作为监控,日志等平台的数据来源;笔者也见过在边车代理上做安全策略控制的场景,我们可以在边车代理上监控所有的进出流量以及关键字,以此来增强运行在容器实例中应用的安全性。

好了,这篇文章就这么多了,咱们下篇文章来介绍容器网络安全,敬请期待!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容