关于 Dockerfile 安全加固的那些事

  • Ross Fairbanks
  • 薛命灯

2017 年 2 月 2 日

话题:语言 & 开发架构

来自 MicroBadger 的 Ross Fairbanks 从两个关于容器安全的视频里总结了几个针对 dockerfile 的安全调整,有助于构建更加安全的容器镜像。以下内容翻译自 Ross 的文章。

最近我观看了两个有关容器安全的演讲视频,其中一个视频的演讲者是来自 Docker 的 Justin Cormack,另一个是来自 Container Solutions 的 Adrian Mouat。我们已经遵循了很多安全方面的建议,不过仍然有改进的空间。所以,我们觉得是时候对我们的 dockerfile 来一次安全大调整。

官方镜像

我们是Alpine Linux镜像的忠实用户,因为相比于 Debian 或 Ubuntu,它的体积更小,并且具有更小的可攻击表面。所以,我们使用官方的 Alpine 镜像作为其它镜像的基础。除此以外,官方镜像还有另一个好处——Docker 有一个专门的团队在维护它,并一直遵循着最佳实践。

我们的主要开发语言是 Go,不过有时候也会用 Ruby 来处理一些基于 Web 和脚本的任务。我们使用ruby:2.3-alpine作为这些镜像的基础。ruby:2.3-alpine 的 Ruby 是从源代码安装的,并非来自 Alpine 包。通过语义化版本控制(Semantic Versioning),Ruby 2.3 能够接收到来自 Ruby 核心团队的安全更新。同时,Docker 团队会更新标签 2.3-alpine。

如果不使用 ruby:2.3-alpine,我们就要自己从源代码安装 Ruby,并持续跟进 Ruby 新版本的发布情况,或者使用 Alpine 包,并持续检查 Alpine 包内是否有新的 Ruby 版本。

通知和 Web 钩子(Webhook)

从 Ruby 的例子可以看到,当底层的基础镜像接收到安全更新时,你的整个镜像也要跟着重新构建。这个时候,我们的MicroBadger通知系统可以帮我们完成一些事情。我们在 Alpine 和 Ruby 的官方镜像上使用了通知。我们会从 Slack 接收到我们所关心的公共镜像的变更通知。

当基础镜像发生变更时,我们还会利用通知来自动触发镜像的构建。Docker Hub 也有这个功能,不过我们的通知可以用于任何支持 Web 钩子的系统,比如 CI 系统或安全扫描器。

非授权用户

容器和虚拟机之间的一个关键区别是容器与主机共享内核。在默认情况下,Docker 容器运行在 root 用户下,这会导致泄露风险。因为如果容器遭到破坏,那么主机的 root 访问权限也会暴露。

使用非授权用户来运行容器可以降低这种风险。下面是一个有关 Rails 应用的例子:

# Create working directory.
WORKDIR /app
# Copy Rails app code into the image
COPY . ./
# Create non privileged user, set ownership and change user
RUN addgroup rails && adduser -D -G rails rails \
 && chown -R rails:rails /app
USER rails

安全扫描

在容器注册中心运行安全扫描可以为我们带来额外的价值。除了存放镜像,镜像注册中心定期运行安全扫描可以帮助我们找出薄弱点。Docker 为官方镜像和托管在 Docker Cloud 的私有镜像提供了安全扫描

来自 CoreOS 的Clair也很不错,它是开源的,并被用于 Quay.io 的镜像注册中心安全扫描。Clair 将支持 Alpine,这是一个好消息,希望可以尽快在 Quay 上面看到它。还有其它一些扫描器,比如TwistLockAqua,它们都是付费产品。

经过 Docker 的安全扫描,我们可以得到一个干净的 Go 镜像。我们把一个二进制版本拷贝到镜像里,然后加入一个 CA 认证依赖,这样我们就可以建立 HTTPS 连接。我们的 Rails 应用会有更多的依赖,因为 Ruby 是解释型语言。我们需要为我们的应用安装所有的 Ruby gem,同时需要为这些 gem 安装所有的操作系统依赖。

安全扫描会把扫描结果保存到 libxml2 和 libxslt 文件里。Nokogiri 是 XML 和 JSON 解析器,在构建镜像时,它会依赖前面提到的两种文件。为了得到更好的性能,Nokogiri 使用了编译过的 C 语言扩展,不过一旦安装完毕,就不再需要 libxml2 和 libxslt 文件了。

下面我们来移除构建依赖,这些命令有点复杂:

# Cache installing gems
WORKDIR /tmp
ADD Gemfile* /tmp/
# Update and install all of the required packages.
# At the end, remove build packages and apk cache
RUN apk update && apk upgrade && \
 apk add --no-cache $RUBY_PACKAGES && \
 apk add --no-cache --virtual build-deps $BUILD_PACKAGES && \
 bundle install --jobs 20 --retry 5 && \
 apk del build-deps

Gemfile 包含了需要安装的 gem,同时 Gemfile.lock 用来管理依赖关系链。通过把这些文件缓存在 /tmp 目录,只有当 Gemfile 发生变更时,bundle install 命令才会被执行。如果 Gemfile 没有发生变更,它会使用缓存里的文件。这个缓存很有用,因为安装 gem 是一个很耗时的任务,还会占用很多带宽。

run 命令跨了很多行,只有一个层会被添加到镜像里,这个层包含了 apk 和 gem 包。构建包是作为虚拟包被添加进去的,在安装完毕之后它们可以很容易被移除。

更新:编译过的二进制仍然可能是不安全的

移除构建依赖会为我们带来一些好处,不过为了 gem 的扩展,镜像里仍然会存留一个二进制包。这个二进制包很难被扫描器发现,所以它或许仍然是一个会暴露薄弱点的地方。

所以,对这个二进制包进行安全检查是必要的。对于 Nokogiri 来说,我们使用了 1.6.8 版本,这个版本包含了最新的 libxml 和 libxslt 安全补丁。不过这些包的 CVE 元数据可能存在一个问题,我已经向 Docker 安全扫描团队反馈过这个问题。

自动构建

关于容器安全很关键的一点是,当镜像或基础镜像有安全方面的更新时,需要对镜像进行重新构建。镜像被关联到 Git 仓库,所以自动构建会让整个过程变得很容易。当仓库分支上有新的提交时,如果我们对其进行了跟踪,那么一个构建就会被触发。在之前的例子里我们已经看到,当基础镜像发生变更时也会自动触发构建。

我们的 Ruby 镜像自动构建会简单一些,因为自动构建可以直接使用本地的 Dockerfile。不过我们的 Go 镜像就麻烦一些,因为在把二进制包加入镜像之前需要先通过编译,我们通过使用本地的 makefile 来完成编译。

我们可以使用钩子来进行自动构建,并使用 Docker 容器来编译二进制包。来自CenturyLinkLabsPrometheus的 Go 语言构建器镜像都是很不错的选择。

可以通过钩子来调用这些构建器镜像。我们也可以使用钩子往镜像里添加动态的元数据,就像我最近在博客里所说的那样。

我无法覆盖视频里所提到的所有主题,所以如果有可能,大家可以自己去看视频。最后我想说的是,我们在 MicroBadger 里添加了对私有仓库的支持,所以你们可以为 Docker Hub 上的私有仓库使用通知。

本文已获得原作者翻译授权,查看英文原文:Dockerfile security tuneup


感谢木环对本文的审校。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注我们。

语言 & 开发架构