容器化部署大多数情况下都是基于Linux操作系统部署的(当然在Windows平台也能运行容器化应用),因此了解Linux操作系统提供的安全机制对运行在其上的容器进程的安全来说,至关重要。特别是理解Linux操作系统提供的各种安全特性有助于我们深刻的理解很多容器安全的原理,进而让我们在设计系统的时候,能够防微杜渐,给出成体系化全面的安全整体方案。
具体来说,Linux的安全机制主要体现在系统调用,文件权限机制,以及我们经常讨论权限提升攻击中出现的系统capabilities。可能很多读者对这些概念以及操作系统机制已经有比较深入的理解了,但是笔者还是建议大家仔细的阅读一下本文,因为我们无论是在Docker这样的容器平台还是Kubernetes这样的容器编排平台上,操作系统提供的这些能力都是整个安全的基石,万丈高楼平地起,咱们就从打好地基开始吧!
笔者在介绍容器的本质系统文章中,强调过容器是运行在操作系统上的特殊进程,因此容器进程(容器实例)本质上就是运行在操作系统上的进程,我们在宿主机上通过ps能看到正在运行的容器应用程序。运行在同一台机器上的多个进程共享机器的硬件资源,对于容器化的进程来说,使用系统资源和普通进程无二,都是通过系统调用(system call),并且应该具备相应系统调用访问权限。
容器进程和普通进程不同的地方就在于系统调用的权限控制,因为对于容器来说,我们可以在镜像打包的时候,或者在容器运行的时候,来赋予容器进程访问系统资源的权限,选择的时机不同,对于系统造成的安全影响也有很大的不同。
在Linux操作系统上,应用程序一般运行在用户空间(user space),对系统资源使用的权限要远比操作系统内核低(operating system kernel)。运行在用户空间的应用程序如果需要从文件中读取数据,发送数据或者获取系统当前的时间,都需要请求内核的帮助来完成。具体来说,内核提供了用户空间代码可调用的编程接口,我们通常情况下将这些接口称作system call或者syscall接口。
在最新版本的Linux操作系统上,大概有300+的syscall接口,提供的功能涵盖了从文件读取到网络操作,系统配置和进程控制等等,以下是我们熟知的一些接口:
1,read接口,从文件中读取数据
2,write接口,写输入到文件中
3,open接口,在读写文件之前需要先打开文件
4,execve接口,在操作系统上运行可执行文件
5,chown接口,改变文件的ownership(所有者)
6,clone接口,在操作系统上创建新的进程
坦白讲,对于大部分应用系统的开发人员来说,很少有机会直接调用操作系统接口,并且这些操作系统功能大概率已经被使用的编程语言进行了封装,比如我们使用Core Java编写文件处理程序,io包里就已经封装了文件open,read和write操作,并提供了丰富的库函数来加速开发的效率。但是对于底层代码框架开发人员,比如说Golang语言的syscall代码包,java语言的io代码包,就需要熟知这些接口。幸运的是,大部分编写业务系统的同学,几乎不太需要深入的了解这些底层的系统调用。
运行在操作系统上的应用程序和运行在容器中的应用程序使用这些系统调用的方式没有什么本质的区别,并且运行在同一台机器上的多个应用程序,或者同一个应用程序的多个实例,访问系统资源的时候,本质上共享了同一套操作系统内核。当然,并不是所有的应用程序都需要进行系统调用,咱容器安全模型文章中曾经提到过几个重要的安全原则,最小权限原则告诉我们,如无必要,无需赋予权限。Linux提供了安全控制机制来限制进程可以执行的系统调用,比如seccomp,我们会在后续的文章中详细的介绍。
Linux操作系统上另外一个非常重要的安全机制是文件权限,可以说文件权限(file permission)是整个操作系统的安全基石。在Linux操作系统上,有一个非常重要的概念:Everything is a file(Linux操作系统上所有事物都是文件)。应用程序的代码,数据,配置文件,日志,甚至是如显示器,打印机,磁盘等这样的硬件设备,都是文件。文件的权限决定了用户能在文件上具体执行什么操作,是否能够访问文件的数据等。如果读者用过Mac,或者自己搭建过虚拟机,一定不会对下图所示的通过ls -l命令罗列文件列表的输出感到陌生:
➜ Kubernetes安全 ls -l
-rwxr-xr-x 1 gaopanqi staff 31328 Oct 6 08:04 mysleep
上边的输出显示了笔者电脑上的一个可执行文件,其中可以看到这个叫mysleep的文件所有者是名叫“gaopanqi”的用户,并且gaopanqi属于工作组staff。输出的最开始的字符串“rwxr-xr-x”标明了文件的访问权限。具体来说一共有9个字符,被分为三组:
- 最开始的三个字符rwx标识了文件的所有者gaopanqi所具有的权限
- 中间的三个字符r-x代表gaopanqi所属的工作组中其他用户具有的权限
- 最后三个字符r-x代表其他用户(不是gaopanq,也不在工作组staff)具有的权限
文件的操作权限有三种:read(r),write(w)和execute(x),用户是否能够对文件进行操作取决于对应位置的权限是否被打开,比如上边的例子,对于属于staff工作组的所有用户,对mysleep这个文件有read和执行的权限(通过r和x表示),而没有写的权限(修改的权限,通过-表示)。
Linux操作系统上文件的管线管理机制并不复杂,我相信大部分同学应该都非常清楚,但是Linux上文件的权限不仅仅局限于此,文件的权限还会受到setuid,setgid以及sticky bits的影响,咱们着重聊一下前两个,因为从安全的角度来看,这两个位会允许进程获得额外的权限,很容易被恶意攻击者用来进行权限提升以达到窃取敏感数据的目的。
通常情况下,我们在Linux操作系统上运行可执行文件,启动的进程会继承运行这个可执行文件的用户ID,但是如果可执行文件设置了setuid位,那么进程会被设置成这个可执行文件所有者的用户ID(UID),我们来通过一个具体的例子说明一下。
Linux操作系统上还有sleep这个命令,用来将当前的操作延迟一段时间。首先启动本地虚拟机(建议读者使用vagrant工具来管理虚拟机,笔者使用的是Ubuntu操作系统),然后登陆到Linux操作系统,运行命令which sleep确定可执行程序的安装位置,笔者的机器上在/bin目录下。
然后执行cp /bin/sleep ./mysleep将可执行文件拷贝到vagrant用户的默认登陆目录,我们可以使用命令ls -l mysleep来查看文件的具体权限情况,如下图所示:
vagrant@vagrant:~$ ls -l mysleep
-rwxr-xr-x 1 vagrant vagrant 35000 Oct 8 08:49 mysleep
接着我们在当前窗口以root用户来运行这个拷贝过来的mysleep可执行文件,执行命令sudo ./mysleep 100(100的含义是让启动的进程睡眠100s,我们有足够的时间来观察进程的状态),然后在另外一个窗口执行ps ajf,在笔者的机器上输出如下:
vagrant@vagrant:~$ ps ajf
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
16451 16452 16452 16452 pts/0 16540 Ss 1000 0:00 -bash
16452 16540 16540 16452 pts/0 16540 S+ 0 0:00 \_ sudo ./mysleep 100
16540 16541 16540 16452 pts/0 16540 S+ 0 0:00 \_ ./mysleep 100
从输出的UID列可以看出,sudo进程和mysleep进程都运行在root账号(0),接着我们来将mysleep的setuid位打开,执行命令:chmod +s mysleep,然后可以通过ls -l ./mysleep来查看文件的详细信息,如下在笔者电脑上的输出:
vagrant@vagrant:~$ ls -l mysleep
-rwsr-sr-x 1 vagrant vagrant 35000 Oct 8 08:49 mysleep
大家需要注意的是原来x位置变成了s,标记着setuid位被打开,我们重新运行sudo ./mysleep 100,并在另外一个窗口查看进程列表的信息,如下图所示:
vagrant@vagrant:~$ ps ajf
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
16451 16452 16452 16452 pts/0 16546 Ss 1000 0:00 -bash
16452 16546 16546 16452 pts/0 16546 S+ 0 0:00 \_ sudo ./mysleep 100
16546 16547 16546 16452 pts/0 16546 S+ 1000 0:00 \_ ./mysleep 100
从输出的UID列可以看出,sudo进程运行在root账户下(0),而mysleep进程这次运行在1000号用户下(也就是vagrant用户)。setuid位主要用来给应用程序足够的权限来运行。这么说可能有点绕,解释setuid位最好的例子当属ping这个可执行程序了。
ping是Linux操作系统上用来检测某个IP地址是否网络可达的工具,大家有所不知的是,ping可执行程序需要足够的权限来发送网络数据包(ping不经过TCP协议,直接发送IP数据包。在Linux系统中,这种权限也叫capabilities,我们稍后会介绍到)。
虽然说ping是Linux操作系统上检测网络可达一个非常建立的工具,但是从系统管理员的角度,让大家欢乐的使用这个工具并不意味着所有用户都应该具备直接在sockets上发送raw network数据包,因为恶意的攻击者如果具备权限,他肯定不会只发送ping这样无趣的数据包了。
因此,在Linux操作系统上,ping命令在安装的时候会设置setuid位(文件的所有者是root用户),普通的用户就可以有足够的权限来运行ping进行网络问题诊断。接下来我们首先确定一下默认情况下,ping工具的所有者是root用户,并且setuid位已经被设置:
vagrant@vagrant:~$ ls -l /bin/ping
-rwsr-xr-x 1 root root 64424 Jun 28 2019 /bin/ping
从上边输出的s位以及root可以看到默认情况下,ping工具符合我们的预期。接下来我们通过命令cp /bin/ping ./myping来拷贝一份到登陆目录,并且通过ls -l ./myping以及./myping 10.0.0.1来验证默认情况下是否有权限执行,在笔者的机器上操作的输出如下:
vagrant@vagrant:~$ ls -l ./myping
-rwxr-xr-x 1 vagrant vagrant 64424 Oct 8 15:02 ./myping
vagrant@vagrant:~$ ./myping 10.0.0.1
ping: socket: Operation not permitted
在Linux操作系统上,当我们拷贝可执行文件的时候,文件的所有者会被设置为操作用户(也就是用户vagrant),但是为了安全起见,setuid位不会被拷贝,这就是我们从上边输出可以看到s位被标记为x,当我们运行这个叫myping的工具时,操作系统显示没有足够的权限。
我们可以尝试通过sudo chown root ./myping来修改文件的所有者为root,重新运行还是会报没有权限,如下图所示的输出:
vagrant@vagrant:~$ ls -l ./myping
-rwxr-xr-x 1 root vagrant 64424 Oct 8 15:02 ./myping
vagrant@vagrant:~$ ./myping 10.0.0.1
ping: socket: Operation not permitted
接下来我们给myping设置setuid位,执行命令sudo chmod +s ./myping,并运行ls -l ./myping来检查s设置成功,这样我们就不需要通过sudo来运行ping了,如下输出所示:
vagrant@vagrant:~$ sudo chmod +s ./myping
vagrant@vagrant:~$ ls -l ./myping
-rwsr-sr-x 1 root vagrant 64424 Oct 8 15:02 ./myping
vagrant@vagrant:~$ ./myping 10.0.0.1
PING 10.0.0.1 (10.0.0.1) 56(84) bytes of data.
64 bytes from 10.0.0.1: icmp_seq=1 ttl=63 time=3.46 ms
64 bytes from 10.0.0.1: icmp_seq=2 ttl=63 time=3.71 ms
^C
--- 10.0.0.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1002ms
rtt min/avg/max/mdev = 3.463/3.591/3.719/0.128 ms
通过给可执行文件设置setuid位,普通用户就可以有足够的权限来运行ping给网络协议栈发送数据包。读到这里读者可能会说,这不就是以root权限运行ping吗?其实不然,如果你在ping运行的过程中,打开另外一个窗口,并运行ps uf -C myping,从输出的结果你会惊讶的发现,这个进程运行在用户vagrant下,并不是预期的root,你会非常疑惑的看着这个输出,问为什么?
其实啊,在最新版本的操作系统内核中,ping可执行文件在启动的时候,的确以root账户运行,但是当走了足够的权限(capabilities)之后,操作系统会reset用户ID到文件的所有者,这就是我们上边看到的为啥myping即便是设置了setuid之后,有root权限,但还是运行在vagrant账户下。如果你对此还是存在疑问,可以在运行myping之前,先在另外一个窗口启动strace -f -p
我们说了这么多主要目的是想告诉各位读者,如果我们在比如说bash这样的登陆进程上设置了setuid,那么风险就太高了。任何登陆到系统的用户,都会以root用户所具有的权限来运行。不过当前主流的Linux操作系统当然不会这么天真,但是笔者想强调的是,我们运行在容器中的应用进程,很容易犯上边介绍的安全错误。具体来说,我们编写的应用程序不恰当的设置了setuid,运行起来后持有root权限,结果就是黑客如果攻破了容器中的应用程序,那么就可以在进程中运行代码来启动shell(以root运行)。
因此在很多容器安全静态扫描工具中,会非常关注那些设置了setuid的文件和可执行程序,另外笔者建议大家在启动容器进程的时候,最好使用--no-new-privileges选项。
setuid这个标记位是在操作系统发展的早期引入的,当时系统的权限系统还比较简单直接:有权限或者没有权限。而setuid提供了给普通非root用户执行需要root权限可执行程序的机制。从Linux内核版本2.2开始,权限控制的粒度更加细,我们可以通过capabilities来控制用户的权限。
在Linux的最新版本内核上,总共有超过30+的capabilities,我们可以讲capabilities赋予线程来执行对应的操作。比如线程需要CAP_NET_BIND_SERVICE capability来使用小于1024的端口号;CAP_SYS_BOOT用来赋予线程重启系统的能力;CAP_SYS_MODULE用来赋予线程加载或者卸载操作系统内核模块的能力。
回到我们前边一直用来演示的工具ping,线程需要持有CAP_NET_RAW来给网络协议栈直接发送ping数据包。我们首先确定bash命令的进程编号,然后通过getpcaps的输出可以看到bash默认情况下,没有赋予任何capabilities,
vagrant@vagrant:~$ ps
PID TTY TIME CMD
16452 pts/0 00:00:00 bash
16686 pts/0 00:00:00 ps
vagrant@vagrant:~$ getpcaps 16452
Capabilities for `16452': =
vagrant@vagrant:~$ sudo bash
root@vagrant:~# ps
PID TTY TIME CMD
16689 pts/0 00:00:00 sudo
16692 pts/0 00:00:00 bash
16700 pts/0 00:00:00 ps
root@vagrant:~# getpcaps 16692
Capabilities for `16692': = 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+ep
然后我们以root账号运行bash,通过输出的capabilties for部分可以看到shell就具备了很多权限。最后我们来看看如何给myping应用赋予对应的权限来让普通用户可以执行ping命令。
首先我们把之前已经设置过setuid的myping文件删除,然后重新拷贝一份到登陆目录,并且通过命令./myping 10.0.0.1来检查一下默认情况下操作不被允许,没有足够的权限。
接着,我们使用setcap来将权限赋予拷贝过来的myping可执行程序,我们需要通过root命令来给文件赋予某项权限,运行命令sudo setcap 'cap_net_raw+p' ./myping,接下来我们可以运行ls -l ./myping和getcap ./myping来检查文件是否已经持有给网络协议栈发送数据的权限,在笔者的机器上输出如下:
vagrant@vagrant:~$ sudo setcap 'cap_net_raw+p' ./myping
vagrant@vagrant:~$ ls -l ./myping
-rwxr-xr-x 1 vagrant vagrant 64424 Oct 9 00:59 ./myping
vagrant@vagrant:~$ getcap ./myping
./myping = cap_net_raw+p
最后我们重提一下权限提升这个话题,在安全领域,权限提升(privilege escalation)的意思是通过某种手段来提升自己持有的权限,以期执行默认不被允许的操作。恶意攻击者一般都是通过系统的漏洞或者配置漏洞来获取额外的权限来达成攻击的目的。
通常情况下,恶意攻击者都不具备执行特殊操作的权限,因此大部分黑客的首要目标就是获取root权限,一个非常常见的做法就是扫描机器上以root权限运行的进程,然后基于对运行在进程中应用已知安全漏洞信息,来执行恶意代码。如果web服务器以root账户运行,那么任何运能够运行在web服务器中的代码,都可能被黑客控制来提升到root权限,因此我们在运行web应用程序的时候,一定要建立单独的账户。
回到容器领域,默认情况下,容器进程以root账户运行,因此运行在容器中的应用程序有极高的可能性以root账户运行在宿主机上,造成的风险是如果恶意攻击者从容器中逃逸出来,那么就会以root账户运行在宿主机上,那么我们在应用程序的数据安全,稳定性就会收到极大的威胁。
那么是不是不让容器进程以root账户运行就没有风险了,其实不是,我们在这篇文章中介绍了setuid以及赋予非root账户capabilities可以用来提升权限,因此我们必须从应用配置以及操作系统多个维度来审视安全,笔者会在后续的文章中详细介绍how如何做的问题。
好了,这篇文章就真没多了,下篇文章我们继续聊容器的三个基石之一control groups,敬请期待!