云计算时代容器安全综述-容器镜像安全(上)

笔者在前边的多篇文章中介绍过Docker这家公司为容器化技术进入寻常企业“家”作出的卓越贡献。除去我们耳熟能像的容器打包,容器仓库,开发人员视角等贡献之外,笔者认为Docker公司推动容器化被企业广泛接受的最最重要的创新就是容器镜像(Docker Images)。如果我们仔细分析整个Docker技术栈,你会发现支撑Docker体系所依赖的核心技术在Linux都已经存在很多年了,并且也有各种各样的使用场景。而对于开发人员来说,打包是Docker之前应用部署遇到的最大的痛点。

应用打包有两个极端:像VM这样将操作系统和应用以及应用的依赖都打包在一起,结果就是随随便便高达几个G的镜像文件,而像Java这样的编程语言对应用程序打包又略显单薄,因为底层操作系统层面的依赖很容易在多应用的场景下出现版本冲突的问题,开发人员又会陷入:在我机器上运行的好好的,为啥在生产环境就挂了呢?这样的灵魂拷问。具体来说Docker镜像最大创新就是应用彻底的打包和镜像的分层机制。应用打包的完整性(除了操作系统内核之外的多有依赖都打包到应用程序的镜像中)解决了在同一台机器上运行多个应用可能造成的依赖项版本冲突的问题,而进行的分层机制让我们不在为动辄几个G的镜像问题烦恼,因为很多场景下你可能只需要下载很少的数据就能将应用启动起来,灵活性和可运维性得到巨大的提升。

那么容器镜像到底如何理解?大家可以简单的把容器镜像(Docker Image)看成将应用程序运行需要的除操作系统内核之外的所有依赖都打包到一起的应用构建方式,镜像中除了应用程序代码之外,还包括应用程序的外部依赖,操作系统的文件系统等。有了这个镜像包之后,为了让应用程序运行起来,我们唯一缺少的就是操作系统内核,而我们一般通过物理机或者虚拟机的方式来提供容器运行需要的内核和计算资源。

如果我们换个角度来看Docker的镜像(笔者后边的内容会在容器镜像,Docker镜像,镜像之间切换),镜像可以被分为两部分,分别为root文件系统和配置信息。笔者在前边介绍容器三大支柱的文章中介绍root文件系统的使用过单独下载的alpine root文件系统,来启动容器实例,我们可以对比docker run -it alpine sh,在启动容器实例后,运行ls返回的结果和我们单独下载的alpine root文件系统的内容完全一致(当然前提是docker启动实例的alpine版本必须和下载的一致),结果如下图所示:

《图1.1 对比容器实例的文件系统和下载的Alpine root文件系统》

从上边的对比可以看到,在Alpine版本一致的情况下,Docker实例和下载的root文件系统的文件夹结构完全一致。由于Docker的广泛普及,大多人对容器镜像的反应就是Docker,具体来说就是通过Dockerfile来构建容器镜像。Dockerfile中包含了构建镜像的指令,比如FROM,ADD,COPY,RUN等,这些指令会修改基础镜像的root文件系统的内容,还有一些诸如USER,PORT,ENV等指令,会修改应用程序的配置信息。不过镜像中的配置信息我们可以在通过命令行参数在启动容器实例的时候进行修改,比如使用命令docker run -e <VARNAME>=<NEWVALUE> ...来重写镜像中的参数值。

如果读者有过Kubernetes的使用经验,或者阅读过笔者之前的系列文章,应该知道我们可以在YAML文件中来rewrite环境变量等配置信息。当我们在YAML中重新给变量赋值后,那么YAML文件中的值会有更高的优先级。在Kuberntes体系中,专门用来规范容器镜像和运行时的标准叫OCI(Open Conatiner Initiative),由于Docker在Kubernetes最初几个版本中是默认的容器运行时,以及大量的用户群体已经习惯了Docker的操作习惯,因此OCI的目的也变得很奇怪:将容器镜像和运行时标准化,并且尽量和Docker的使用体验保持一致。另外OCI规范定义了容器镜像的格式,以及如何构建镜像和分发镜像。

为了更加方便的分析OCI镜像,我们需要使用Skopeo工具来把镜像从Docker格式转换成OCI格式(如果读者机器上还没有安装Skopeo工具,可以自行安装。如果读者的环境是macOS,执行brew install skopeo就可以完成安装)。笔者在自己机器上运行的结果如下:

➜  skopeo skopeo copy docker://alpine:latest oci:alpine:latest --override-os linux

Getting image source signatures

Copying blob a0d0a0d46f8b done

Copying config 696d33ca15 done

Writing manifest to image destination

Storing signatures

➜  skopeo ls

alpine

➜  skopeo cd alpine

➜  alpine ls

blobs      index.json oci-layout

从输出的结果可以看到OCI格式的镜像中包含的数据。不过OCI格式的镜像并不能直接被runc这样的运行时直接使用,我们需要将镜像解压成运行时filesystem bundle,为了解压数据,我们需要使用另外一个工具umoci,在笔者的机器上输出如下:

➜  skopeo sudo umoci unpack --image alpine:latest alpine-bundle

➜  skopeo ls

alpine        alpine-bundle

➜  skopeo sudo ls alpine-bundle

config.json

rootfs

sha256_03014f0323753134bf6399ffbe26dcd75e89c6a7429adfab392d64706649f07b.mtree

umoci.json

➜  skopeo sudo ls alpine-bundle/rootfs

bin dev etc home    lib media  mnt opt proc    root    run sbin    srv sys tmp usr var

如上图所示,bundle中包含了rootfs,通过名字我们不难理解这是一个root filesystems文件夹,读者可以对比图1.1,文件夹的结构应该是完全一样的。除了rootfs之外,还有一个叫config.json的文件,这个文件非常重要,这是容器运行时启动容器实例时,所有的配置信息都从这个文件中读取。因此我们可以说,容器运行时初始化容器实例的时候,,使用rootfs来作为root文件系统,而容器实例的配置信息从config.json中读取。

上边的信息可能让读者有点摸不着头脑,我平时使用Docker的时候,没有这么复杂啊,不就是docker run一下就启动实例了。不过从这点大家应该能够看到Docker为容器化应用部署在寻常企业家落地作出了巨大的贡献啊,操作起来是如此的方便。如果要早Docker中类比这个配置文件,大家可以运行docker image inspect命令,笔者在自己机器上运行docker image inspect alpine命令返回的结果如下图所示:

《图1.2 Docker中的容器运行时配置文件》

如果我们仔细对比图1.2的输出内容和config.json中的内容,会发现这些配置文件中包含的信息基本一样。我们以config.json为例,其中包含了runc要启动容器实例所需要的所有配置信息,包括但不限于:需要创建的namespace,需要cgroup来约束的资源配置信息等。到这里为止,希望大家能够充分理解笔者在前边提到的一个观点:容器镜像可以分为root文件系统和配置信息两个部分的概念。

有了对容器镜像的深入理解,接下来我们来看看如何构建容器镜像。不过说起来镜像构建,读者应该第一时间就会反应出来docker build命令。Docker build会读取输入的Dockerfile的配置信息,来生成对应的镜像文件。不过由于docker build本身的工作原理已经非常成熟了,没有什么值得大书特书的,咱主要还是从安全的角度来看看,docker build这种构建容器镜像的方式到底有哪些安全风险。

当我们在安装了Docker Desktop的机器上运行docker build命令的时候,本质上我们通过docker的客户端工具,来发送指令给服务端的Docker deamon,docker build命令会发送给机器上的Docker socket,也就是说任何可以访问这个socket的进程,都可以发送API请求命令给deamon。

Docker deamon是服务器端运行的进程,主要任务是接受客户端的命令,并基于用户提供的配置信息来运行容器实例。由于运行实例需要创建对应的命名空间,因此deamon必须以root权限在机器上运行,要不然就没有办法创建容器实例。那么问题就来了,如果我们部队Docker deamon进行安全保护,那么就会有恶意用户通过docker run来运行任意的容器实例,进而窃取敏感数据。更为不幸的是,这些恶意的行为即便是通过审计日志进行跟踪,日志中输出的的也只有proess id(deamon process),而没有用户ID,这就给系统造成的极大的安全风险。因此为了降级这种安全风险,我们需要使用Deamonless build机制,来安全的构建容器镜像。关于Deamonless构建方式,市场上目前可选择的有Buildkit,podman,buildah等,感兴趣的读者可以查阅相关的资料,笔者这里就不在累述了。笔者想特别强调的是,Buildkit是Docker rootless模式的基础。

不过无论我们选择什么的构建方式,容器镜像的构建核心还是Dockerfile文件,Dockerfile中包含了很多指令instructions,这些类如ADD,RUN,变量的指令的运行结果要么给镜像的基础文件系统增加一层(layer),要么修改镜像的配置信息。不过从安全的角度看,容器镜像本身根本没有任何安全性可言,因为任何可以docker pull拉取到镜像的用户,可以访问镜像中的所有数据。因此笔者强烈建议大家不要把敏感信息放到容器镜像中,比如数据库连接信息,token信息等。

容器镜像是由多个层(layer)叠加构成的,因此我们必须非常小心,避免把敏感信息保存到容器镜像中,即便是我们在容器启动的时候,通过delete命令删除了某些文件,实际上这些文件在底层并未被删除,而是在root文件系统之上,覆盖了一层来达到删除的效果,我们来举个例子说明一下。假设我们有如下的Dockerfile内容:

FROM alpine

RUN echo "yunpan-secret-info" > /yingsixinxi.txt

RUN rm /yingsixinxi.txt

咱们这个Dockerfile purposely先在容器镜像中创建了yingsixinxi.txt文件,然后又通过rm命令删除了这个包含隐私信息的文件(虽然这么写在实际业务场景中没什么价值,笔者的目的是为了演示删除文件并无法保证敏感信息不被其他人看到),从逻辑的角度来讲,敏感信息应该已经被删除了,但是实际上我们仍然可以获取到敏感信息"yunpan-secret-info"。接下来咱们一起来验证一下,看看是否如笔者所说。

首先咱们来在本地先把镜像build出来,在包含Dockerfile的目录中运行命令docker build -t qigaopan/sensitive . 来构建这个叫sensitive的镜像包,然后我们可以通过将镜像运行起来并验证文件系统是否包含yingsixinxi.txt文件,运行命令docker run --rm -it qigaopan/sensitive ls /yingsixinxi.txt,在笔者的机器上输出如下:

➜  source docker run --rm -it qigaopan/sensitive ls /yingsixinxi.txt

ls: /yingsixinxi.txt: No such file or directory

但是不要被上边的输出”No such file or directory“蒙蔽你的双眼啊,敏感信息依然保存在镜像中,并且如果我们持有镜像,是可以访问到敏感信息的。首先我们通过docker save将镜像保存到本地,然后对镜像进行解压,在笔者机器上的操作序列如下:

➜  source docker save qigaopan/sensitive > sensitive.tar

➜  source cd sensitive

➜  sensitive tar -xf ../sensitive.tar

➜  sensitive ls

058f0762dc128781d769f93f1a632fc747e256ad3005cb535fdfadf8c609c6e9.json deddfc0e9831c17535387ec0ef82957f5687c0e27e19478046acb7aa437cf149

304e424bc2d24962ebf3497697c15f6c58bb58a8bf58832fa5286f30d0d36285      manifest.json

4cc007462fec3a663fe5537098abe3f633f20dcd872cee7d357fa5719905635a      repositories

解压镜像后会包含很多信息,manifest.json包含了对镜像的描述信息,058***.json文件是镜像的配置文件,剩下的文件夹是组成这个镜像的多个层(layer)。由于配置文件058*.json中包含了构建这个镜像的历史信息,因此我们可以很容通过分析这个文件来获取到敏感数据信息。在对应的目录运行命令cat 058f*.json | jq '.history',输出入下图所示:

《图1.3 通过配置文件获取镜像中探索敏感信息》

基于配置文件的输出,我们确定镜像中包含了敏感信息,接下来我们就只需要把对应文件夹下的压缩包解压,在笔者机器上的输出如下:

➜  sensitive cd deddfc0e9831c17535387ec0ef82957f5687c0e27e19478046acb7aa437cf149

➜  deddfc0e9831c17535387ec0ef82957f5687c0e27e19478046acb7aa437cf149 tar -xf layer.tar

➜  deddfc0e9831c17535387ec0ef82957f5687c0e27e19478046acb7aa437cf149 ls

VERSION        etc            json            layer.tar      yingsixinxi.txt

如上图所示,任何在容器镜像中出现过的信息,都会被保存下来,因此大家千万不要在自己的镜像中包含任何敏感数据,也不要借助于rm这样的指令来试图掩耳盗铃。

好了, 今天这篇文章就这么多了,我们下篇文章继续讨论镜像是如何被保存和分发的,借此来讨论涉及到的安全风险,敬请期待!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容