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

时间:2024-10-25 10:06:16

签署容器镜像的动机是只信任专门的镜像提供者以减轻中间人 (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

  1. [root@localhost ~]# podman run -d -p 5000:5000 /registry
  2. Trying to pull /library/registry:latest...
  3. Getting image source signatures
  4. Copying blob 5299e6f78605 skipped: already exists
  5. Copying blob 213ec9aee27d skipped: already exists
  6. Copying blob 44c4c74a95e4 done
  7. Copying blob 4c2fb79b7ce6 done
  8. Copying blob 74a97d2d84d9 done
  9. Copying config 3a0f7b0a13 done
  10. Writing manifest to image destination
  11. Storing signatures
  12. ddfb8e13c6abe86269371114ed12f79961d69c393e5a8c9e779b60af24696cf6
  13. [root@localhost ~]#

注册表对镜像签名一无所知,它只是为容器镜像提供远程存储。这意味着如果我们想要对图像进行签名,我们必须注意如何分发签名。

alpine让我们为我们的签名实验选择一个标准图像:

  1. [root@localhost ~]# podman pull docker:///alpine:latest
  2. Trying to pull /library/alpine:latest...
  3. Getting image source signatures
  4. Copying blob 213ec9aee27d skipped: already exists
  5. Copying config 9c6f072447 done
  6. Writing manifest to image destination
  7. Storing signatures
  8. 9c6f0724472873bb50a2ae67a9e7adcb57673a183cea8b06eb778dca859181b5
  9. [root@localhost ~]#
  10. [root@localhost ~]# podman images alpine
  11. REPOSITORY TAG IMAGE ID CREATED SIZE
  12. /library/alpine latest 9c6f07244728 6 days ago 5.83 MB
  13. [root@localhost ~]#

现在我们可以重新标记图像以将其指向我们的本地注册表:

  1. [root@localhost ~]# podman tag alpine localhost:5000/alpine
  2. [root@localhost ~]# podman images alpine
  3. REPOSITORY TAG IMAGE ID CREATED SIZE
  4. /library/alpine latest 9c6f07244728 6 days ago 5.83 MB
  5. localhost:5000/alpine latest 9c6f07244728 6 days ago 5.83 MB

Podman 现在可以通过一个命令推送图像并对其进行签名。但是要让它工作,我们必须在以下位置修改我们的系统范围的注册表配置

/etc/containers//:

  1. [root@localhost ~]# cd /etc/containers/
  2. [root@localhost containers]# ls
  3. oci
  4. [root@localhost containers]# cd
  5. [root@localhost ]# ls
  6. default.yaml registry.access.
  7. [root@localhost ]# vim default.yaml
  8. # This is the default signature write location for docker registries.
  9. default-docker:
  10. # sigstore: file:///var/lib/containers/sigstore
  11. sigstore: http://localhost:8000 # Added by us
  12. sigstore-staging: file:///var/lib/containers/sigstore

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

  • sigstore: 引用 Web 服务器进行签名读取
  • sigstore-staging: 引用文件路径进行签名写入

现在,让我们推送并签署图像:

  1. sudo -E GNUPGHOME=$HOME/.gnupg \ #普通用户需要加,管理员不需要
  2. podman push \
  3. --tls-verify=false \
  4. --sign-by sgrunert@ \
  5. localhost:5000/alpine
  6. Storing signatures
  7. [root@localhost ~]# gpg --full-gen-key
  8. gpg (GnuPG) 2.2.20; Copyright (C) 2020 Free Software Foundation, Inc.
  9. This is free software: you are free to change and redistribute it.
  10. There is NO WARRANTY, to the extent permitted by law.
  11. Please select what kind of key you want:
  12. (1) RSA and RSA (default)
  13. (2) DSA and Elgamal
  14. (3) DSA (sign only)
  15. (4) RSA (sign only)
  16. (14) Existing key from card
  17. Your selection?
  18. RSA keys may be between 1024 and 4096 bits long.
  19. What keysize do you want? (2048)
  20. Requested keysize is 2048 bits
  21. Please specify how long the key should be valid.
  22. 0 = key does not expire
  23. <n> = key expires in n days
  24. <n>w = key expires in n weeks
  25. <n>m = key expires in n months
  26. <n>y = key expires in n years
  27. Key is valid for? (0)
  28. Key does not expire at all
  29. Is this correct? (y/N) y
  30. GnuPG needs to construct a user ID to identify your key.
  31. Real name: aliyonghu
  32. Email address: 1337576329@
  33. Comment: jjyy
  34. You selected this USER-ID:
  35. "aliyonghu (jjyy) <1337576329@>"
  36. Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O
  37. We need to generate a lot of random bytes. It is a good idea to perform
  38. some other action (type on the keyboard, move the mouse, utilize the
  39. disks) during the prime generation; this gives the random number
  40. generator a better chance to gain enough entropy.
  41. We need to generate a lot of random bytes. It is a good idea to perform
  42. some other action (type on the keyboard, move the mouse, utilize the
  43. disks) during the prime generation; this gives the random number
  44. generator a better chance to gain enough entropy.
  45. gpg: key 95CF6AD881FB0B8C marked as ultimately trusted
  46. gpg: directory '/root/.gnupg/' created
  47. gpg: revocation certificate stored as '/root/.gnupg//'
  48. public and secret key created and signed.
  49. pub rsa2048 2022-08-16 [SC]
  50. A2CF9C5375A8E8B2462F044D95CF6AD881FB0B8C
  51. uid aliyonghu (jjyy) <1337576329@>
  52. sub rsa2048 2022-08-16 [E]
  53. [root@localhost ~]#
  54. [root@localhost ~]# gpg --list-keys aliyonghu
  55. gpg: checking the trustdb
  56. gpg: marginals needed: 3 completes needed: 1 trust model: pgp
  57. gpg: depth: 0 valid: 1 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 1u
  58. pub rsa2048 2022-08-16 [SC]
  59. A2CF9C5375A8E8B2462F044D95CF6AD881FB0B8C
  60. uid [ultimate] aliyonghu (jjyy) <1337576329@>
  61. sub rsa2048 2022-08-16 [E]
  62. [root@localhost ~]# podman push --tls-verify=false --sign-by aliyonghu localhost:5000/alpine
  63. Getting image source signatures
  64. Copying blob 994393dc58e7 done
  65. Copying config 9c6f072447 done
  66. Writing manifest to image destination
  67. Signing manifest
  68. Storing signatures

如果我们现在看一下系统签名存储,我们会看到有一个新的签名可用,这是由图像推送引起的:

  1. [root@localhost ~]# ls /var/lib/containers/sigstore
  2. '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/) ...

  1. [root@localhost ~]# dnf module list | grep python
  2. libselinux-python 2.8 common Python 2 bindings for libselinux
  3. python27 2.7 [d] common [d] Python programming language, version 2.7
  4. python36 3.6 [d] build, common [d] Python programming language, version 3.6
  5. python38 3.8 [d] build, common [d] Python programming language, version 3.8
  6. python39 3.9 [d] build, common [d] Python programming language, version 3.9
  7. [root@localhost ~]# yum -y module install python38
  8. [root@localhost ~]# cd /var/lib/containers/sigstore
  9. [root@localhost sigstore]# ls
  10. 'alpine@sha256=9c3027f0149e5ed26a613a53282e2d13616f387b1327035ecb9831c27a53b447'
  11. [root@localhost sigstore]# python3 -m
  12. Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
  13. 可以看出8000端口有了
  14. [root@localhost ~]# ss -anlt
  15. State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
  16. LISTEN 0 5 0.0.0.0:8000 0.0.0.0:*
  17. LISTEN 0 128 0.0.0.0:5000 0.0.0.0:*
  18. LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
  19. LISTEN 0 128 [::]:22 [::]:*
  20. [root@localhost ~]#
  21. [root@localhost ~]#
  22. 在防火墙在添加规则
  23. [root@localhost ~]# firewall-cmd --add-rich-rule 'rule family=ipv4 source address=192.168.119.0/24 port port=8000 protocol=tcp accept' --permanent
  24. success
  25. [root@localhost ~]# firewall-cmd --reload
  26. success
  27. [root@localhost ~]#

可以成功访问

让我们删除本地图像以进行验证测试:

  1. [root@localhost ~]# podman rmi /alpine localhost:5000/alpine
  2. Untagged: /library/alpine:latest
  3. Untagged: localhost:5000/alpine:latest
  4. Deleted: 9c6f0724472873bb50a2ae67a9e7adcb57673a183cea8b06eb778dca859181b5

我们必须编写一个策略来强制签名必须是有效的。这可以通过在 中添加新规则来完成/etc/containers/。从下面的示例中,将"docker"条目复制到"transports"您的 .

  1. {
  2. "default": [{ "type": "insecureAcceptAnything" }],
  3. "transports": {
  4. "docker": {
  5. "localhost:5000": [
  6. {
  7. "type": "signedBy",
  8. "keyType": "GPGKeys",
  9. "keyPath": "/tmp/"
  10. }
  11. ]
  12. }
  13. }
  14. }

 

 

keyPath尚不存在,因此我们必须将 GPG 密钥放在那里:

gpg --output /tmp/key.gpg --armor --export aliyonghu

如果我们现在拉取图像:

  1. sudo podman pull --tls-verify=false localhost:5000/alpine
  2. Storing signatures
  3. e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a

然后我们可以在web服务器的日志中看到签名被访问过:

127.0.0.1 - - [04/Mar/2020 11:18:21] "GET /alpine@sha256=e9b65ef660a3ff91d28cc50eba84f21798a6c5c39b4dd165047db49e84ae1fb9/signature-1 HTTP/1.1" 200 -

作为一个对应的例子,如果我们在 处指定了错误的键/tmp/

  1. gpg --output /tmp/key.gpg --armor --export mail@saschagrunert.de
  2. File '/tmp/' exists. Overwrite? (y/N) y

然后不再可能拉动:

  1. sudo podman pull --tls-verify=false localhost:5000/alpine
  2. Trying to pull localhost:5000/alpine...
  3. 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 对容器镜像进行签名时,通常需要考虑四个主要事项:

  1. 我们需要签名机器上的有效 GPG 私钥和每个系统上的相应公钥,这将拉取图像
  2. Web 服务器必须在可以访问签名存储的地方运行
  3. 必须在任何 /etc/containers//*.yaml文件中配置 Web 服务器
  4. 每个图像拉取系统都必须配置为包含强制策略配置

这就是图像签名和 GPG 的内容。很酷的是,这个设置也可以与CRI-O一起使用,并且可以用于在 Kubernetes 环境中对容器镜像进行签名。