容器在近些年变得炙手可热,提到容器就不能不提到镜像,如果说容器是云计算时代的核心内容之一,那么镜像就是容器这个核心的灵魂。所以镜像的安全也就显得尤为重要。
s$ Q$ O0 R. ]8 u6 d( J$ X; N但是从下面的几组数据可以看到:镜像的安全问题却不那么令人乐观。( |# ]: R2 k7 E1 C! h8 w
1) Linux OS的漏洞呈逐年上涨趋势& Q/ N- `! B/ [. L
从2008年的不到300+,上升至2018年的2000+。而且近两年呈指数级上升趋势。
' b# y, u& a: R5 h2) 高危漏洞数量呈逐年上涨趋势
8 D' b& g' S, C* W) @从2014年的不到2000+,上升至2018年的4000+。1 ~8 X& I' O1 G7 v; r. K
3) Docker 镜像普遍存在漏洞& L& }# S0 Z4 r0 D
最受欢迎的镜像比如nginx,ubuntu,java等普遍存在漏洞,且多达数十个。' q1 x# z5 v: _9 m; T
4) 40%的受访者表示,没有在CI阶段采取任何安全手段。
3 f$ v3 ~( z# d/ c' f因此,保证镜像安全,不仅是保障云平台安全的重要一环,更是DevSecOps治理体系中一个重要的话题。/ o* k4 ]1 N1 B5 R2 ~- H
/ |( q! K* [" W& x
$ _) j: n$ a: {/ b- O1 l' ^$ d- e
一、镜像的特点 - T! M: f: B0 o V
镜像的本质、镜像的来源以及镜像的制作共同造就了镜像的以下几个特点:
! @: }( i: ?7 n+ d0 R4 a/ n8 g
( ?2 r2 O5 S: T: V1 S# |. i
2 s: k- B7 \! H. Z镜像内容的复杂性(既包括一些OS的各种库、包,又包含一些Python或者Ruby等文件)、来源的复杂性(官方的、个人的)、Dockerfile 命令的复杂性(ADD和COPY的区别使用,CMD和ENTRYPOINT的区别使用,命令顺序不同造就不同的镜像)使得镜像本身就变得非常复杂,如果考虑安全因素,那就更加复杂了。: c4 h: M/ a1 j `8 l1 g: _# O! Z
, T- M A. n4 q+ R& o9 F
" f, P/ L& [. P r% Y. C5 T. d3 S* g. D4 T& E
镜像的制作是非常方便的,只要会写Dockerfile,就能制作出镜像,或者也可将既有容器制作成一个镜像。而且镜像的使用也是非常方便的,一个命令(docker run)就能将一个静态的镜像转变为一个动态的容器,恰恰是这种便捷性造成了镜像的另外一个特点,也就是下面的混乱性。0 w) i( T7 T' b6 P8 j+ M2 |& p
( Y7 F4 M8 G. N
- 混乱性! K% R! x- T) D# ?" N: Q7 ?
) p: ]( A3 ~% y
人人可制作镜像,人人可管理镜像,也就使得镜像的种类、数量都是异常庞大的,而且针对某个特定功能的镜像可达几十个,甚至上百个,既有官方的,又有个人的,既有共有仓库的,又有私有仓库的。没有一个行之有效的标准去规范的管理这些镜像,就使得镜像良莠不齐,选用时较为困难。 O) X r/ ~9 L' ~" y
' }. J9 `. T$ J, f0 t上述的特点也就使得镜像的安全变成了一个复杂且容易被忽略的点。然而再复杂,也有手段去完成这个复杂的工作,也就是下面要讲的镜像扫描。
- l' J, \7 g# e/ l( D c2 S; s& f N8 h* Z* d
8 h$ U( t: S9 R. {+ i
二、镜像扫描在SDLC中的位置 / U3 y) m% h3 z" K- j, a9 O% V
' v2 W& z6 n$ A" \
: v* I6 |# E9 ~4 u" z* N镜像扫描是保证镜像安全的一个强有力手段,其通常发生在软件开发生命周期(SDLC: Software Development Life Cycle)的构建阶段,如下所示:" T$ L7 R9 S9 m ]: Q
& \! I' A( n( |* N1 [% I
0 e) e4 U t: c! _+ l$ g
% |" t; F: L$ e- R( l" w& O3 x
现有的镜像扫描大都是依赖于镜像仓库提供的扫描功能(内置镜像扫描工具),一般流程如下:
$ W, B- S! Q; v
) Z2 I" X' f& j- t, ?
0 S$ g$ r* X% Y$ R- 开发人员提交代码变更;
- 触发一个构建流程;
- 进行镜像构建(docker build);
- 镜像镜像推送(docker push);
- 进行镜像扫描(利用镜像仓库内置扫描工具进行);
- 进行镜像部署(docker run)。
2 U: U, u9 U u7 ^+ u . u4 ^1 r/ g8 J4 g: t1 K. Y* m$ N
. T3 e2 A% Z" N1 l9 O这种流程有诸多弊端:9 R0 X4 ^% ^$ V2 S
- 扫描滞后0 c+ e, S4 _" a8 Z& T- v
镜像的扫描依赖于镜像仓库自带的镜像扫描功能,只有镜像推送至镜像仓库才会进行镜像扫描,这种滞后的扫描会导致仓库中保存有漏洞的镜像。在DevSecOps中,希望做到的是:镜像仓库中存储的是安全的、可随时部署的镜像。
L* C5 P+ q4 D9 q8 r+ s+ c* {3 f. H4 O% s* K
- 不能有效的终止CI/CD流程
' C/ c! A, y& z0 O 成熟的DevSecOps CI/CD 应该是:当检测到镜像是不安全的,那么就应该立即终止CI/CD流程,防止不安全的镜像被部署到环境中。这种依赖于镜像仓库自带扫描功能的滞后镜像扫描,没有办法做到这一点,因为镜像推送之后的扫描,和镜像的部署是同时进行的。
* Q5 }+ q4 O/ `$ I) D7 g8 R, h4 R6 U0 N" Z( Y, A0 y; X) E% n2 K. L
镜像是要占据一定的磁盘空间的(有些镜像大小上百M,甚至上G),在镜像仓库中保存有大量有漏洞的镜像,就会占据大量的磁盘空间,使仓库的使用成本上升。0 g0 o- k* u* |; V$ s( ?! c
8 j# h3 [6 B0 G9 i5 ]# y7 h K/ Y3 s M
三、镜像扫描前置 : I7 r9 {; b6 [2 C' }1 v6 o: ]
# o: `8 Z2 S' V
" s3 v/ n* w+ p! p1 h基于扫描滞后的不足,镜像扫描的前置就显得尤为重要了,将镜像扫描的操作前置(如下图绿色虚线方框所示):
1 }0 z1 ]3 h0 X: {& M7 k& Y2 I% `3 P* b) [/ S( G+ m
0 g' a3 t! F" Y, Q
' W8 S$ a% f9 W让其位于镜像构建之后,镜像推送之前,这样一旦一个镜像扫描结束,如果其结果是安全的,那么就顺利进行后续的操作(推送至镜像仓库并进行镜像部署);如果扫描结果是不安全的,那么就立即终止该CI/CD流程,并通知相应的人员。这样就防止了不安全镜像推送至镜像仓库。保证镜像仓库中存储的镜像都是安全的,而且节省了磁盘空间。同时,前置扫描对镜像的操作空间会变得更大,比如可以对镜像做一些黑白名单、RBAC(Role Based Access Control)权限控制等。
/ l8 S, p4 W8 Y* Z$ z- V1 R% J对于前置的镜像扫描操作,就需要借助一些现有的镜像扫描工具来帮助我们完成这个操作。这些工具既有大家耳熟能详的Clair,也有一些新贵比如Anchore,Trivy等,关于这些工具的具体使用和对比,会在后期有有一篇专门的文章来分析。
/ Q. M, N# f; w" _% r镜像扫描只是帮助我们,发现镜像问题,然后解决镜像问题,这只是镜像问题的治疗手段,然而对于镜像问题,最好是要做到防止问题发生,也就是在构建镜像的时候,尽可能的根据一些最佳实践来构建一些尽量安全的镜像,这也就是我们接下来要讲的镜像安全的一些最佳实践。这样通过预防为主,防治结合(防:根据最佳实践构建安全镜像;治:嵌入CI/CD中的前置镜像扫描)的方法来确保镜像的安全。% p; q# a+ T' p1 \( }
8 f! f/ D/ b" l# Z3 [) b
7 H5 A# |( ]' _# H! V$ X四、容器镜像安全最佳实践 * g( I x7 ~7 Q3 W& T; D
5 F* _! n; i; p, P% M
* V0 q. |2 E+ I4 j* A; o! `4.1 尽量选择轻量的基础镜像
& ~9 S* _* k0 t, E y+ g0 Z- s7 O, G: Z9 c7 n9 [
5 \8 u: b8 a( q
每个镜像都有一个基础镜像,也就是在 Dockerfile中的 FROM 中指定的镜像,一般这个基础镜像就是一个Linux发行版,比如alpine,ubuntu等。在为一个新项目选取基础镜像的时候,应该考虑这个项目的运行是否需要一个全量的操作系统(包含各种库),如果说alpine就能能满足要求,就不要选择ubuntu这个相对庞大的操作系统,作为新项目的基础镜像。不同量级的操作系统,除了有镜像大小的区别,更重要的是,操作系统包含的文件系统越多,攻击面也就越大,包含的漏洞也就越多。 ! ~" \5 \- N Z5 o' r' `1 p
0 w2 [% r* b- V
用Anchore对 openjdk:8-jdk-alpine和openjdk:8-jdk进行扫描,结果显示openjdk:8-jdk有128个漏洞,而openjdk:8-jdk-alpine却只有48个。而且openjdk:8-jdk-alpine的镜像大小仅为openjdk:8-jdk的三分之一不到。
( z: z$ T% C; x
" p1 k- Z9 _/ [+ N* |8 c
/ k! J9 |2 z2 M2 a8 j4 |. |
4 Y; o$ f9 K( V$ o" C3 O4.2 添加非root用户,以最小授权用户运行容器, P- K( R f, h# L5 }0 A+ J- c
: F0 a4 i) d- v( t. Q( }容器是和宿主机共享内核的,如果以root用户启动容器,也就意味着容器是有root权限去操作宿主机,这样就使得宿主机的受攻击面增大,潜在威胁系数提高。因此,应该指定一个非root的特定用户,然后以此特定用户来启动容器。如果是通过Dockerfile的方式来构建镜像,可以在Dockerfile中添加如下代码,来添加一个位于特定group的特定用户,并以此特定用户来启动容器:0 O9 F( A1 n# P
& n/ w7 X; r3 g! J: vFROM your_base_image
* }% O$ D, G% O6 SRUN groupadd -r devsecops && useradd -r -g devops devsecops
* z7 O! }4 j) hUSER devsecops4 g8 s9 c; C. q1 O: H5 @8 }1 P
......(your other dockerfile steps)
2 j" i5 O2 v) H3 @* M* n8 i0 V2 Q' v
4.3 不要将敏感信息暴漏在镜像中
1 x8 ?) |! ]- B7 r% d
! {4 [6 c0 H- `7 j$ B! |不要将token,密码,ssh key等敏感信息,存储在镜像中,一旦镜像被推送至公共镜像仓库,那么就会造成上述敏感信息的泄漏。结果将是灾难性的。如果在镜像中必须要用到一些敏感信息,可以采用docker提供的secret功能,或者通过多阶段构建来完成。具体的使用可以参考docker官网。
( o& O! A. `- F5 Q4 V% p# q1 q# P6 v; i1 W8 k( n( [
4.4 不要安装不需要的包
/ |& b& ~" F# P' k/ u+ ]
4 g! E* K) p1 n# K8 W很多镜像中都通过"apt-get update && apt-get install xxx" 或者"yum update && yum install xxx"的方式来安装软件包,但是切记:只安装与应用程序运行有关的包,不要安装非必须的包。比如在数据库镜像中安装vim。安装的包越多,镜像的复杂性就越高,依赖也越多,不仅导致容器镜像的体积变大,还使得镜像的受攻击面变大,安全性降低。
. Q: y. ]$ J, V, Q! W/ D5 n+ }; |7 j$ y1 y' ?6 r
当以ubuntu:16.04为基础镜像时,执行 "apt-get update && apt-get install vim telnet curl -y"命令和不执行此命令的镜像,其漏洞数量和镜像体积都是不一样的。详细对比如下图。
+ U& B: h6 j" } L2 h
/ r2 W$ V9 O4 {4 j; ?* _$ O& a$ Y9 W
. i8 M, c* [7 w9 T
4 T) x' v* b9 }4.5 对Dockerfile进行扫描& h U1 @" E; y% x
/ n$ c" w7 _0 M
可以用Dockerfile扫描工具(比如Hadolint)来对Dockerfile来进行扫描,以发现Dockerfile中的不规范写法或者一些可能引起安全问题的构建命令。比如用Hadolint扫描如下的Dockerfile:
% C2 W7 |6 W8 u1 `0 o
5 |$ s9 a6 Q+ {- A( _FROM alpine0 C/ R$ v' O2 N, H" F1 n% Q' w
RUN apk add busybox-extras
9 m" g8 p" X) s% \" ]. i4 H# p
5 l0 u7 T2 ]( R2 y
可以看到如下输出结果: & D! G5 m+ L. m% W7 g
i2 m1 e6 H+ N" W, ?2 d$ O
/dev/stdin:1 DL3006 Always tag the version of an image explicitly4 h+ V# Z9 S$ i( b) T
/dev/stdin:2 DL3018 Pin versions in apk add. Instead of `apk add <package>` use `apk add <package>=<version>`
- g+ P$ I' C/ Z* t3 S3 T5 n/dev/stdin:2 DL3019 Use the `--no-cache` switch to avoid the need to use `--update` and remove `/var/cache/apk/*` when done installing packages
) J5 p+ x5 j5 p. Z3 V
3 s: E& u( ]! s: B) }
结果提示,基础镜像(alpine)应该要指定tag(DL3006提示所示),安装包时(apk add) 要指定包(busybox-extras)的版本(DL3018提示所示)。
5 E4 P0 j* c, e- b1 ]' X+ d3 n9 u9 |
) t7 d5 C# K8 M3 h7 a
4.6 不要选取未知来源且没有维护的镜像 - K4 E1 w# K8 u/ J3 y8 q+ q+ |9 v
2 H4 b& e. ~. P. H2 @$ R4 y2 N
当选取基础镜像时,不要选取来源未知(不知道这个镜像的作者是何方神圣,不知道Dockerfile的内容,不知道基础镜像的内容)、停止维护(最后一次更新是几个月甚至几年前,这段时间积累了多少问题,天知道)的镜像。因为不能确定镜像中包含的内容是否是安全的。更不要仅以镜像被拉取的次数、Star数量作为指标来决定是否使用镜像,因为这个次数是可以被刷上去的。一定要尽量选取官方的、Dockerfile内容明确的,更新频繁的镜像,如果没有符合条件的镜像,可以尝试自己制作镜像。
% s- E4 M, H i, @1 ?
( P( N, \9 }- |; Y1 p' ~3 I2 N
上述几点只是构建安全、规范镜像的一部分手段,本文只是列出来一些重要,而且常见的。其他还有诸如使用镜像签名来防止镜像被篡改、构建受信的镜像仓库并通过RBAC和黑名单等机制来对镜像的拉取和推送等做权限控制等手段来进一步保证镜像的安全。限于篇幅,本文不再进一步深入探讨,感兴趣的,可以深入进行研究。 ( A2 B( e0 x( J8 y' Y
2 l% _+ A3 `6 E5 w3 k
& ]( y9 C& J! Y9 y
# T3 {* x p% t4 L3 @: ]* u0 B! p! {& Q
; [& M$ M& U1 ?' b
五、总结
# J, q" Z" q( l( ^
% a5 h; a# u7 X/ X9 s
: n& O: B/ F+ O0 {
5 g: m! f2 Q) k( G1 E
没有绝对的安全,也没有一劳永逸的安全,只有永不止步的行动。镜像扫描只是通过工具帮助我们来发现安全问题,然后去有目标的解决问题,属于一种治疗手段;能够未雨绸缪,预防问题的发生才是最重要的,所以,应该养成良好的习惯:从安全的角度出发,结合上述的最佳实践,来构建安全,规范的镜像。以"预防为主,防治结合"的方式来进一步确保镜像的安全。(IDCF)
& @" j1 n7 D. ~ |