独栋别墅等同于在单个隔离节点上运行一个应用程序,双拼别墅等同于在单独的虚拟机中运行每个应用容器。酒店或者公寓楼类似于容器。你有自己的公寓,但你依赖前台的安全性来控制对你的居住空间的访问。如果前台被攻陷,那么你的公寓也会被攻陷。容器也类似于此,因为它们依赖内核的安全性。如果一个容器可以接管主机内核,那么它就可以接管系统上运行的所有容器化应用程序。此外,如果它们逃逸到底层文件系统,就可能读取和写入系统上的所有容器数据。

容器安全的最重要目标是保护主机内核和文件系统免受容器进程的影响。如果内核存在漏洞,那么系统的其余部分和所有容器都将易受攻击。大多数情况下,容器和主机之间唯一的接触点就是主机内核本身。

2025.02 广州·荔湾区·圆大厦

安全隔离

只读的内核伪文件系统

  1. Podman以只读方式挂载/sys、/sys/fs/cgroup、/sys/fs/selinux
  2. 容器内部/proc不是主机的/proc,容器内部进程只能影响容器内的其他进程
    1. 在/proc/acpi、/proc/kcore等目录上挂载只读 tmpfs
    2. 只读绑定挂载/proc/fs、/proc/sys等部分目录

取消屏蔽的文件路径

1
2
3
4
5
6
7
8
9
10
11
12
# 宿主机的/proc/scsi目录
[sujx@docker ~]$ ls /proc/scsi/
device_info mptspi scsi sg
# 容器的/proc/scsi目录
[sujx@docker ~]$ podman run --rm ubi9 ls /proc/scsi
[sujx@docker ~]$
# 取消屏蔽后的容器/proc/scsi目录
[sujx@docker ~]$ podman run --rm --security-opt unmask=/proc/scsi ubi9 ls /proc/scsi
device_info
mptspi
scsi
sg

屏蔽其他路径

1
2
3
4
5
6
7
8
9
10
11
# 查看容器的/proc/sys/dev目录
[sujx@docker ~]$ podman run --rm ubi9 ls /proc/sys/dev
cdrom
hpet
mac_hid
raid
scsi
tty
# 将容器的/proc/sys/dev目录屏蔽
[sujx@docker ~]$ podman run --rm --security-opt mask=/proc/sys/dev ubi9 ls /proc/sys/dev
[sujx@docker ~]$

Linux能力

如果容器已经挂载了什么,那么如何防止容器内的root用户删除这些挂载点或重新挂载文件系统以进行读写并攻击主机内核?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# root 用户的能力,默认为所有的41个能力
root@docker ~]# capsh --print
Current: =ep
Bounding set =cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read,cap_perfmon,cap_bpf,cap_checkpoint_restore
uid=0(root) euid=0(root)
gid=0(root)
groups=0(root)
Guessed mode: HYBRID (4)

# 普通用户的能力为空
[sujx@docker ~]$ capsh --print
Current: =
uid=1000(sujx) euid=1000(sujx)
gid=1000(sujx)
groups=10(wheel),1000(sujx)
Guessed mode: HYBRID (4)

# 容器的root用户能力为11个能力
[sujx@docker ~]$ podman run --rm ubi9 capsh --print
Current: cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_sys_chroot,cap_setfcap=ep
uid=0(root) euid=0(root)
gid=0(root)
groups=
Guessed mode: UNCERTAIN (0)

用户的能力主要涉及进程的控制,例如CAP_SETUID 和CAP_SETGID 允许进程切换到不同的UID和GID。Podman的另一个有趣的例子是普通用户权限的podman是无法将80端口绑定到容器的80端口,就是因为容器内的root不具有 CAP_NET_BIND_SERVICE 能力,它的作用就是让进程绑定到端口号小于1024的网络端口。那么如何防止root进程卸载或者重新挂载只读文件系统呢?答案就是删除root的CAP_SYS_ADMIN能力。

删除Linux能力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 删除容器root用户的 CAP_NET_BIND_SERVICE 能力,
[sujx@docker ~]$ podman run --cap-drop CAP_NET_BIND_SERVICE ubi9 capsh --print
Current: cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_sys_chroot,cap_setfcap=ep
uid=0(root) euid=0(root)
gid=0(root)
groups=
Guessed mode: UNCERTAIN (0)

# 也可以将容器root用户的所有能力都删除
[sujx@docker ~]$ podman run --cap-drop all ubi9 capsh --print
Current: =
uid=0(root) euid=0(root)
gid=0(root)
groups=
Guessed mode: UNCERTAIN (0)

添加Linux能力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 给容器内的root用户添加CAP_NET_RAW能力
[sujx@docker ~]$ podman run --rm --cap-add CAP_NET_RAW ubi9 capsh --print
Current: cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_setfcap=ep
uid=0(root) euid=0(root)
gid=0(root)
groups=
Guessed mode: UNCERTAIN (0)
# 也可以只给容器内的用户添加CAP_NET_RAW能力
[sujx@docker ~]$ podman run --rm --cap-drop all --cap-add CAP_NET_RAW ubi9 capsh --print
Current: cap_net_raw=ep
uid=0(root) euid=0(root)
gid=0(root)
groups=
Guessed mode: UNCERTAIN (0)

无新特权

Podman可以使用–privileged 让容器获取所有特权,同时也有一个开关 –security-opt no-new-privileges ,用来禁用容器进程获取附加特权的能力。就是将容器进程锁定在它们启动时拥有的Linux能力组中。即便它可以执行setid,内核也拒绝它获取额外能力。该开关也可以阻止selinux的标签切换。

root始终是危险的

容器内的进程始终是以root身份运行,root进程允许修改系统上的所有属于root的文件,还可以修改系统文件并欺骗有特权的管理员执行它。

UID隔离

非特权用户

使用非特权用户运行容器解决了将进程以root用户身份运行的问题,但所有容器都在同一个非特权用户命名空间中运行,那么理论上一个容器就可以攻击另一个容器,因为它们使用相同的UID。同时,如果容器进程突破了限制,它们还是可以修改用户家目录中的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# podman默认自动给容器从1开始默认分配1024个UiD
# 对于非特权用户这个UID=1并不是系统的root用户,而是非特权用户自己
[sujx@docker ~]$ podman run --userns=auto ubi9 cat /proc/self/uid_map
0 1 1024
# UID 1是主机系统上非特权用户UID=1000
# 非特权用户只能取得65535个UID,因此只能启动64个容器且无法运行UID大于65535的容器
[sujx@docker ~]$ podman run ubi9 cat /proc/self/uid_map
0 1000 1
1 524288 65536
# 使用UID为2000创建容器,则是从1024开始创建UID,创建的数量为2001个,就是分配UID加root用户
# 第二个容器的的起始UID减去第一个容器的起始UID等于1024,即使的两个容器的进程UID完全不同.
[sujx@docker ~]$ podman run --user=2000 --userns=auto ubi9 cat /proc/self/uid_map
0 1025 2001
# 删除所有容器之后,UID会被回收,并用于下一个创建的容器
[sujx@docker ~]$ podman rm --all
88e9eea85815dd6e46609ca72b0d37bb727164cbe2776398f62aae9301232c2e
2bbc128638801cea8b96b8623c2e89571ca9f75aa7d667131df8221fa26d03c0
e657d51c3769d02161c60bd32417c8ad61ddaa25c7ae4e0b22a7f150a19cbf89
3812fa5956c9439956411ed4fd2c08c77c0e7d6e0d20a634666d1dc7319d4f44
[sujx@docker ~]$ podman run --userns=auto ubi9 cat /proc/self/uid_map
0 1 1024
[sujx@docker ~]$ podman run --userns=auto ubi9 cat /proc/self/uid_map
0 1025 1024
[sujx@docker ~]$ podman run --userns=auto ubi9 cat /proc/self/uid_map
0 2049 1024
# 以上这些限制在storage.conf配置中确认
[sujx@docker ~]$ cat /usr/share/containers/storage.conf |grep auto-userns-m
# auto-userns-min-size=1024
# auto-userns-max-size=65536

特权用户

对于特权用户root而言,情况又有不同。以root用户运行Podman容器使用隔离的用户命名空间,需要手动指定contianer的UID范围。否则会出现以下报错:

[root@docker ~]# podman run –userns=auto ubi9 cat /proc/self/uid_map
ERRO[0000] Cannot find mappings for user “containers”: no subuid ranges found for user “containers” in /etc/subuid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 查看root用户的UID范围,可见系统的UID范围是40亿个可用UID
[root@docker ~]# podman run ubi9 cat /proc/self/uid_map
0 0 4294967295
# Podman建议给容器分配最高20亿个UID
cat >> /etc/subuid <<EOF
containers:2147483647:2147483647
EOF
cat >> /etc/subgid <<EOF
containers:2147483647:2147483647
EOF
# 执行自动分配用户空间的容器进程UID范围
[root@docker ~]# podman run --userns=auto ubi9 cat /proc/self/uid_map
0 2147483647 1024
[root@docker ~]# podman run --userns=auto ubi9 cat /proc/self/uid_map
0 2147484671 1024
[root@docker ~]# podman run --user=2000 --userns=auto ubi9 cat /proc/self/uid_map
0 2147485695 2001
[root@docker ~]# podman run --userns=auto ubi9 cat /proc/self/uid_map
0 2147487696 1024
# 2147487696-2147485695=2001

进程隔离

在自己的PID命名空间中运行容器,只能看到本容器内的进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 默认情况下使用PID命名空间隔离
[root@docker ~]# podman run ubi9 find /proc -maxdepth 1 -type d -regex ".*/[0-9]*"
/proc/1
[root@docker ~]# podman run ubi9 pstree
pstree

# 去掉PID命名空间隔离,能看到系统中的所有进程
[root@docker ~]# podman run --pid=host ubi9 find /proc -maxdepth 1 -type d -regex ".*/[0-9]*"
/proc/1
/proc/2
/proc/3
/proc/4
/proc/5
/proc/6
/proc/7
/proc/11
root@docker ~]# podman run --pid=host ubi9 pstree
systemd-+-ModemManager---3*[{ModemManager}]
|-NetworkManager---3*[{NetworkManager}]
|-VGAuthService
|-3*[abrt-dump-journ]
|-abrtd---3*[{abrtd}]
|-agetty
|-atd
|-auditd-+-sedispatch
| `-2*[{auditd}]
|-catatonit
|-chronyd
|-conmon---pstree
|-crond
|-dbus-broker-lau---dbus-broker
|-gssproxy---5*[{gssproxy}]
|-irqbalance---{irqbalance}
|-mcelog
|-nm-dispatcher---4*[{nm-dispatcher}]
|-polkitd---3*[{polkitd}]
|-rsyslogd---2*[{rsyslogd}]
|-snmpd
|-sshd---sshd---bash---sudo---sudo---su---bash---podman---8*[{podman}]
|-sshd---sshd---bash
|-sshd
|-systemd-+-(sd-pam)
| `-dbus-broker-lau---dbus-broker
|-systemd-homed
|-systemd-journal
|-systemd-logind
|-systemd-oomd
|-systemd-resolve
|-systemd-udevd---20*[(udev-worker)]
|-systemd-userdbd---3*[systemd-userwor]
|-udisksd---5*[{udisksd}]
`-vmtoolsd---3*[{vmtoolsd}]

网络隔离

网络命名空间设置了与主机网络的隔离,允许Podman设置虚拟专用网络,以控制那些容器可以与其他容器通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 创建不同的网络
[sujx@docker ~]$ podman network create net1
net1
[sujx@docker ~]$ podman network create net2
net2
# 创建目标主机
[sujx@docker ~]$ podman run -d --network net1 --name cnet1 ubi9 sleep 1000
# 执行net1的ping
[sujx@docker ~]$ podman run --network net1 alpine ping -c 3 cnet1
PING cnet1 (10.89.1.2): 56 data bytes
64 bytes from 10.89.1.2: seq=0 ttl=42 time=0.021 ms
64 bytes from 10.89.1.2: seq=1 ttl=42 time=0.197 ms
64 bytes from 10.89.1.2: seq=2 ttl=42 time=0.076 ms

--- cnet1 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.021/0.098/0.197 ms
# 执行net2的ping
[sujx@docker ~]$ podman run --network net2 alpine ping -c 3 cnet1
ping: bad address 'cnet1'

Fork/exec模型

Podman使用fork/exec模型,与Docker的守护进程不同,容器进程只能以你的用户UID运行。通过非root进程访问docker.sock比提供root进程或sudo访问权限更危险。

1
2
3
4
5
6
7
8
9
10
11
12
# 查看当前用户的UID
[sujx@docker ~]$ cat /proc/self/loginuid
1000
# 创建目标容器
[sujx@docker ~]$ podman run -d ubi9 sleep 100
c9a449ef680e58c3502f4a73b12b66965e20bf74d33863947eb5a91e9203ab04
# 查看容器运行进程PID
[sujx@docker ~]$ podman inspect -l --format '{{ .State.Pid }}'
28610
# 根据容器进程PID查找实际登陆运行的UID
[sujx@docker ~]$ cat /proc/28610/loginuid
1000

机密处理

使用secret机制可以避免将机密信息硬编码到镜像中去,而容器应用的用户必须提供sercet才能正常使用。Podman提供了podman secret机制,允许向容器添加文件或者环境变量,而不会在容器提交到镜像时保存这些secret。

1
2
3
4
5
6
7
8
9
10
11
# 设定secret
[sujx@docker ~]$ echo "This is my secret." > /tmp/secret
# 创建secret
[sujx@docker ~]$ podman secret create my_secret /tmp/secret
7c6f641a0001db0e3e2b9a57d
# 将secret注入容器
[sujx@docker ~]$ podman run --secret my_secret ubi9 cat /run/secrets/my_secret
This is my secret.
# 使用环境变量
[sujx@docker ~]$ podman run --secret my_secret,type=env ubi9 bash -c 'echo $my_secret'
This is my secret.

镜像信任

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 设定拒绝docker.io拉取镜像
[sujx@docker ~]$ sudo podman image trust set -t reject docker.io
# 设定可以接受docker.io/library拉取镜像
[sujx@docker ~]$ sudo podman image trust set -t accept docker.io/library
# 查看当前策略
[sujx@docker ~]$ podman image trust show
TRANSPORT NAME TYPE ID STORE
all default accept
repository docker.io reject
repository docker.io/library accept
docker-daemon accept
# 设置默认拒绝
[sujx@docker ~]$ sudo podman image trust set -t reject default
[sujx@docker ~]$ podman image trust show
TRANSPORT NAME TYPE ID STORE
all default reject
repository docker.io reject
repository docker.io/library accept
docker-daemon accept
# 其本质时配置/etc/container/policy.json文件
[sujx@docker ~]$ cat /etc/containers/policy.json
{
"default": [
{
"type": "reject"
}
],
"transports": {
"docker": {
"docker.io": [
{
"type": "reject"
}
],
"docker.io/library": [
{
"type": "insecureAcceptAnything"
}
]
},
"docker-daemon": {
"": [
{
"type": "insecureAcceptAnything"
}
]
}
}
}

镜像扫描

Podman不提供镜像扫描工具,但可以直接挂载一个可以扫描的对象,能够看到镜像的挂载内容,而不必执行镜像的任何代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 不能在非特权模式下执行podman mount
[sujx@docker ~]$ podman image mount ubi9
Error: cannot run command "podman image mount" in rootless mode, must execute `podman unshare` first
# 需要首先进入用户命名空间
[sujx@docker ~]$ podman unshare
# 挂载ubi9镜像
[root@docker ~]$ podman image mount ubi9
/home/sujx/.local/share/containers/storage/overlay/708bb73104717130dfb640b54783babb28adfb2296ef1fb40023808c1861e3ad/merged
[root@docker ~]$ mnt=$(podman image mount ubi9)
[root@docker ~]$ echo $mnt
/home/sujx/.local/share/containers/storage/overlay/708bb73104717130dfb640b54783babb28adfb2296ef1fb40023808c1861e3ad/merged
[root@docker ~]$ cd $mnt
[root@docker merged]$ ls
afs boot etc lib media opt root sbin sys usr
bin dev home lib64 mnt proc run srv tmp var
[root@docker merged]$ cd root
[root@docker root]$ ls
buildinfo

镜像签名

创建GPG密钥

  1. 创建GPG密钥对
  2. 创建Web服务器存储签名

签名并推送镜像

  1. 配置registries.d/default.yaml文件
  2. 配置容器引擎镜像信任

配置podman并拉取已签名镜像

  1. 拉取镜像并校验