签署容器镜像的动机是只信任专门的镜像提供者以减轻中间人 (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]
现在让我们假设我们运行一个容器注册表。例如,我们可以简单地在本地机器上启动一个:
[root@localhost ~]# sudo podman run -d -p 5000:5000 docker.io/registry
532c7d4fd848e05fc39dc75b772e27df842f5ed5308745889c4487aadcf20f9f
[root@localhost ~]#
注册表对镜像签名一无所知,它只是为容器镜像提供远程存储。这意味着如果我们想要对图像进行签名,我们必须注意如何分发签名。
alpine让我们为我们的签名实验选择一个标准图像:
[root@localhost ~]# sudo podman pull docker://docker.io/alpine:latest
9c6f0724472873bb50a2ae67a9e7adcb57673a183cea8b06eb778dca859181b5
[root@localhost ~]#
[root@localhost ~]# sudo podman images alpine
REPOSITORY TAG IMAGE ID CREATED SIZE
docker.io/library/alpine latest 9c6f07244728 6 days ago 5.83 MB
[root@localhost ~]#
现在我们可以重新标记图像以将其指向我们的本地注册表:
[root@localhost ~]# sudo podman tag alpine localhost:5000/alpine
[root@localhost ~]# sudo podman images alpine
REPOSITORY TAG IMAGE ID CREATED SIZE
docker.io/library/alpine latest 9c6f07244728 6 days ago 5.83 MB
localhost:5000/alpine latest 9c6f07244728 6 days ago 5.83 MB
[root@localhost ~]#
Podman 现在可以通过一个命令推送图像并对其进行签名。但是要让它工作,我们必须在以下位置修改我们的系统范围的注册表配置
[root@localhost ~]# vim /etc/containers/registries.d/default.yaml
14 default-docker:
15 sigstore: sigstore: http://localhost:8000 # Added by us
16 sigstore-staging: file:///var/lib/containers/sigstore
我们可以看到我们配置了两个签名存储:
现在,让我们推送并签署图像:
sudo -E GNUPGHOME=$HOME/.gnupg \
podman push \
--tls-verify=false \
--sign-by sgrunert@suse.com \
localhost:5000/alpine
…
Storing signatures
如果我们现在看一下系统签名存储,我们会看到有一个新的签名可用,这是由图像推送引起的:
[root@localhost ~]# sudo ls /var/lib/containers/sigstore
'alpine@sha256=e9b65ef660a3ff91d28cc50eba84f21798a6c5c39b4dd165047db49e84ae1fb9'
我们编辑的版本中的默认签名存储 /etc/containers/registries.d/default.yaml引用了一个正在监听的 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 ~]# sudo podman rmi docker.io/alpine localhost:5000/alpine
我们必须编写一个策略来强制签名必须是有效的。这可以通过在 中添加新规则来完成/etc/containers/policy.json。从下面的示例中,将"docker"条目复制到"transports"您的 policy.json.
{
"default": [{ "type": "insecureAcceptAnything" }],
"transports": {
"docker": {
"localhost:5000": [
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "/tmp/key.gpg"
}
]
}
}
}
keyPath尚不存在,因此我们必须将 GPG 密钥放在那里:
[root@localhost ~]# gpg --output /tmp/key.gpg --armor --export sgrunert@suse.com
如果我们现在拉取图像:
[root@localhost ~]# sudo podman pull --tls-verify=false localhost:5000/alpine
9c6f0724472873bb50a2ae67a9e7adcb57673a183cea8b06eb778dca859181b5
然后我们可以在web服务器的日志中看到签名被访问过:
127.0.0.1 - - [04/Mar/2020 11:18:21] "GET /alpine@sha256=e9b65ef660a3ff91d28cc50eba84f21798a6c5c39b4dd165047db49e84ae1fb9/signature-1 HTTP/1.1" 200 -
作为一个对应的例子,如果我们在 处指定了错误的键/tmp/key.gpg:
[root@localhost ~]# gpg --output /tmp/key.gpg --armor --export mail@saschagrunert.de
File '/tmp/key.gpg' exists. Overwrite? (y/N) y
然后不再可能拉动:
[root@localhost ~]# 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 对容器镜像进行签名时,通常需要考虑四个主要事项:
1.我们需要签名机器上的有效 GPG 私钥和每个系统上的相应公钥,这将拉取图像
2.Web 服务器必须在可以访问签名存储的地方运行
3.必须在任何 /etc/containers/registries.d/*.yaml文件中配置 Web 服务器
4.个图像拉取系统都必须配置为包含强制策略配置policy.conf
Podman 远程客户端的目的是允许用户在单独的客户端上与 Podman“后端”进行交互。远程客户端的命令行界面与常规 Podman 命令完全相同,只是删除了一些标志,因为它们不适用于远程客户端。
远程客户端利用客户端-服务器模型。您需要将 Podman 安装在同时运行 SSH 守护程序的 Linux 机器或 VM 上。在本地操作系统上,当您执行 Podman 命令时,Podman 通过 SSH 连接到服务器。然后它通过使用 systemd 套接字激活连接到 Podman 服务,并点击我们的Rest API。Podman 命令在服务器上执行。从客户端的角度来看,Podman 似乎在本地运行。
客户端机器
您将需要 Podman 或 podman-remote 客户端。两者的区别在于编译后的 podman-remote 客户端只能充当远程客户端连接到后端,而 Podman 可以运行本地的标准 Podman 命令,以及充当远程客户端(使用podman --remote)
服务器机器
需要在服务器机器上安装 Podman
在服务器机器上启用 Podman 服务。
在执行任何 Podman 客户端命令之前,您必须在 Linux 服务器上启用 podman.sock SystemD 服务。在这些示例中,我们以普通的非特权用户(也称为无根用户)运行 Podman。默认情况下,无根套接字侦听/run/user/${UID}/podman/podman.sock. 您可以使用以下命令永久启用此套接字:
[root@localhost ~]# systemctl --user enable --now podman.socket
您需要为此用户启用 linger 以便在用户未登录时套接字可以工作:
[root@localhost ~]# sudo loginctl enable-linger $USER
仅当您没有以 root 身份运行 Podman 时才需要这样做。
您可以使用简单的 Podman 命令验证套接字是否正在侦听。
[root@localhost ~]# podman --remote info
host:
arch: amd64
buildahVersion: 1.16.0-dev
cgroupVersion: v2
conmon:
package: conmon-2.0.19-1.fc32.x86_64
启用 sshd
为了让 Podman 客户端与服务器通信,您需要在 Linux 机器上启用并启动 SSH 守护程序(如果当前未启用)。
[root@localhost ~]# sudo systemctl enable --now sshd
设置 SSH
Remote Podman 使用 SSH 在客户端和服务器之间进行通信。远程客户端使用 SSH 密钥工作起来更加顺畅。要设置 ssh 连接,您需要从客户端计算机生成 ssh 密钥对。注意:在某些情况下,使用rsa密钥会导致连接问题,请务必创建ed25519密钥。
[root@localhost ~]# ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519
默认情况下,你的公钥应该在你的主目录下/.ssh/id_ed25519.pub。然后,您需要复制内容id_ed25519.pub并将其附加到/.ssh/authorized_keysLinux 服务器上。可以使用以下方法自动执行此操作ssh-copy-id:
[root@localhost ~]# ssh-copy-id -i ~/.ssh/id_ed25519.pub 192.168.122.1
如果不想使用 SSH 密钥,系统会在每个 Podman 命令中提示输入登录密码。
注意:podman-remote相当于podman --remote这里,取决于你选择安装什么。
使用 Podman 远程客户端的第一步是配置连接。
可以使用podman-remote system connection add命令添加连接。
[root@localhost ~]# podman-remote system connection add myuser --identity ~/.ssh/id_ed25519 ssh://192.168.89.150/run/user/1000/podman/podman.sock
这将向 Podman 添加一个远程连接,如果它是添加的第一个连接,它将将该连接标记为默认连接。您可以通过以下方式观察您的联系podman-remote system connection list:
podman-remote system connection list
Name Identity URI
myuser* id_ed25519 ssh://myuser@192.168.89.150/run/user/1000/podman/podman.sock
现在我们可以测试连接podman info:
podman-remote info
host:
arch: amd64
buildahVersion: 1.16.0-dev
cgroupVersion: v2
conmon:
package: conmon-2.0.19-1.fc32.x86_64
Podman-remote 还引入了一个“–connection”标志,您可以在其中使用您定义的其他连接。如果没有提供连接,将使用默认连接。
[root@localhost ~]# podman-remote system connection --help
似乎一旦人们掌握了容器的基础知识,网络就是他们开始尝试的第一个方面之一。而关于网络,在最终进入池的最深处之前只需要很少的实验。以下指南显示了 Podman 有根容器和无根容器最常见的网络设置。每个设置都有一个示例支持。
Podman 容器联网的指导因素之一是容器是否由 root 用户运行。这是因为非特权用户无法在主机上创建网络接口。因此,对于无根容器,默认的网络模式是 slirp4netns。由于权限的限制,slirp4netns 相比 rootful Podman 的联网,缺乏联网的一些特性;例如,slirp4netns 不能给容器一个可路由的 IP 地址。另一端的 rootful 容器的默认联网模式是 netavark,它允许容器有一个可路由的 IP 地址。
防火墙的作用不会影响网络的设置和配置,但会影响这些网络上的流量。最明显的是到容器主机的入站网络流量,通常通过端口映射传递到容器。根据防火墙实现,我们观察到防火墙端口由于运行具有端口映射的容器而自动打开(例如)。如果容器流量似乎无法正常工作,请检查防火墙并允许容器正在使用的端口上的流量。一个常见的问题是重新加载防火墙会删除 cni/netavark iptables 规则,从而导致 rootful 容器的网络连接丢失。Podman v3 提供了 podman network reload 命令来恢复它,而无需重新启动容器。
大多数使用 Podman 运行的容器和 Pod 都遵循几个简单的场景。默认情况下,rootful Podman 将创建一个桥接网络。这是 Podman 最直接和首选的网络设置。桥接网络在内部桥接网络上为容器创建一个接口,然后通过网络地址转换 (NAT) 连接到 Internet。我们还看到用户也希望macvlan 用于网络。这macvlan插件将整个网络接口从主机转发到容器中,允许它访问主机所连接的网络。最后,无根容器的默认网络配置是 slirp4netns。slirp4netns 网络模式功能有限,但可以在没有 root 权限的用户上运行。它创建从主机到容器的隧道以转发流量。
桥
桥接网络被定义为在连接容器和主机的地方创建内部网络。然后这个网络能够允许容器在主机之外进行通信。
网桥网络
一个笔记本电脑用户运行两个容器:一个 web 和 db 实例。这两个容器与主机位于虚拟网络上。此外,默认情况下,这些容器可以启动笔记本电脑外部的通信(例如到 Internet)。虚拟网络上的容器通常具有不可路由的,也称为私有 IP 地址。
当处理在主机外部发起的通信时,外部客户端通常必须寻址笔记本电脑的外部网络接口和给定的端口号。假设主机允许传入流量,主机将知道将该端口上的传入流量转发到特定容器。为此,当容器请求转发特定端口时,会添加防火墙规则来转发流量。
桥接网络是作为 root 创建的 Podman 容器的默认设置。Podman 提供了一个默认的桥接网络,但您可以使用该podman network create 命令创建其他网络。容器可以在使用 --network标志创建时加入网络,或者在通过podman network connectand podman network disconnect命令创建后加入网络。
如前所述,slirp4netns 是无根用户的默认网络配置。但是从 Podman 4.0 版开始,无根用户也可以使用 netavark。无根网盘的用户体验与有根网盘非常相似,只是没有提供默认的网络配置。您只需要创建一个网络,该网络将被创建为一个桥接网络。如果您想从 CNI 网络切换到 netavark,您必须发出podman system reset --force命令。这将删除您的所有图像、容器和自定义网络。
podman network create
当运行无根容器时,网络操作将在一个额外的网络命名空间内执行。要加入此命名空间,请使用 podman unshare --rootless-netns
默认网络
podmannetavark的默认网络是纯内存的。由于向后兼容 Docker,它不支持 dns 解析。要更改设置,请导出内存网络并更改文件。
对于默认的 rootful 网络使用
[root@localhost ~]# podman network inspect podman | jq .[] > /etc/containers/networks/podman.json
并且对于无根网络使用
[root@localhost ~]# podman network inspect podman | jq .[] > ~/.local/share/containers/storage/networks/podman.json
例子
默认情况下,如果您尚未从 Podman v3 迁移,rootful 容器使用 netavark 作为其默认网络。在这种情况下,不必将网络名称传递给 Podman。但是,您可以使用 podman create 命令创建其他桥接网络。
以下示例显示了如何设置 Web 服务器并将其以有根和无根的形式向主机外部的网络公开。它还将显示外部客户端如何连接到容器。
[root@localhost ~]# sudo podman run -dt --name webserver -p 8080:80 quay.io/libpod/banner
00f3440c7576aae2d5b193c40513c29c7964e96bf797cf0cc352c2b68ccbe66a
现在运行容器
[root@localhost ~]# podman run -dt --name webserver --net cni-podman1 -p 8081:80 quay.io/libpod/banner
269fd0d6b2c8ed60f2ca41d7beceec2471d72fb9a33aa8ca45b81dc9a0abbb12
注意在上面的运行命令中,容器的 80 端口(运行 Nginx 服务器的地方)被映射到主机的 8080 端口。选择 8080 端口是为了演示如何映射主机和容器端口以供外部访问。该端口也很可能是 80(无根用户除外)。
要从外部客户端连接到 Web 服务器,只需将 HTTP 客户端指向主机的 IP 地址,端口为 8080(rootful)和端口 8081(无根)。
(outside_host): $ curl 192.168.89.150:8080
___ __
/ _ \___ ___/ /_ _ ___ ____
/ ___/ _ \/ _ / ' \/ _ `/ _ \
/_/ \___/\_,_/_/_/_/\_,_/_//_/
(outside_host): $ curl 192.168.89.150:8081
___ __
/ _ \___ ___/ /_ _ ___ ____
/ ___/ _ \/ _ / ' \/ _ `/ _ \
/_/ \___/\_,_/_/_/_/\_,_/_//_/
Macvlan
使用 macvlan,容器可以访问主机上的物理网络接口。该接口可以配置多个子接口。每个子接口都能够拥有自己的 MAC 和 IP 地址。在 Podman 容器的情况下,容器将呈现自己,就好像它与主机位于同一网络上一样。
macvlan_network
外部客户端将能够通过其 IP 地址直接访问 Web 容器。通常网络信息(包括 IP 地址)是从 DHCP 服务器租用的,就像网络上的大多数其他网络客户端一样。如果笔记本电脑正在运行防火墙,例如 firewalld,则需要进行调整以进行正确访问。
例子
以下示例演示了如何在 macvlan 上设置 Web 容器以及如何从主机外部访问该容器。首先,创建 macvlan 网络。您需要知道连接到可路由网络的主机上的网络接口。在示例情况下,它是 eth0。在 Podman v4.0 中如果需要使用可租用的地址,应该继续使用 CNI 而不是 netavark。
[root@localhost ~]# sudo podman network create -d macvlan -o parent=eth0 webnetwork
webnetwork
下一步是确保 DHCP CNI 插件正在运行。该插件有助于从网络租用 DHCP。
[root@localhost ~]# sudo /usr/libexec/cni/dhcp daemon
现在运行容器并确保将其附加到我们之前创建的网络。
[root@localhost ~]# sudo podman run -dt --name webserver --network webnetwork quay.io/libpod/banner
03d82083c434d7e937fc0b87c25401f46ab5050007df403bf988e25e52c5cc40
[root@localhost ~]# sudo podman exec webserver ip address show eth0
2: eth0@if3: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state
UP
link/ether 0a:3c:e2:eb:87:0f brd ff:ff:ff:ff:ff:ff
inet 192.168.89.150/24 brd 192.168.89.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::83c:e2ff:feeb:870f/64 scope link
valid_lft forever preferred_lft forever
因为容器有一个可路由的 IP 地址(在这个网络上)并且不受 firewalld 管理,所以不需要更改防火墙。
(outside_host): $ curl http://192.168.99.186
___ __
/ _ \___ ___/ /_ _ ___ ____
/ ___/ _ \/ _ / ' \/ _ `/ _ \
/_/ \___/\_,_/_/_/_/\_,_/_//_/
Slirp4netns
Slirp4netns 是无根容器和 Pod 的默认网络设置。它的发明是因为不允许非特权用户在主机上创建网络接口。Slirp4netns 在容器的网络命名空间中创建一个 TAP 设备并连接到用户模式 TCP/IP 堆栈。
slirp_network
这台笔记本电脑上的非特权用户创建了两个容器:一个 DB 容器和一个 Web 容器。这两个容器都能够访问笔记本电脑以外的网络上的内容。如果容器绑定到主机端口并且笔记本电脑防火墙允许,则外部客户端可以访问容器。请记住,非特权用户必须使用端口 1024 到 65535,因为较低的端口需要 root 权限。(CAP_NET_BIND_SERVICE) 注意:这可以使用sysctl net.ipv4.ip_unprivileged_port_start
slirp4netns 的缺点之一是容器彼此完全隔离。与桥接方法不同,没有虚拟网络。对于容器之间的通信,它们可以使用与主机系统的端口映射,或者可以将它们放入共享相同网络命名空间的 Pod 中。
例子
以下示例将展示两个无根容器如何相互通信,其中一个是 Web 服务器。然后它将显示主机网络上的客户端如何与无根网络服务器通信。
首先,运行无根网络服务器并将容器中的端口 80 映射到非特权端口,例如 8080。
[root@localhost ~]# podman run -dt --name webserver -p 8080:80 quay.io/libpod/banner
17ea33ccd7f55ff45766b3ec596b990a5f2ba66eb9159cb89748a85dc3cebfe0
```
因为无根容器不能通过 IP 地址直接与 TCP/IP 相互通信,所以使用主机和端口映射。为此,必须知道主机(接口)的 IP 地址。
```
[root@localhost ~]# ip address show eth0
3: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group
default qlen 1000
link/ether 3c:e1:a1:c1:7a:3f brd ff:ff:ff:ff:ff:ff
altname eth0
inet 192.168.89.150/24 brd 192.168.89.255 scope global dynamic noprefixroute eth0
valid_lft 78808sec preferred_lft 78808sec
inet6 fe80::5632:6f10:9e76:c33/64 scope link noprefixroute
valid_lft forever preferred_lft forever
```
从另一个无根容器中,使用主机的 IP 地址和端口在两个无根容器之间成功通信。
```
[root@localhost ~]# podman run -it quay.io/libpod/banner curl http://192.168.89.150:8080
___ __
/ _ \___ ___/ /_ _ ___ ____
/ ___/ _ \/ _ / ' \/ _ `/ _ \
/_/ \___/\_,_/_/_/_/\_,_/_//_/
```
从主机外部的客户端,也可以使用 IP 地址和端口:
```
(outside_host): $ curl http://192.168.99.109:8080
___ __
/ _ \___ ___/ /_ _ ___ ____
/ ___/ _ \/ _ / ' \/ _ `/ _ \
/_/ \___/\_,_/_/_/_/\_,_/_//_/
```
### 容器和 Pod 之间的通信
---
大多数容器用户都对容器如何相互通信以及与世界其他地方通信有很好的理解。通常每个容器都有自己的 IP 地址和网络信息。它们使用常规 TCP/IP 方式(如 IP 地址)相互通信,或者在许多情况下,使用通常基于容器名称的 DNS 名称。但是 pod 是一个或多个容器的集合,因此继承了一些独特性。
根据定义,Podman pod 中的所有容器共享相同的网络命名空间。这一事实意味着它们将具有相同的 IP 地址、MAC 地址和端口映射。您可以使用 localhost 在 pod 中的容器之间方便地进行通信。