0x00 前言
今天,我们将从日志审计的角度探讨:为什么 Podman 比 Docker 更安全?
-
首先,我们需要了解 Linux 的 audit 日志审计功能和 loginuid
的基础知识; -
然后,我们将分别使用 Podman 和 Docker 启动容器,分析容器中 loginuid
的变化及其原因; -
最后,我们将通过真实攻击场景,展示 loginuid
的变化如何影响安全防护;
本文所有的验证都在 ubuntu 22.04 之上进行,且登录用户为 yaney
。
0x01 Linux 的基础知识
unsetunset1.1 audit 审计功能unsetunset
在讨论 Podman[1] 和 Docker 的安全性之前,我们需要了解一些关于 Linux audit 审计功能的基础知识。
Linux 内核有一个名为 audit 的安全功能,管理员可以通过该功能监视系统上的安全事件,并记录在本地的 audit.log
文件中。此外,audit 日志还可以远程存储在其他主机上,以防攻击者试图掩盖其入侵的痕迹。
我们以审计 /etc/shadow
文件上的操作为例进行说明。/etc/shadow
文件是一个常见的安全文件,攻击者可以向其中增加记录,从而重新获得系统的访问权限。
-
如上图所示,左边图我们安装了 auditd 软件包,并通过
auditctl
写入规则,开始审计/etc/shadow
文件。sudo auditctl -w /etc/shadow
-
右边的图,我们切换到 root 用户身份,并通过下面命令,对目标文件进行操作。
cat /etc/shadow
-
最后,我们使用 audit 所提供的工具
ausearch
来搜索近期与/etc/shadow
文件相关的事件。ausearch -f /etc/shadow -i -ts recent
一会儿时间,audit 就记录了许多信息,我们不需要去解读这些信息,只需关注一个问题,那就是:为什么 audit 能够正确地记录是 root 用户(uid=root)查看了/etc/shadow
文件,但该进程的原始登录用户(auid=yaney)是 yaney 呢?
unsetunset1.2 loginuidunsetunset
要回答这个问题,我们不得不了解 Linux 内核有个名为loginuid
的属性了。
loginuid
存储在虚拟文件系统/proc/self/loginuid
中,用来标识登录的用户。每当我们登录系统时,loginuid
就被设置为用户的 ID(UID
),且内核不允许任何进程对他进行修改。
最重要的是,从该登录进程派生出来的所有子进程都会继承这个 loginuid
,并形成一条“跟踪链”,这使得系统能够追踪到当前活动是由哪个登录用户发起的。例如:当我以 yaney 的身份进入系统时,loginuid
的编号为 1000;当我切换用户至 root 时,loginuid
的编号还是 1000。
图 4: 由登录进程派生出来的子进程会继续继承 loginuid
0x02 Podman 和 Docker 容器中的 loginuid 差异
众所周知,容器只是宿主机中一个特殊的进程,那么容器中的loginuid
又是怎样的呢?
我们分别用 Podman 和 Docker 启动容器,发现:Podman 的容器正确记录了我们的loginuid
,而 Docker 的容器记录的却4294967295
。docker 所记录的这个数字并不是一个随机的数字,我们后面再说。
图 5:docker 容器和 podman 容器中的 loginuid 不一致
那么,造成这种差异的原因又是什么呢?
Podman 使用的是传统的 fork/exec 模型,即 Podman 调用 fork
系统调用来创建一个子进程,这个子进程几乎完全复制了父进程的状态,与父进程共享相同的内存内容。子进程创建后,再调用 exec
或 execve
来替换自己的代码和数据。因此 Podman 容器进程是 Podman 的后代,podman 容器中的loginuid
与宿主机上的loginuid
完全一致。
而 Docker 使用的是 C/S 模型。当我们执行 docker 命令时,本质上是 Docker 客户端工具(docker cli)向 Docker 守护进程(dockerd)发送请求,dockerd 来处理与 docker cli 的 stdin/stdout 通信并创建容器。所以 Docker 容器是 dockerd 的后代,需要与守护进程的loginuid
一致。
前面我们说了 Docker 容器的 loginuid
并不是一个随机数,那是因为 dockerd 是初始化系统的子进程,它的 loginuid
是未经过设置的默认值,具体为 32 位无符号整数的最大值0xFFFFFFFF
,即十进制的4294967295
。这种情况,audit 一般会将 auid
设置为 unset
表示该进程无法关联到登录用户。
我们可以查看 dockerd 或 systemd 的loginuid
来进行验证。如图所示,dockerd 和 systemd 的 loginuid 都是默认值4294967295
。
-
查看 dockerd 和 systemd 的进程 ID(systemd 的进程 ID 一般为 1)。
ps -eo pid,uid,comm | egrep 'dockerd|systemd'
-
查看 dockerd 和 systemd 的 loginuid。
cat /proc/[pid]/loginuid
0x03 攻击案例:从容器中修改宿主机文件
那么,如果我们通过启动 Podman 容器和 Docker 容器来修改宿主机的 /etc/shadow
文件,audit 记录下来的日志又有什么区别呢?我们先来看看 Docker 容器的。
-
启动一个可交互的临时的特权容器,并将宿主机的根目录挂在到容器的
/host
目录中。sudo docker run -it --rm --privileged -v /:/host alpine sh
-
向宿主机的
/etc/shadow
文件写入数据。echo'docker,miao~' | tee -a /host/etc/shadow > /dev/null
-
使用 audit 所提供的工具
ausearch
来搜索近期与/etc/shadow
文件相关的事件。ausearch -f /etc/shadow -i -ts recent
如下图所示,audit 可以记录 /etc/shadow
文件被修改了,但是不知道是谁修改的(auid=unset)。在真实的业务环境中,如果攻击者随后删除了这个 Docker 容器,那么我们根本无法从海量的数据中准确找到修改 /etc/shadow
文件的痕迹。
接下来,我们来看看相同场景下,Podman 的表现如何。Podman 由于使用传统的 fork/exec 模型,因此可以正确记录所有内容。
图8:podman 容器修改宿主机的 /etc/shadow 文件
最后我们看到 /etc/shadow
都被写入了恶意数据。虽然这只是关于/etc/shadow
文件的一个简单的操作,但有的安全产品底层会使用 audit 来分析进程上的恶意行为,所以为了保持更好的安全性,我们推荐使用基于 fork/exec 启动容器的运行时而不是基于 C/S 启动容器的运行时。
0x04 总结
本文,我们从日志审计的角度解释为什么 Podman 比 Docker 更安全:
-
Podman 使用传统的 fork/exec 模型。容器进程正确继承父进程的 loginuid
,使系统能追踪到操作的发起用户; -
而 Docker 使用 C/S 模型。容器进程继承 Docker 守护进程(dockerd)的 loginuid
(值为 4294967295),因此无法追踪具体的操作用户。
在实际攻击场景中,攻击者通过 Docker 容器修改宿主机的关键文件时,audit 日志无法记录操作者身份。相比之下,Podman 容器的所有操作都能被 audit 准确记录,有利于后续的安全审计和追踪。
除了日志审计的优势,Podman 还具有以下安全特性:
-
无需 root 权限即可运行容器,避免了守护进程可能带来的安全风险; -
Docker 这类采用 C/S 模型的运行时需要用户以 root 权限运行守护进程并打开套接字来启动容器。这导致我们只能依赖守护进程实现的安全机制,若守护进程存在安全漏洞,攻击者就可能直接影响主机操作系统的安全性。 -
支持 systemd 的 SD_NOTIFY
功能和套接字激活功能,提供更安全高效的服务管理方式。 -
当我们将 podman 命令放入 systemd unit 文件中,容器进程可通过 Podman 向 systemd 发送通知,表明容器已就绪并可接受外部请求。 SD_NOTIFY
功能提高了服务启动效率,避免竞态条件,实现更精确的服务管理。 -
此外,我们可以将连接套接字从 systemd 传递到 Podman,再传递到容器进程。这样容器无需独立处理网络连接,只需通过 systemd 就能直接响应外部网络请求,减少了额外配置。
Podman: https://podman.io/
原文始发于微信公众号(喵苗安全):为什么 Podman 比 Docker 更安全(一)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论