Podman签署和分发容器镜像
签署容器镜像的动机是只信任专门的镜像提供者以减轻中间人 (MITM) 攻击或对容器注册表的攻击。签署镜像的一种方法是使用 GNU Privacy Guard ( GPG ) 密钥。这种技术通常与任何符合 OCI 的容器注册表兼容,例如:Quay.io。值得一提的是,OpenShift 集成容器注册表开箱即用地支持这种签名机制,这使得单独的签名存储变得不必要。
从技术角度来看,我们可以利用 Podman 对镜像进行签名,然后再将其推送到远程注册表。之后,所有运行 Podman 的系统都必须配置为从远程服务器检索签名,远程服务器可以是任何简单的 Web 服务器。这意味着在镜像拉取操作期间,每个未签名的镜像都将被拒绝。
在使用 Podman 和 GPG 对容器镜像进行签名时,通常需要考虑四个主要事项:
- 我们需要签名机器上的有效 GPG 私钥和每个系统上的相应公钥,这将拉取镜像
- Web 服务器必须在可以访问签名存储的地方运行
- 必须在任何
/etc/containers/registries.d/*.yaml
文件中配置 Web 服务器 - 每个镜像拉取系统都必须配置为包含强制策略配置
policy.conf
生成GPG密钥
//生成GPG密钥
[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.
gpg: directory '/root/.gnupg' created
gpg: keybox '/root/.gnupg/pubring.kbx' created
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 #按y该密钥不会过期
GnuPG needs to construct a user ID to identify your key.
Real name: guguniao #用户id
Email address: yf1121@163.com #邮箱
Comment: xxxx #描述
You selected this USER-ID:
"guguniao (xxxx) <yf1121@163.com>"
Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? o #按O提交
//第一个方框是设置给密钥设置密码,第二个是确认密码。
┌──────────────────────────────────────────────────────┐
│ Please enter the passphrase to │
│ protect your new key │
│ │
│ Passphrase: ********________________________________ │
│ │
│ <OK> <Cancel> │
└──────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────┐
│ Please re-enter this passphrase │
│ │
│ Passphrase: ********________________________________ │
│ │
│ <OK> <Cancel> │
└──────────────────────────────────────────────────────┘
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: /root/.gnupg/trustdb.gpg: trustdb created
gpg: key 75A8BC88C9C0AC53 marked as ultimately trusted
gpg: directory '/root/.gnupg/openpgp-revocs.d' created
gpg: revocation certificate stored as '/root/.gnupg/openpgp-revocs.d/6308282BF98C9D14D7F9F85875A8BC88C9C0AC53.rev'
public and secret key created and signed.
pub rsa2048 2022-08-15 [SC]
6308282BF98C9D14D7F9F85875A8BC88C9C0AC53
uid guguniao (xxxx) <yf1121@163.com>
sub rsa2048 2022-08-15 [E]
//查看已有的密钥
[root@localhost ~]# gpg --list-keys yf1121@163.com
pub rsa2048 2022-08-15 [SC]
6308282BF98C9D14D7F9F85875A8BC88C9C0AC53
uid [ultimate] guguniao (xxxx) <yf1121@163.com>
sub rsa2048 2022-08-15 [E]
部署私有仓库
有两种方案:
- 在另一台主机部署harbor私有仓库
👉部署harbor的文章
- 直接在本机部署私有仓库,如下面操作所示
[root@localhost ~]# podman run -d -p 5000:5000 docker.io/registry
c579dd21d3addd8a181aede2c30bb9c349a5f65d3d62256f747473d7c205a8f5
[root@localhost ~]# podman container ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c579dd21d3ad docker.io/library/registry:latest /etc/docker/regis... 24 seconds ago Up 24 seconds ago 0.0.0.0:5000->5000/tcp trusting_hellman
拉取alpine镜像作为本次用作测试的镜像
//拉取镜像
[root@localhost ~]# podman pull docker://docker.io/alpine:latest
Trying to pull docker.io/library/alpine:latest...
Getting image source signatures
Copying blob 59bf1c3509f3 done
Copying config c059bfaa84 done
Writing manifest to image destination
Storing signatures
c059bfaa849c4d8e4aecaeb3a10c2d9b3d85f5165c66ad3a4d937758128c4d18
给alpine打上标签指向本地的私有仓库
[root@localhost ~]# podman tag alpine localhost:5000/alpine
为了让镜像能上传至本地的私有仓库,修改/etc/containers/registries.d/default.yaml
文件
我们可以看到我们配置了两个签名存储:
sigstore
: 引用 Web 服务器进行签名读取sigstore-staging
: 引用文件路径进行签名写入
[root@localhost ~]# vim /etc/containers/registries.d/default.yaml
default-docker:
sigstore: http://localhost:8000 #添加这一行
sigstore-staging: file:///var/lib/containers/sigstore
推送alpine镜像至本地的私有仓库
//--tls-verify是访问私有仓库时需要https验证,=false是关闭https验证。
//--sign-by是使用指定的密钥在目标处添加签名,这里填生成该密钥的邮箱
[root@localhost ~]# podman push --tls-verify=false --sign-by yf1121@163.com localhost:5000/alpine
Getting image source signatures
Copying blob 8d3ac3489996 done
Copying config c059bfaa84 [======================================] 1.4KiB / 1.4KiB
Writing manifest to image destination
Signing manifest
Storing signatures
┌────────────────────────────────────────────────────────────────┐
│ Please enter the passphrase to unlock the OpenPGP secret key: │
│ "guguniao (xxxx) <yf1121@163.com>" │
│ 2048-bit RSA key, ID 75A8BC88C9C0AC53, │
│ created 2022-08-15. │
│ │
│ │
│ Passphrase: ********__________________________________________ │
│ │
│ <OK> <Cancel> │
└────────────────────────────────────────────────────────────────┘
现在看一下系统签名存储,我们会看到有一个新的签名可用,这是由镜像推送引起的。每推送一个镜像就会生成一个签名。
[root@localhost ~]# ls /var/lib/containers/sigstore
'alpine@sha256=964248be4bb8e3052c8b411271126f70c5c5015df31e014bfc41fad50edf78d8'
前面编辑的默认签名存储 /etc/containers/registries.d/default.yaml
引用了一个正在监听的 Web 服务器 http://localhost:8000
。我们只需在本地临时签名存储中启动一个新服务器:
//下载python3,下面的命令会需要此依赖关系
[root@localhost ~]# dnf -y install python36
//启动用来读取签名的web服务器
[root@localhost ~]# 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/) ...
192.168.92.1 - - [15/Aug/2022 21:48:47] "GET / HTTP/1.1" 200 -
192.168.92.1 - - [15/Aug/2022 21:48:47] code 404, message File not found
192.168.92.1 - - [15/Aug/2022 21:48:47] "GET /favicon.ico HTTP/1.1" 404 -
删除本地镜像用来进行拉取镜像验证
[root@localhost ~]# podman rmi docker.io/alpine localhost:5000/alpine
Untagged: docker.io/library/alpine:latest
Untagged: localhost:5000/alpine:latest
Deleted: c059bfaa849c4d8e4aecaeb3a10c2d9b3d85f5165c66ad3a4d937758128c4d18
我们必须编写一个策略来强制签名必须是有效的。这可以通过在 中添加新规则来完成/etc/containers/policy.json
。
[root@localhost ~]# vim /etc/containers/policy.json
{
"default": [{ "type": "insecureAcceptAnything" }],
"transports": {
"docker": {
"localhost:5000": [ #把这一行修改为localhost:5000
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "/tmp/key.gpg" #修改为密钥所在的位置
}
]
}
}
}
这个/tmp/key.gpg位置的密钥不存在,因此我们必须将GPG密钥放在那里
[root@localhost ~]# gpg --output /tmp/key.gpg --armor --export yf1121@163.com
拉取本地私有仓库中的alpine镜像,目前也只有这一个镜像,因为我们创建这个私有仓库后只上传了这个镜像
/拉取镜像
[root@localhost ~]# podman pull --tls-verify=false localhost:5000/alpine
Trying to pull localhost:5000/alpine:latest...
Getting image source signatures
Checking if image destination supports signatures
Copying blob 3c4e9198e8c1 done
Copying config c059bfaa84 done
Writing manifest to image destination
Storing signatures
c059bfaa849c4d8e4aecaeb3a10c2d9b3d85f5165c66ad3a4d937758128c4d18
拉取镜像后我们可以在web服务器的日志中看到签名被访问过
127.0.0.1 - - [15/Aug/2022 22:00:30] "GET /alpine@sha256=964248be4bb8e3052c8b411271126f70c5c5015df31e014bfc41fad50edf78d8/signature-2 HTTP/1.1" 404 -