25、Docker 镜像分发、构建与安全部署指南

Docker 镜像分发、构建与安全部署指南

1. 镜像分发替代方案

若你既不信任私有镜像仓库,也不信任 Docker Hub 来分发镜像,可使用 docker load docker save 命令来导出和导入镜像。镜像能通过内部下载站点分发,或者简单地复制文件。不过,采用这种方式可能会让你重新实现 Docker 镜像仓库和内容信任组件的诸多功能。

2. 可重现且可信的 Dockerfile

理想情况下,Dockerfile 每次都应生成完全相同的镜像,但实际很难做到。同一 Dockerfile 随时间推移可能会生成不同的镜像,这会让人难以确定镜像里的具体内容。编写 Dockerfile 时遵循以下规则,可接近实现完全可重现的构建:
- 指定 FROM 指令的标签 FROM redis 不可取,因为它会拉取最新标签,而该标签随时间可能会有重大版本变更。 FROM redis:3.0 稍好,但仍可能因小更新和 bug 修复而改变。若要确保每次拉取的是完全相同的镜像,需使用摘要,例如:

FROM redis@sha256:3479bbcab384fa343b52743b933661335448f8166203688006...

使用摘要还能防止意外损坏或篡改。
- 安装软件时提供版本号 apt-get install cowsay 可行,因为 cowsay 不太可能改变,但 apt-get install cowsay=3.03+dfsg1 - 6 更好。其他包安装器(如 pip )也应尽量提供版本号。若旧包被移除,构建会失败,但至少能给出警告。不过,包可能会引入依赖项,这些依赖项常以 >= 形式指定,会随时间改变。可使用 aptly 等工具对仓库进行快照,以锁定版本。
- 验证从互联网下载的软件或数据 :使用校验和或加密签名。这是所有步骤中最重要的,若不验证下载内容,易受意外损坏和攻击者篡改的影响,尤其是通过 HTTP 传输软件时,无法防止中间人攻击。

2.1 官方镜像 Dockerfile 示例

大多数官方镜像的 Dockerfile 提供了使用标签版本和验证下载的良好示例。它们通常使用基础镜像的特定标签,但在使用包管理器安装软件时不使用版本号。

2.2 安全下载软件

2.2.1 官方 Node.js 镜像示例
RUN gpg --keyserver pool.sks-keyservers.net \
        --recv-keys 7937DFD2AB06298B2293C3187D33FF9D0246406D \
                    114F43EE0176B71C7BC219DD50A3051F888C628D 
ENV NODE_VERSION 0.10.38
ENV NPM_VERSION 2.10.0
RUN curl -SLO "http://nodejs.org/dist/v$NODE_VERSION/\
node-v$NODE_VERSION-linux-x64.tar.gz" \ 
  && curl -SLO "http://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \ 
  && gpg --verify SHASUMS256.txt.asc \ 
  && grep " node-v$NODE_VERSION-linux-x64.tar.gz\$" SHASUMS256.txt.asc \
     | sha256sum -c - 

操作步骤:
1. 获取用于签署 Node.js 下载的 GPG 密钥。
2. 下载 Node.js 压缩包。
3. 下载压缩包的校验和。
4. 使用 GPG 验证校验和是否由获取的密钥所有者签署。
5. 使用 sha256sum 工具检查校验和是否与压缩包匹配。若 GPG 测试或校验和测试失败,构建将中止。

2.2.2 官方 Nginx 镜像示例
RUN apt-key adv --keyserver hkp://pgp.mit.edu:80 \
                --recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62
RUN echo "deb http://nginx.org/packages/mainline/debian/ jessie nginx" \
         >> /etc/apt/sources.list

操作步骤:
1. 获取 Nginx 的签名密钥并添加到密钥库。
2. 将 Nginx 包仓库添加到软件检查的仓库列表。之后可使用 apt-get install -y nginx (最好指定版本号)安全安装 Nginx。

2.2.3 自定义校验和示例

若没有签名包或校验和,可自行创建。例如,为 Redis 版本创建校验和:

$ curl -s -o redis.tar.gz http://download.redis.io/releases/redis-3.0.1.tar.gz
$ sha1sum -b redis.tar.gz 
fe1d06599042bfe6a0e738542f302ce9533dde88 *redis.tar.gz

将以下内容添加到 Dockerfile:

RUN curl -sSL -o redis.tar.gz \
         http://download.redis.io/releases/redis-3.0.1.tar.gz \
    && echo "fe1d06599042bfe6a0e738542f302ce9533dde88 *redis.tar.gz" \
       | sha1sum -c -

此操作会下载文件为 redis.tar.gz ,并让 sha1sum 验证校验和。若校验失败,命令将失败,构建会中止。若频繁发布,可自动化此过程,很多官方镜像仓库有 update.sh 脚本用于此目的。

3. 安全部署容器的实用技巧

3.1 设置用户

切勿在容器内以 root 身份运行生产应用程序。攻击者攻破应用程序后,将完全访问容器的所有数据和程序。若攻击者突破容器,还会获得主机的 root 权限。为避免以 root 身份运行,Dockerfile 应创建非特权用户,并使用 USER 语句或入口点脚本切换到该用户。例如:

RUN groupadd -r user_grp && useradd -r -g user_grp user
USER user

此操作会创建一个名为 user_grp 的组和一个属于该组的新用户 user USER 语句对后续所有指令及从该镜像启动容器时均有效。若需先执行需要 root 权限的操作(如安装软件),可推迟 USER 指令。

3.2 使用 gosu 而非 sudo

sudo 是执行命令的传统工具,但在入口点脚本中使用有副作用。例如,在 Ubuntu 容器中运行 sudo ps aux 会有两个进程( sudo 和执行的命令):

$ docker run --rm ubuntu:trusty sudo ps aux
USER       PID %CPU ... COMMAND
root         1  0.0     sudo ps aux
root         5  0.0     ps aux

而在安装了 gosu 的 Ubuntu 镜像中运行 gosu root ps aux 只有一个进程,且命令作为 PID 1 运行,能正确接收发送到容器的信号:

$ docker run --rm amouat/ubuntu-with-gosu \
                          gosu root ps aux
USER       PID %CPU ... COMMAND
root         1  0.0     ps aux

若应用程序坚持以 root 身份运行且无法修复,可考虑使用 sudo 、SELinux 和 fakeroot 等工具约束进程。

3.3 限制容器网络

容器在生产环境中应仅开放所需端口,且这些端口应仅对需要访问的其他容器开放。默认情况下,容器无论端口是否明确发布或暴露都能相互通信。可通过以下示例说明:

$ docker run --name nc-test -d amouat/network-utils nc -l 5001 
$ docker run \
   -e IP=$(docker inspect -f {{.NetworkSettings.IPAddress}} nc-test) \
   amouat/network-utils sh -c 'echo -n "hello" | nc -v $IP 5001' 

第二个容器能连接到 nc - test ,即便没有发布或暴露端口。可通过 --icc=false 标志运行 Docker 守护进程来关闭容器间通信,防止受攻击的容器攻击其他容器,但显式链接的容器仍可通信。

例如,在 Ubuntu 上配置 Docker 守护进程:

$ cat /etc/default/docker | grep DOCKER_OPTS=
DOCKER_OPTS="--iptables=true --icc=false" 
$ docker run --name nc-test -d --expose 5001 amouat/network-utils nc -l 5001
$ docker run \
    -e IP=$(docker inspect -f {{.NetworkSettings.IPAddress}} nc-test)
    amouat/network-utils sh -c 'echo -n "hello" | nc -w 2 -v $IP 5001' 
$ docker run \
   --link nc-test:nc-test \
   amouat/network-utils sh -c 'echo -n "hello" | nc -w 2 -v nc-test 5001'

第一个连接会失败,因为容器间通信关闭且无链接;第二个命令成功,因为添加了链接。发布端口到主机时,Docker 默认发布到所有接口( 0.0.0.0 ),可显式指定要绑定的接口,如:

$ docker run -p 87.245.78.43:8080:8080 -d myimage

这能通过仅允许来自指定接口的流量来减少攻击面。

3.4 移除 Setuid/Setgid 二进制文件

应用程序可能不需要 setuid setgid 二进制文件,禁用或移除这些文件可防止它们被用于权限提升攻击。可使用以下命令获取镜像中此类二进制文件的列表:

$ docker run debian find / -perm +6000 -type f -exec ls -ld {} \; 2> /dev/null

使用 chmod a - s 移除 suid 位,例如创建一个去除危险的 Debian 镜像:

FROM debian:wheezy
RUN find / -perm +6000 -type f -exec chmod a-s {} \; || true 

构建并运行:

$ docker build -t defanged-debian .
$ docker run --rm defanged-debian \
  find / -perm +6000 -type f -exec ls -ld {} \; 2> /dev/null | wc -l
0

通常 Dockerfile 比应用程序更依赖 setuid/setgid 二进制文件,可在相关调用之后、更改用户之前执行此步骤(若应用程序以 root 权限运行,移除 setuid 二进制文件无意义)。

3.5 限制内存

限制内存可防止 DoS 攻击和内存泄漏的应用程序逐渐耗尽主机内存,此类应用程序可自动重启以维持服务水平。使用 docker run -m --memory - swap 标志限制容器使用的内存和交换内存。 --memory - swap 参数设置的是总内存(内存 + 交换内存),而非仅交换内存。默认无限制,若使用 -m 标志但未使用 --memory - swap ,则 --memory - swap 会设置为 -m 参数的两倍。示例如下:

$ docker run -m 128m --memory-swap 128m amouat/stress \
     stress --vm 1 --vm-bytes 127m -t 5s 
$ docker run -m 128m --memory-swap 128m amouat/stress \
     stress --vm 1 --vm-bytes 130m -t 5s 
$ docker run -m 128m amouat/stress \
     stress --vm 1 --vm-bytes 255m -t 5s 

第一个命令请求 127 MB 内存,可成功运行;第二个命令请求 130 MB 内存,会失败;第三个命令请求 255 MB 内存,因 --swap - memory 默认设为 256 MB 而成功。

3.6 限制 CPU

若攻击者使一个或一组容器占用主机所有 CPU,会导致其他容器资源匮乏,引发 DoS 攻击。在 Docker 中,CPU 份额由相对权重决定,默认值为 1024,即默认情况下所有容器获得相等的 CPU 份额。例如:

$ docker run -d --name load1 -c 2048 amouat/stress
$ docker run -d --name load2 amouat/stress
$ docker run -d --name load3 -c 512 amouat/stress
$ docker run -d --name load4 -c 512 amouat/stress
$ docker stats $(docker inspect -f {{.Name}} $(docker ps -q))

在这个例子中, load1 权重为 2048, load2 为默认权重 1024, load3 load4 权重为 512。在 8 核机器上, load1 约获得一半 CPU, load2 获得四分之一, load3 load4 各获得八分之一。若只有一个容器运行,它可获取所需的所有资源。

也可使用完全公平调度器(CFS)通过 --cpu - period --cpu - quota 标志共享 CPU。容器在给定周期内有设定的 CPU 配额(以微秒为单位),若超过配额,需等待下一个周期才能继续执行,例如:

$ docker run -d --cpu-period=50000 --cpu-quota=25000 myimage

通过遵循上述镜像构建和安全部署的方法,可以提高 Docker 容器的安全性、稳定性和可维护性,确保应用程序在容器环境中可靠运行。

4. 总结与对比

4.1 镜像构建规则对比

规则 描述 示例 优点
指定 FROM 指令的标签 避免拉取最新标签带来的版本不确定性 FROM redis@sha256:3479bbcab384fa343b52743b933661335448f8166203688006... 确保每次拉取相同镜像,防止意外损坏或篡改
安装软件时提供版本号 明确软件版本,减少依赖变化带来的问题 apt-get install cowsay=3.03+dfsg1 - 6 构建失败可给出旧包移除警告,可使用工具锁定版本
验证从互联网下载的软件或数据 使用校验和或加密签名保证下载内容安全 官方 Node.js 镜像使用 GPG 验证 防止意外损坏和攻击者篡改,尤其是 HTTP 传输时

4.2 安全部署技巧对比

技巧 操作 作用 示例
设置用户 创建非特权用户并切换 防止攻击者获得容器和主机的 root 权限 RUN groupadd -r user_grp && useradd -r -g user_grp user<br>USER user
使用 gosu 而非 sudo 使用 gosu 执行命令 避免 sudo 在入口点脚本的副作用,确保命令正确接收信号 $ docker run --rm amouat/ubuntu-with-gosu<br> gosu root ps aux
限制容器网络 使用 --icc=false 关闭容器间通信,显式指定绑定接口 防止受攻击容器攻击其他容器,减少攻击面 $ docker run -p 87.245.78.43:8080:8080 -d myimage
移除 Setuid/Setgid 二进制文件 使用 chmod a - s 移除 suid 防止权限提升攻击 FROM debian:wheezy<br>RUN find / -perm +6000 -type f -exec chmod a-s {} \; || true
限制内存 使用 -m --memory - swap 标志 防止 DoS 攻击和内存泄漏耗尽主机内存 $ docker run -m 128m --memory-swap 128m amouat/stress<br> stress --vm 1 --vm-bytes 127m -t 5s
限制 CPU 设置相对权重或使用 CFS 调度器 防止单个容器或容器组占用所有 CPU 资源导致 DoS 攻击 $ docker run -d --cpu-period=50000 --cpu-quota=25000 myimage

4.3 操作流程总结

以下是一个 mermaid 流程图展示从镜像构建到安全部署的主要流程:

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;

    A([开始]):::startend --> B(编写 Dockerfile):::process
    B --> C{是否需要下载软件}:::process
    C -->|是| D(验证下载软件):::process
    C -->|否| E(设置非特权用户):::process
    D --> E
    E --> F(移除 Setuid/Setgid 二进制文件):::process
    F --> G(构建镜像):::process
    G --> H(运行容器):::process
    H --> I{是否需要网络通信}:::process
    I -->|是| J(限制容器网络):::process
    I -->|否| K(限制内存和 CPU):::process
    J --> K
    K --> L([结束]):::startend

5. 常见问题及解决方法

5.1 镜像构建问题

  • 问题 :使用 FROM 指令拉取镜像时版本不一致。
    • 解决方法 :使用摘要指定精确的镜像版本,如 FROM redis@sha256:3479bbcab384fa343b52743b933661335448f8166203688006...
  • 问题 :安装软件时依赖项版本变化导致构建失败。
    • 解决方法 :提供软件版本号,并使用如 aptly 等工具对仓库进行快照。

5.2 安全部署问题

  • 问题 :容器以 root 身份运行,存在安全风险。
    • 解决方法 :在 Dockerfile 中创建非特权用户并使用 USER 语句切换,或在入口点脚本中使用 gosu 切换用户。
  • 问题 :容器间网络通信不受控制,可能导致攻击传播。
    • 解决方法 :使用 --icc=false 关闭容器间通信,仅对需要的容器进行显式链接。
  • 问题 :容器占用过多内存或 CPU 资源,影响其他容器运行。
    • 解决方法 :使用 -m --memory - swap 标志限制内存,通过设置相对权重或使用 --cpu - period --cpu - quota 标志限制 CPU。

6. 最佳实践建议

6.1 镜像构建最佳实践

  • 始终遵循可重现构建的规则,确保每次构建的镜像一致。
  • 及时更新官方镜像的基础版本,以获取安全补丁和性能优化。
  • 对下载的软件和数据进行严格验证,确保其安全性。

6.2 安全部署最佳实践

  • 避免在生产环境中以 root 身份运行容器,使用非特权用户。
  • 定期检查和移除不必要的 Setuid/Setgid 二进制文件。
  • 根据应用程序的需求合理限制容器的内存和 CPU 资源。

6.3 自动化与监控

  • 利用 update.sh 等脚本自动化镜像构建和部署过程,减少人工操作带来的错误。
  • 建立监控系统,实时监测容器的资源使用情况和网络通信,及时发现并处理异常情况。

通过以上的总结、对比、问题解决和最佳实践建议,能够帮助开发者更好地理解和应用 Docker 镜像构建和安全部署的相关知识,提高 Docker 应用的安全性和稳定性。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值