对容器镜像进行签名源于这样的动机:仅信任专用镜像提供商,以减轻中间人攻击(MITM)或针对容器注册表的攻击。签名镜像的一种方式是使用 GNU 隐私卫士(GPG)密钥。该技术通常兼容任何符合 OCI 标准的容器注册库(如 Quay.io)。值得一提的是,OpenShift 集成容器注册库开箱即支持此签名机制,从而无需单独存储签名。
从技术角度来看,我们可以在将镜像推送到远程注册表之前使用Podman对其进行签名。之后,所有运行Podman的系统都必须配置为从远程服务器获取签名,该服务器可以是任何简单的Web服务器。这意味着在镜像拉取操作过程中,所有未签名的镜像都将被拒绝。但具体如何实现呢?
首先,我们需要创建一个GPG密钥对或选择本地已有的密钥。要生成新的GPG密钥,只需运行gpg --full-gen-key并按照交互式提示操作。现在我们应该能够验证该密钥是否存在于本地:
> gpg --list-keys sgrunert@suse.com pub rsa2048 2018-11-26 [SC] [expires: 2020-11-25] XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX uid [ultimate] Sascha Grunert <sgrunert@suse.com> sub rsa2048 2018-11-26 [E] [expires: 2020-11-25]
现在让我们假设我们运行着一个容器注册表。例如,我们可以简单地在本地机器上启动一个:
sudo podman run -d -p 5000:5000 docker.io/registry
注册表并不了解镜像签名机制,它仅为容器镜像提供远程存储服务。这意味着若需对镜像进行签名,我们必须自行处理签名的分发方式。
让我们选择标准的 Alpine 镜像进行签名实验:
sudo podman pull docker://docker.io/alpine:latest sudo podman images alpine REPOSITORY TAG IMAGE ID CREATED SIZE docker.io/library/alpine latest e7d92cdc71fe 6 weeks ago 5.86 MB
现在我们可以重新标记该镜像,使其指向我们的本地注册表:
sudo podman tag alpine localhost:5000/alpine sudo podman images alpine REPOSITORY TAG IMAGE ID CREATED SIZE localhost:5000/alpine latest e7d92cdc71fe 6 weeks ago 5.86 MB docker.io/library/alpine latest e7d92cdc71fe 6 weeks ago 5.86 MB
现在 Podman 能够通过单条命令推送镜像并进行签名。但要实现这一功能,我们需要修改系统范围的注册表配置文件 /etc/containers/registries.d/default.yaml:
default-docker: sigstore: http://localhost:8000 # Added by us sigstore-staging: file:///var/lib/containers/sigstore
我们可以看到已配置了两个签名存储库:
sigstore: 引用一个用于读取签名的网络服务器
sigstore-staging: 引用一个用于写入签名的文件路径
现在,让我们推送并签名这个镜像:
sudo -E GNUPGHOME=$HOME/.gnupg \ podman push \ --tls-verify=false \ --sign-by sgrunert@suse.com \ localhost:5000/alpine … Storing signatures
如果我们现在查看系统的签名存储,会发现有一个新的签名可用,这是由镜像推送导致的:
sudo ls /var/lib/containers/sigstore 'alpine@sha256=e9b65ef660a3ff91d28cc50eba84f21798a6c5c39b4dd165047db49e84ae1fb9'
在我们编辑的 /etc/containers/registries.d/default.yaml 文件中,默认签名存储库引用了一个监听 http://localhost:8000的Web 服务器。为进行实验,我们只需在本地暂存签名存储库内启动一个新服务器:
sudo bash -c 'cd /var/lib/containers/sigstore && python3 -m http.server' Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
让我们为验证测试删除本地镜像:
sudo podman rmi docker.io/alpine localhost:5000/alpine
我们需要制定一项策略来强制要求签名必须有效。这可以通过在 /etc/containers/policy.json 中添加新规则来实现。请从下方示例中将“docker”条目复制到您 policy.json 文件的“transports”部分。
{
"default": [{ "type": "insecureAcceptAnything" }],
"transports": {
"docker": {
"localhost:5000": [
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "/tmp/key.gpg"
}
]
}
}
}keyPath 目前尚不存在,因此我们必须将GPG密钥放在那里:
gpg --output /tmp/key.gpg --armor --export sgrunert@suse.com
如果我们现在拉取该镜像:
sudo podman pull --tls-verify=false localhost:5000/alpine … Storing signatures e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a
然后我们可以在 Web 服务器的日志中看到签名已被访问:
127.0.0.1 - - [04/Mar/2020 11:18:21] "GET /alpine@sha256=e9b65ef660a3ff91d28cc50eba84f21798a6c5c39b4dd165047db49e84ae1fb9/signature-1 HTTP/1.1" 200 -
作为对应示例,如果我们在 /tmp/key.gpg 中指定了错误的密钥:
gpg --output /tmp/key.gpg --armor --export mail@saschagrunert.de File '/tmp/key.gpg' exists. Overwrite? (y/N) y
那么就再也无法进行拉取操作了:
sudo podman pull --tls-verify=false localhost:5000/alpine Trying to pull localhost:5000/alpine... Error: pulling image "localhost:5000/alpine": unable to pull localhost:5000/alpine: unable to pull image: Source image rejected: Invalid GPG signature: …
因此,使用 Podman 和 GPG 对容器镜像进行签名时,通常需要考虑以下四个主要方面:
签名机器上需配置有效的私有 GPG 密钥,且所有将拉取镜像的系统都需部署对应的公钥
需在某处运行 Web 服务器,该服务器需具备访问签名存储库的权限
需在任意 /etc/containers/registries.d/*.yaml 配置文件中设置 Web 服务器
所有拉取镜像的系统都需通过 policy.conf 文件配置强制执行策略
关于镜像签名和GPG的设置就这些。值得一提的是,该方案同样能开箱即用支持 CRI-O,并可用于在 Kubernetes 环境中签名容器镜像。