签署容器镜像的动机是只信任专门的镜像提供者以减轻中间人 (MITM) 攻击或对容器注册表的攻击。签署图像的一种方法是使用 GNU Privacy Guard ( GPG ) 密钥。这种技术通常与任何符合 OCI 的容器注册表兼容,例如。值得一提的是,OpenShift 集成容器注册表开箱即用地支持这种签名机制,这使得单独的签名存储变得不必要。
从技术角度来看,我们可以利用 Podman 对镜像进行签名,然后再将其推送到远程注册表。之后,所有运行 Podman 的系统都必须配置为从远程服务器检索签名,远程服务器可以是任何简单的 Web 服务器。这意味着在图像拉取操作期间,每个未签名的图像都将被拒绝。但这是如何工作的?
首先,我们必须创建一个 GPG 密钥对或选择一个已经在本地可用的密钥对。要生成新的 GPG 密钥,只需运行gpg --full-gen-key并按照交互式对话框操作。现在我们应该能够验证密钥在本地是否存在:
> gpg --list-keys sgrunert@ pub rsa2048 2018-11-26 [SC] [expires: 2020-11-25] XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX uid [ultimate] Sascha Grunert <sgrunert@> sub rsa2048 2018-11-26 [E] [expires: 2020-11-25]
现在让我们假设我们运行一个容器注册表。例如,我们可以简单地在本地机器上启动一个:
sudo podman run -d -p 5000:5000 docker.io/registry
-
[root@localhost ~]# podman run -d -p 5000:5000 /registry
-
Trying to pull /library/registry:latest...
-
Getting image source signatures
-
Copying blob 5299e6f78605 skipped: already exists
-
Copying blob 213ec9aee27d skipped: already exists
-
Copying blob 44c4c74a95e4 done
-
Copying blob 4c2fb79b7ce6 done
-
Copying blob 74a97d2d84d9 done
-
Copying config 3a0f7b0a13 done
-
Writing manifest to image destination
-
Storing signatures
-
ddfb8e13c6abe86269371114ed12f79961d69c393e5a8c9e779b60af24696cf6
-
[root@localhost ~]#
注册表对镜像签名一无所知,它只是为容器镜像提供远程存储。这意味着如果我们想要对图像进行签名,我们必须注意如何分发签名。
alpine让我们为我们的签名实验选择一个标准图像:
-
[root@localhost ~]# podman pull docker:///alpine:latest
-
Trying to pull /library/alpine:latest...
-
Getting image source signatures
-
Copying blob 213ec9aee27d skipped: already exists
-
Copying config 9c6f072447 done
-
Writing manifest to image destination
-
Storing signatures
-
9c6f0724472873bb50a2ae67a9e7adcb57673a183cea8b06eb778dca859181b5
-
[root@localhost ~]#
-
-
-
[root@localhost ~]# podman images alpine
-
REPOSITORY TAG IMAGE ID CREATED SIZE
-
/library/alpine latest 9c6f07244728 6 days ago 5.83 MB
-
[root@localhost ~]#
现在我们可以重新标记图像以将其指向我们的本地注册表:
-
[root@localhost ~]# podman tag alpine localhost:5000/alpine
-
-
[root@localhost ~]# podman images alpine
-
REPOSITORY TAG IMAGE ID CREATED SIZE
-
/library/alpine latest 9c6f07244728 6 days ago 5.83 MB
-
localhost:5000/alpine latest 9c6f07244728 6 days ago 5.83 MB
Podman 现在可以通过一个命令推送图像并对其进行签名。但是要让它工作,我们必须在以下位置修改我们的系统范围的注册表配置
/etc/containers//:
-
[root@localhost ~]# cd /etc/containers/
-
[root@localhost containers]# ls
-
oci
-
[root@localhost containers]# cd
-
[root@localhost ]# ls
-
default.yaml registry.access.
-
[root@localhost ]# vim default.yaml
-
-
-
-
# This is the default signature write location for docker registries.
-
default-docker:
-
# sigstore: file:///var/lib/containers/sigstore
-
sigstore: http://localhost:8000 # Added by us
-
sigstore-staging: file:///var/lib/containers/sigstore
我们可以看到我们配置了两个签名存储:
- sigstore: 引用 Web 服务器进行签名读取
- sigstore-staging: 引用文件路径进行签名写入
现在,让我们推送并签署图像:
-
sudo -E GNUPGHOME=$HOME/.gnupg \ #普通用户需要加,管理员不需要
-
-
podman push \
-
--tls-verify=false \
-
--sign-by sgrunert@ \
-
localhost:5000/alpine
-
…
-
Storing signatures
-
-
-
-
-
-
[root@localhost ~]# gpg --full-gen-key
-
gpg (GnuPG) 2.2.20; Copyright (C) 2020 Free Software Foundation, Inc.
-
This is free software: you are free to change and redistribute it.
-
There is NO WARRANTY, to the extent permitted by law.
-
-
Please select what kind of key you want:
-
(1) RSA and RSA (default)
-
(2) DSA and Elgamal
-
(3) DSA (sign only)
-
(4) RSA (sign only)
-
(14) Existing key from card
-
Your selection?
-
RSA keys may be between 1024 and 4096 bits long.
-
What keysize do you want? (2048)
-
Requested keysize is 2048 bits
-
Please specify how long the key should be valid.
-
0 = key does not expire
-
<n> = key expires in n days
-
<n>w = key expires in n weeks
-
<n>m = key expires in n months
-
<n>y = key expires in n years
-
Key is valid for? (0)
-
Key does not expire at all
-
Is this correct? (y/N) y
-
-
GnuPG needs to construct a user ID to identify your key.
-
-
Real name: aliyonghu
-
Email address: 1337576329@
-
Comment: jjyy
-
You selected this USER-ID:
-
"aliyonghu (jjyy) <1337576329@>"
-
-
Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O
-
We need to generate a lot of random bytes. It is a good idea to perform
-
some other action (type on the keyboard, move the mouse, utilize the
-
disks) during the prime generation; this gives the random number
-
generator a better chance to gain enough entropy.
-
We need to generate a lot of random bytes. It is a good idea to perform
-
some other action (type on the keyboard, move the mouse, utilize the
-
disks) during the prime generation; this gives the random number
-
generator a better chance to gain enough entropy.
-
gpg: key 95CF6AD881FB0B8C marked as ultimately trusted
-
gpg: directory '/root/.gnupg/' created
-
gpg: revocation certificate stored as '/root/.gnupg//'
-
public and secret key created and signed.
-
-
pub rsa2048 2022-08-16 [SC]
-
A2CF9C5375A8E8B2462F044D95CF6AD881FB0B8C
-
uid aliyonghu (jjyy) <1337576329@>
-
sub rsa2048 2022-08-16 [E]
-
-
[root@localhost ~]#
-
-
-
[root@localhost ~]# gpg --list-keys aliyonghu
-
gpg: checking the trustdb
-
gpg: marginals needed: 3 completes needed: 1 trust model: pgp
-
gpg: depth: 0 valid: 1 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 1u
-
pub rsa2048 2022-08-16 [SC]
-
A2CF9C5375A8E8B2462F044D95CF6AD881FB0B8C
-
uid [ultimate] aliyonghu (jjyy) <1337576329@>
-
sub rsa2048 2022-08-16 [E]
-
-
-
-
[root@localhost ~]# podman push --tls-verify=false --sign-by aliyonghu localhost:5000/alpine
-
Getting image source signatures
-
Copying blob 994393dc58e7 done
-
Copying config 9c6f072447 done
-
Writing manifest to image destination
-
Signing manifest
-
Storing signatures
-
如果我们现在看一下系统签名存储,我们会看到有一个新的签名可用,这是由图像推送引起的:
-
[root@localhost ~]# ls /var/lib/containers/sigstore
-
'alpine@sha256=9c3027f0149e5ed26a613a53282e2d13616f387b1327035ecb9831c27a53b447'
我们编辑的版本中的默认签名存储 /etc/containers//引用了一个正在监听的 Web 服务器 http://localhost:8000。对于我们的实验,我们只需在本地临时签名存储中启动一个新服务器:
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/) ...
-
[root@localhost ~]# dnf module list | grep python
-
libselinux-python 2.8 common Python 2 bindings for libselinux
-
python27 2.7 [d] common [d] Python programming language, version 2.7
-
python36 3.6 [d] build, common [d] Python programming language, version 3.6
-
python38 3.8 [d] build, common [d] Python programming language, version 3.8
-
python39 3.9 [d] build, common [d] Python programming language, version 3.9
-
[root@localhost ~]# yum -y module install python38
-
-
-
-
[root@localhost ~]# cd /var/lib/containers/sigstore
-
[root@localhost sigstore]# ls
-
'alpine@sha256=9c3027f0149e5ed26a613a53282e2d13616f387b1327035ecb9831c27a53b447'
-
[root@localhost sigstore]# python3 -m
-
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
-
-
可以看出8000端口有了
-
[root@localhost ~]# ss -anlt
-
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
-
LISTEN 0 5 0.0.0.0:8000 0.0.0.0:*
-
LISTEN 0 128 0.0.0.0:5000 0.0.0.0:*
-
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
-
LISTEN 0 128 [::]:22 [::]:*
-
[root@localhost ~]#
-
[root@localhost ~]#
-
-
-
在防火墙在添加规则
-
[root@localhost ~]# firewall-cmd --add-rich-rule 'rule family=ipv4 source address=192.168.119.0/24 port port=8000 protocol=tcp accept' --permanent
-
success
-
[root@localhost ~]# firewall-cmd --reload
-
success
-
[root@localhost ~]#
可以成功访问
让我们删除本地图像以进行验证测试:
-
[root@localhost ~]# podman rmi /alpine localhost:5000/alpine
-
Untagged: /library/alpine:latest
-
Untagged: localhost:5000/alpine:latest
-
Deleted: 9c6f0724472873bb50a2ae67a9e7adcb57673a183cea8b06eb778dca859181b5
我们必须编写一个策略来强制签名必须是有效的。这可以通过在 中添加新规则来完成/etc/containers/。从下面的示例中,将"docker"条目复制到"transports"您的 .
-
{
-
"default": [{ "type": "insecureAcceptAnything" }],
-
"transports": {
-
"docker": {
-
"localhost:5000": [
-
{
-
"type": "signedBy",
-
"keyType": "GPGKeys",
-
"keyPath": "/tmp/"
-
}
-
]
-
}
-
}
-
}
keyPath
尚不存在,因此我们必须将 GPG 密钥放在那里:
gpg --output /tmp/key.gpg --armor --export aliyonghu
如果我们现在拉取图像:
-
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/
:
-
gpg --output /tmp/key.gpg --armor --export mail@saschagrunert.de
-
File '/tmp/' exists. Overwrite? (y/N) y
然后不再可能拉动:
-
sudo podman pull --tls-verify=false localhost:5000/alpine
-
Trying to pull localhost:5000/alpine...
-
Error: 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//*.yaml
文件中配置 Web 服务器 - 每个图像拉取系统都必须配置为包含强制策略配置
这就是图像签名和 GPG 的内容。很酷的是,这个设置也可以与CRI-O一起使用,并且可以用于在 Kubernetes 环境中对容器镜像进行签名。