跳到主要内容

如何使用 Podman 签名和分发容器镜像

如何使用 Podman 签名和分发容器镜像

对容器镜像进行签名源自于仅信任专门的镜像提供者以缓解中间人(MITM)攻击或对容器注册中心的攻击的动机。签名镜像的一种方法是使用 GNU Privacy Guard(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 # 我们添加的
sigstore-staging: file:///var/lib/containers/sigstore

我们可以看到已经配置了两个签名存储:

  • sigstore:引用一个用于读取签名的Web服务器
  • 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 签名容器镜像时需要考虑以下四个主要方面:

  1. 我们需要在签名机器上有一个有效的私有 GPG 密钥,并在每个可能拉取镜像的系统上放置相应的公钥
  2. 必须有一个可以访问签名存储的 Web 服务器正在运行
  3. 必须在任何 /etc/containers/registries.d/*.yaml 文件中配置 Web 服务器
  4. 每个拉取镜像的系统都必须通过 policy.conf 配置为包含强制策略配置

这就是关于镜像签名和 GPG 的全部内容。值得一提的是,这个设置同样可以即插即用地与 CRI-O 配合使用,并且可以用于在 Kubernetes 环境中签名容器镜像。