【DevOps构筑篇】用SELinux强化Docker的安全性
推荐超级课程:
@TOC
这篇文章中,我们将通过实际操作命令,练习在Rocky Linux上构建启用了SELinux的Docker。 按照官方指南“在CentOS上安装Docker Engine | Docker文档 ”进行操作,来安装Docker。安装步骤将遵循官方指南。
[def-root]# yum install -y yum-utils
[def-root]# yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
[def-root]# yum install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
到这里为止,我们已经能够安装Docker,接下来我们将启动服务。
[def-root]# systemctl start docker
为了确认Docker是否正常运行,让我们下载并启动官方提供的测试用容器。
[def-root]# docker run hello-world
我们已经成功地启动了服务。到这里,Docker的设置已经完成了,但是SELinux的设置还没有完成。按照官方指南安装的Docker默认情况下SELinux并没有启用,因此如果Docker容器以特权模式运行,就可能会发生所谓的“容器逃逸”,即容器能够访问并操作宿主机,这样就不能有效地防止攻击。
因此,我们需要编辑Docker的启动选项,以确保docker容器的进程在SELinux域下运行,并且能够得到适当的访问控制。首先,docker服务是通过systemd执行dockerd命令来启动的。因此,通过编辑systemd的单元文件,我们可以更改docker的启动选项。
[def-root]# cd /usr/lib/systemd/system
[def-root]# cp -p docker.service{,.bak}
在编辑单元文件之前,我已经创建了一个备份,而没有改变时间戳。然后,我开始编辑单元文件。
[def-root]# vi docker.service
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --selinux-enabled
在指定服务启动时执行的命令的 ExecStart
部分,我们正在启动 /usr/bin/dockerd
,因此我们在这里指定选项 --selinux-enabled
。这样,当服务启动时,进程将会在SELinux赋予适当标签的状态下启动。
在重启docker服务之前,由于我们已经编辑了单元文件,所以首先需要重新加载单元文件。
[def-root]# systemctl daemon-reload
重新加载后,让我们尝试启动docker服务。
[def-root]# systemctl restart docker
[def-root]# systemctl status docker
● docker.service - Docker Application Container Engine
Loaded: loaded (/usr/lib/systemd/system/docker.service; disabled; preset: disabled)
CGroup: /system.slice/docker.service
└─20333 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --selinux-enabled
当使用systemctl命令查看状态时,可以看到命令选项中已经添加了–selinux-enabled,这表明Docker已经以启用SELinux的模式启动了。此时,dockerd进程正在container_runtime_t域中运行。
[def-root]# ps auxZ | grep docker
system_u:system_r:container_runtime_t:s0 root 20333 0.0 5.0 1928868 91592 ? Ssl 4月19 1:28 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --selinux-enabled
现在,让我们在这个状态下尝试启动一个容器。在这个过程中,为了之后能够进行验证,我们将宿主机的根目录挂载到启动的容器中,使得容器内部能够访问宿主机的根目录,然后启动容器。
[def-root]# docker run -it --rm -v /root:/hostroot centos:latest /bin/bash
[root@4dd9367681d8 ~]# id
[root@4dd9367681d8 ~]# ps -Z
成功进入了容器内部。使用 ps 命令查看进程的上下文,可以确认 bash 进程正在 container_t 的域中运行。然后,我们也顺便确认一下当前当前目录的上下文。
[def-root]# pwd
/root
[def-root]# ls -dZ .
system_u:object_r:container_file_t:s0:c126,c245 .
当前目录是 container_file 类型,因此看起来是允许文件读写操作的。实际上,使用 vim 命令进行写入操作也是可能的。
[def-root]# vi test.txt
hello
[def-root]# cat test.txt
接下来,我们将当前目录移动到从宿主机挂载的 hostroot 目录。因为这里的目录与宿主机的目录是共通的,所以我们可以通过检查是否能够在这里写入,来验证Docker进程的SELinux是否启用。
[root@4dd9367681d8 /]# cd /hostroot/
[root@4dd9367681d8 hostroot]# ls -d -Z .
system_u:object_r:admin_home_t:s0 .
首先,我们确认一下 hostroot 的上下文,它被标记为 admin_home 类型。这通常是指被标记在 root 目录下的类型,表示只有 root 用户可以写入。现在,让我们在这个 hostroot 目录下尝试创建一个文件。
[root@4dd9367681d8 hostroot]# echo hello > test.txt
bash: test.txt: Permission denied
[root@4dd9367681d8 hostroot]# ls
ls: cannot open directory '.': Permission denied
不仅无法写入文件,甚至无法使用 ls 命令来查看目录内容。那么,让我们检查一下宿主机上的 SELinux 拒绝了哪些操作。
[def-root]# grep denied /var/log/audit/audit.log
type=AVC msg=audit(1713603292.240:1088): avc: denied { write } for pid=21476 comm="bash" name="root" dev="dm-0" ino=16777346 scontext=system_u:system_r:container_t:s0:c170,c393 tcontext=system_u:object_r:admin_home_t:s0 tclass=dir permissive=0
审计日志中的拒绝日志记录了在container域中运行的bash命令尝试写入admin_home类型的目录,但被SELinux拒绝。这意味着从容器内部尝试访问容器外部被SELinux阻止,从而可以保持更高的安全性。
如果SELinux被禁用,那么通常可以像往常一样写入挂载的root目录。试着禁用SELinux并进行验证。
[def-root]# setenforce 0
[def-root]# getenforce
[root@4dd9367681d8 hostroot]# ls
[root@4dd9367681d8 hostroot]# echo hello > test.txt
[root@4dd9367681d8 hostroot]# cat test.txt
hello
[def-root]# cat test.txt
hello
通过禁用SELinux,已经确认可以在宿主机的root用户的主目录中写入文件。但是,禁用SELinux会导致安全级别的降低,因此如果可能的话,我希望不要禁用而是通过设置来解决问题。首先,让我们重新启用SELinux。
[def-root]# setenforce 1
那么,如何在保持SELinux启用的情况下,设置仅能访问宿主机上指定的特定目录呢?一个方案是修改宿主机目录的SELinux上下文。首先,我们可以使用sesearch
命令来调查container_t域可以访问的类型的SELinux设置。
[def-root]# sesearch --allow --source container_t
allow svirt_sandbox_domain container_file_t:dir { add_name create execmod ioctl link lock read relabelfrom relabelto remove_name rename reparent rmdir setattr unlink watch watch_reads write }
allow svirt_sandbox_domain container_ro_file_t:dir { ioctl lock read }
容器域要访问的目标,如果标记为container_file_t或container_ro_file_t等标签,似乎会被SELinux允许访问。试着在宿主机的root用户的主目录下创建一个具有container_file类型的目录,并将其挂载,然后验证是否可以从容器向宿主机侧写入。
[def-root]# pwd
[def-root]# mkdir rw_dir
[def-root]# ll -Z
[def-root]# chcon -t container_file_t rw_dir
[def-root]# ll -Z
我已经准备好了一个可以从容器中进行读写操作的目录,现在我将挂载这个目录并启动容器。
[def-root]# docker run -it --rm -v /root/rw_dir:/hostroot centos:latest /bin/bash
[root@28dfb8df0a10 /]# cd /hostroot/
[root@28dfb8df0a10 hostroot]# ls -dZ .
unconfined_u:object_r:container_file_t:s0 .
[root@28dfb8df0a10 hostroot]# echo hello2 > hello2.txt
[root@28dfb8df0a10 hostroot]# ls
[def-root]# ls rw_dir/
我能够写入挂载的container_file类型的目录。
如果想要将挂载的目录设置为只读,可以使用container_ro_file类型进行标记。让我们试着用命令来验证一下。
[def-root]# cp -r rw_dir/ ro_dir
[def-root]# chcon -R -t container_ro_file_t ro_dir
[def-root]# ll -Z
[def-root]# docker run -it --rm -v /root/ro_dir:/hostroot centos:latest /bin/bash
[def-root]# cd /hostroot
[def-root hostroot]# ls -dZ .
unconfined_u:object_r:container_ro_file_t:s0 .
[def-root hostroot]# ls
hello2.txt
[root@b1e3967e8a69 hostroot]# cat hello2.txt
hello
[root@b1e3967e8a69 hostroot]# echo aaa >> hello2.txt
bash: hello2.txt: Permission denied
[root@b1e3967e8a69 hostroot]# mkdir hoge
mkdir: cannot create directory 'hoge': Permission denied
可以查看挂载的container_ro_file类型的目录和文件内容,但是尝试写入文件或创建目录时,会被SELinux拒绝。
本次我们故意挂载了root的主目录,以创建一个从容器内部向宿主机侧写入的环境。然而,实际上存在利用Docker的漏洞来操作超出容器沙箱限制的宿主机侧的攻击,这种攻击被称为容器逃逸。在这种情况下,通过让Docker进程在适当的域下运行,可以最小化对宿主机侧的损害。
这样的Docker容器即使作为沙箱使用,但由于云上容器的安全管理的不足,存在暴露重要数据的风险,可能成为攻击者的好目标。例如,攻击者可能在Docker Hub上共享的社区容器镜像中植入恶意脚本,或者启动配置有缺陷的容器,这可能导致所谓的容器逃逸,即从容器侧逃逸到宿主机侧,并操作宿主服务器。
总结
像Docker这样以特权运行的服务的子进程,如果不在适当的安全设置下,可能会遭受攻击者的损害。如果容器受到损害,SELinux是能够将宿主机侧的损害限制到最小程度的技术之一。 禁用SELinux可能会让系统开发者操作起来更加方便,但这对攻击者来说也是一样的。攻击者可能会认为这更容易作为攻击的跳板。因此,禁用SELinux与禁用防火墙一样,是一种危险的行为。
SELinux是RedHat系列默认搭载的功能。由于SELinux限制包括Docker容器在内的进程,因此可以帮助维持更高的安全性。