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切换用户。
-
解决方法
:在 Dockerfile 中创建非特权用户并使用
-
问题
:容器间网络通信不受控制,可能导致攻击传播。
-
解决方法
:使用
--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 应用的安全性和稳定性。
超级会员免费看
91

被折叠的 条评论
为什么被折叠?



