Jade Dungeon

docker优化

构建安全镜像

权限管理

避免以容器以root身份运行

在Openshift与k8s环境中默认容器需要以非root身份运行,使用root身份运行的情况很少, 所以不要忘记在dockerfile中包含USER指令, 以将启动容器时默认有效的UID更改为非root用户。

以非root身份运行需要在Dockerfile中做的两个步骤:

  • 确保USER指令中指定的用户存在于容器内。
  • 在进程将要读取或写入的位置提供适当的文件系统权限。
FROM alpine
#创建目录,添加`myuser`用户,目录所有作为`myuser`
RUN mkdir /server && adduser -D myuser  && chown -R myuser /server
USER myuser
WORKDIR /server
COPY myapp ./
CMD ["./myapp"]

限制可执行文件的写权限

容器中的每个可执行文件都应该由root用户拥有,即使它由非root用户执行, 并且不应该是全局可写的。

通过阻止执行用户修改现有的二进制文件或脚本,可以有效降低攻击,保证容器不变性。 不可变容器不会在运行时自动更新其代码,可以防止正在运行的应用程序被意外或恶意修改。

例,使用COPY时:

COPY --chown=myuser:myuser myapp ./
# 应改为
COPY  myapp ./

减少攻击面

避免加载不必要的包、第三方应用或暴露端口以减少攻击面。

采用多阶段构建

采用多阶段构建可以降低构建复杂度,同时有效减小镜像尺寸。

在多阶段构建中,我们创建一个中间容器(阶段), 其中包含编译工具及生成最终可执行文件。 然后,我们只将生成的工件复制到最终镜像中, 而无需额外的开发依赖项、临时构建文件等等。

精心设计的多阶段构建仅包含最终映像中所需的最少二进制文件和依赖项, 而不包含构建工具或中间文件。它更为安全,并且还减小了镜像大小。 可以有效减少了攻击面,减少了漏洞。

使用可信赖的镜像

应该选择来自受信任仓库和经过验证的官方镜像。使用自定义镜像时, 我们应该检查镜像源和构建的 Dockerfile。

更进一步,甚至应该以这个Dockerfile来构建自己的基础镜像。 因为无法保证在公共仓库中发布的映像确实是从指定的Dockerfile构建的。

从头开始构建镜像

假如如果你是从centos镜像开始构建,那么可能将会包含几十个或者上百个漏洞。 在生产中通常会从Scratch空镜像或distroless开始。

distroless镜像仅包含应用程序及其运行时依赖项。 它们不包括在标准 Linux 发行版中发布应用如包管理器、shell 或任何其他程序。 Distroless 镜像非常小。最小的 distroless 图像gcr.io/distroless/static大约为 650 kB。 只有alpine(约2.5 MB)大小的 四分之一 ,不到debian(50 MB)大小的 1.5% 。

FROM golang:1.13-buster as build
WORKDIR /go/src/app
ADD . /go/src/app
RUN go get -d -v ./...
RUN go build -o /go/bin/app
# 引用Distroless镜像
FROM gcr.io/distroless/base-debian10
COPY --from=build /go/bin/app /
CMD ["/app"]

gcr.io/distroless/base-debian10只包含一组基本的包, 如包括只需要的库,如glibclibsslopenssl当然对于像 Go 这样不需要libc 的静态编译应用程序我们就可以替换为如下基镜像

FROM gcr.io/distroless/static-debian10

关于distroless基镜像的更多信息可以参考https://github.com/GoogleContainerTools/distroless

及时更新镜像

使用经常更新的基础镜像,在需要时重构你的镜像。随着新的安全漏洞不断被发现, 坚持使用最新的安全补丁是一种通用的安全最佳实践。

版本控制策略:

坚持使用稳定或长期支持版本,这些版本会迅速提供安全修复程序。提前计划。 准备好在基本镜像版本达到生命周期结束或停止接收更新之前删除旧版本并迁移。 定期重建自己的镜像,从基础发行版、Node、Golang、Python 等获取最新的包。

大多数包或依赖项管理器,如npm或go mod,将提供指定版本最新的安全更新。

端口暴露

仅公开应用程序需要的端口,并且避免公开 SSH (22) 等端口。

我们知道 Dockerfile 提供了EXPOSE命令有暴露端口, 但是该命令仅用于提供信息和用于文档目的。运行容器时, 容器不会自动允许所有EXPOSE端口的连接 (除非在启动容器时使用docker run --publish-all)。

启动容器时,通过-P暴露的端口应与dockerfile中EXPOSE命令指定的端口一致, 这样更便于维护。

敏感数据管理

凭证和密钥

禁止在 Dockerfile 指令(环境变量、参数或其他任何命令中)中放入凭据和密钥。

在复制文件到镜像时,即使文件在 Dockerfile 的后续指令中被删除, 它仍然可以在之前的层上访问。因为镜像分层原理,你的文件并没有真正被删除, 只是隐藏在最终文件系统中。因此在构建镜像时,我们应该遵循以下做法:

  • 如果应用程序支持通过环境变量进行配置,通过docker run中的-e选项配置, 或者使用Docker secrets、Kubernetes secrets提供值作为环境变量。
  • 使用配置文件并在docker 中绑定挂载配置文件,或者使用Kubernetes secret 挂载。

继续研究:docker secrets

ADD、COPY

ADD 和 COPY 指令在 Dockerfile 中提供类似的功能。但是COPY 更为明确。

除非我们确实需要 使用ADD 功能,例如从 URL 或从 tar 文件添加文件。 不然最好使用 COPY,COPY 的结果更具可预测性且不易出错。

在某些情况下,最好使用 RUN 指令而不是 ADD 来下载使用curl或wget的包, 解压缩然后删除原始文件,减少层数。

构建上下文与dockerignore

在构建时我们通常使用.作为上下文

$ docker build -t images:v1 .

使用.作为上下文时我们需要谨慎些, 因为docker CLI会将上下文中机密或不必要的文件添加到守护进程,甚至到容器中。

例如配置文件、凭据、备份、锁定文件、临时文件、源、子文件夹、点文件等等。 在比如:

COPY . /server

此时会将目录下所有内容都添加到镜像中,包括Dockfile本身。

所以正确做法是创建一个包含需要在容器内复制文件的文件夹, 将其用作构建上下文,并在可能的情况下明确 COPY 指令(避免使用通配符)。例如:

$ docker build -t images:v1 build_files/

为了排除不必要的文件,也可以创建一个.dockerignore文件, 在其中明确排除的文件和目录。

精简镜像减少体积

减少镜像层数

指令合并

意味着只有RUNCOPYADD三个指令会创建层,其他指令会创建一个中间镜像, 并且不会影响镜像大小。这样我们说的指令合并也就是以这三个指令为主。

我们以如下Dockerfile为例

FROM debian:stable
WORKDIR /var/www
LABEL version=“v1”
RUN apt-get update
RUN apt-get -y --no-install-recommends install curl
RUN apt-get purge -y curl
RUN apt-get autoremove -y
RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/*

构建镜像

$ docker build -t curl:v1 .

# 查看构建历史
$ docker history curl:v1
IMAGE          CREATED          CREATED BY                                      SIZE      COMMENT
29b721c09b67   18 seconds ago   /bin/sh -c rm -rf /var/lib/apt/lists/*          0B        
aa28ae151e59   20 seconds ago   /bin/sh -c apt-get clean                        0B        
4f733781f557   22 seconds ago   /bin/sh -c apt-get autoremove -y                989kB     
f66887372121   29 seconds ago   /bin/sh -c apt-get purge -y curl                987kB     
d458ee0de463   34 seconds ago   /bin/sh -c apt-get -y --no-install-recommend…   4.46MB    
43fdcf68018c   44 seconds ago   /bin/sh -c apt-get update                       17.6MB    
65631e8bb010   53 seconds ago   /bin/sh -c #(nop)  LABEL version=“v1”           0B        
7ef7c53b019c   53 seconds ago   /bin/sh -c #(nop) WORKDIR /var/www              0B        
8bfa93572e55   13 days ago      /bin/sh -c #(nop)  CMD ["bash"]                 0B        
<missing>      13 days ago      /bin/sh -c #(nop) ADD file:d78d93eff67b18592…   124MB 
  1. 镜像大小

$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE curl v1 29b721c09b67 10 minutes ago 148MB }}}

我们将RUN指令通过类shell操作&&合并后

RUN apt-get update && \
    apt-get -y --no-install-recommends install curl && \
    apt-get purge -y curl && \
    apt-get autoremove -y && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

查看构建历史与镜像大小

$ docker history curl:v2
IMAGE          CREATED              CREATED BY                                      SIZE      COMMENT
928e12c2f57e   About a minute ago   /bin/sh -c apt-get update &&     apt-get -y …   989kB     
5a32372025fb   About a minute ago   /bin/sh -c #(nop)  LABEL version=“v2”           0B        
7ef7c53b019c   30 minutes ago       /bin/sh -c #(nop) WORKDIR /var/www              0B        
8bfa93572e55   13 days ago          /bin/sh -c #(nop)  CMD ["bash"]                 0B        
<missing>      13 days ago          /bin/sh -c #(nop) ADD file:d78d93eff67b18592…   124MB

$ docker images
REPOSITORY            TAG       IMAGE ID       CREATED          SIZE
curl                  v2        928e12c2f57e   3 minutes ago    125MB

多阶段构建

在Docker17.05 中引入了多阶段构建,通过多阶段构建可以大大降低构建复杂度,同时使缩小镜像尺寸更为简单。我们来看多阶段构建的Dockerfile

#阶段1
FROM golang:1.16
WORKDIR /go/src
COPY app.go ./
RUN go build app.go -o myapp
#阶段2
FROM scratch
WORKDIR /server
COPY --from=0 /go/src/myapp ./
CMD ["./myapp"]

构建镜像

$ docker build --no-cache  -t server_app:v2 .

# 查看构建好的镜像
$ docker images
REPOSITORY            TAG       IMAGE ID       CREATED              SIZE
server_app            v2        20225cb1ea6b   12 seconds ago       1.94MB

启用squash特性

通过启用squash特性(实验性功能):

$ docker build --squash -t curl:v3 . 

可以构建的镜像压缩为一层。但是为了充分发挥容器镜像层共享的优越设计,这种方法不被推荐。

缩减容量

选择小的基础镜像

每个linux发行版镜像大小相差很多,甚至相同发行版镜像也存在差异。 我们以debian为例,稳定版和瘦身版相差约40MB:

# docker images 
debian                stable-slim   2aa48a485e3a   13 days ago         80.4MB
debian                stable        8bfa93572e55   13 days ago         124MB

我们将Dockerfile中基础镜像改为瘦身版debian:stable-slim

FROM debian:stable-slim

构建后的镜像尺寸更小

$ docker images 
REPOSITORY            TAG           IMAGE ID       CREATED             SIZE
curl                  v4            1aab5c9bf8b3   17 seconds ago      81.4MB

当前映像基于 Debian,并包含许多二进制文件。Docker 容器应该包含一个进程, 并包含运行它所需的最低限度,不需要整个操作系统。

可以使用基于 Alpine 的镜像 替换Debian 基础镜像。

FROM alpine
WORKDIR /var/www
LABEL version=“v5”
RUN echo -e 'https://mirrors.aliyun.com/alpine/v3.6/main/\nhttps://mirrors.aliyun.com/alpine/v3.6/community/' > /etc/apk/repositories && \
    apk update && \
    apk upgrade && \
    apk add --no-cache curl

查看镜像大小

$ docker images
REPOSITORY            TAG           IMAGE ID       CREATED             SIZE
curl                  v5            7f735bb213be   11 seconds ago      10.1MB

此时我们的镜像来到了10MB。使用alpine镜像包管理工具是apk, 一些软件包名可能不一样。最大的区别在于Alpine 采用的链接库是[musl libc](musl libc) 而不是[glibc](https://www.etalabs.net/compare_libcs.html)系列。

alpine镜像

Alpine一个基于musl libc和busybox、面向安全的轻量级Linux发行版。 它本身的Docker镜像只有4~5M大小。各开发语言和框架都有基于alpine制作的基础镜像, 在开发自己应用的镜像时,选择这些镜像作为基础镜像,可以大大减小镜像的体积。

例如,Java、Python、Node.js语言对应的基础镜像如下:

  • Java(Spring Boot): - openjdk:8-jdk-alpine,openjdk:8-jre-alpine等
  • Java(Tomcat) - tomcat:8.5-alpine等
  • Nodejs - node:9-alpine, node:8-alpine等
  • Python - python:3-alpine, python:2-alpine等

如果你的项目涉及到编译,比如python等涉及编译的项目,要注意,Alpine用的是muslc, 因为它原本是用作嵌入式系统的,所以并没有glibc那么完整的C标准库。

另外如果你要在Alpine中跑一些脚本的话, 那你要注意一些shell和在linux(Ubuntu、CentOS、Debian等)下的还是有所区别的, Alpine是基于busybox的,同样也是设计于嵌入式的,所以很多shell命令做了裁剪, 并不具备Ubuntu、CentOS、Debian等系统中那么完整的功能。

scratch镜像

scratch是一个空镜像,只能用于构建其他镜像, 比如你要运行一个包含所有依赖的二进制文件,如Golang程序, 可以直接使用scratch作为基础镜像。

样例:

FROM scratch
ARG ARCH
ADD bin/pause-${ARCH} /pause
ENTRYPOINT ["/pause"]

busybox镜像

如果你希望镜像里可以包含一些常用的Linux工具,busybox镜像是个不错选择, 它集成了一百多个最常用Linux命令和工具的软件工具箱,镜像本身只有1.16M, 非常便于构建小镜像。

distroless镜像

distroless镜像,它仅包含您的应用程序及其运行时依赖项。 它们不包含您希望在标准 Linux 发行版中找到的包管理器、shell或任何其他程序。

由于Distroless是原始操作系统的精简版本,不包含额外的程序。容器里并没有Shell! 如果黑客入侵了我们的应用程序并获取了容器的访问权限,他也无法造成太大的损害。 也就是说,程序越少则尺寸越小也越安全。不过,代价是调试更麻烦。

需要注意的是,我们不应该在生产环境中,将Shell附加到容器中进行调试,而应依靠正确的日志和监控。

样例:

FROM node:8 as build

WORKDIR /app
COPY package.json index.js ./
RUN npm install

FROM gcr.io/distroless/nodejs

COPY --from=build /app /
EXPOSE 3000
CMD ["index.js"]

Distroless镜像和Alpine镜像

如果是在生产环境中运行,并且注重安全性, Distroless镜像可能会更合适。

Docker镜像中每增加一个二进制程序,就会给整个应用程序带来一定的风险。 在容器中只安装一个二进制程序即可降低整体风险。

举个例子,如果黑客在运行于Distroless的应用中发现了一个漏洞, 他也无法在容器中创建Shell,因为根本就没有。

如果更在意要是大小,则可以换成Alpine基础镜像。

这两个都很小,代价是兼容性。Alpine用了一个稍稍有点不一样的C标准库——muslc, 时不时会碰到点兼容性的问题。

说明:

原生基础镜像非常适合用于测试和开发。它的尺寸比较大, 不过用起来就像你主机上安装的Ubuntu一样。 并且你能访问该操作系统里有的所有二进制程序。

上下文管理

我们经常会用到的COPY指令

COPY . /server/dir

COPY会把整个 构建上下文复制到镜像中,并生产新的缓存层。 为了不必要的文件如日志、缓存文件、Git 历史记录被加载到构建上下文, 我们最好添加.dockerignore用于忽略非必须文件。这也是精简镜像关键一步, 同时能更好的保证我们构建的镜像安全性。

及时清理下载

我们有如下Dockerfile

..
WORKDIR /tmp
RUN curl -LO https://docker.com/download.zip && \
	tar -xf download.zip -C /var/www 
RUN rm  -f download.zip
...

我们虽然使用了rm删除download.zip包,由于镜像分层的问题, download.zip是在新的一层被删除,上一层仍然存在。

我们要在一层中及时清理下载

RUN curl -LO https://docker.com/download.zip &&  \
	tar -xf download.zip -C /var/www &&  rm  -f download.zip

另外在安装软件时应及时使用包管理工具清除你下载的软件依赖及缓存, 比如在我们dockerfile中使用apt包管理工具做清理。