转载

Docker安全

整理自《Docker进阶与实战》

Docker的安全性

Docker的安全性主要体现在如下几个方面:

  • Docker容器的安全性这是指容器是否会危害到宿主机或其他容器;

  • 镜像的安全性用户如何确保下载下来的镜像是可信的、未被篡改过的;

  • Docker daemon的安全性如何确保发送给daemon的命令是由可信用户发起的。用户通过CLI或者REST API向daemon发送命令已完成对容器的各种操作,例如通过docker exec命令删除容器里的数据,因此需要保证client与daemon的连接时可信的。

Docker容器的安全性

容器的安全性问题的根源在于容器和宿主机共用内核,因此受攻击的面特别大,另外,如果容器里的应用导致Linux内核崩溃,那么毫无疑问,整个系统哥都会崩溃。这一点与虚拟机是不同的,虚拟机与宿主机的接口非常有限,而且虚拟机崩溃一般不会导致宿主机崩溃。

在共用内核的前提下,容器主要通过内核的Cgroup和Namespace这两大特性来达到容器隔离和资源限制的目的。目前Cgroup对系统资源的限制已经比较完善了,但Namespace的隔离还是不够完善,只有PID、mount、network、UTS、IPC和user这几种。而对于未隔离的内核资源,容器访问时也就会存在影响到宿主机及其他容器的风险。

比如,procfs里的很多接口都没有被隔离,因此通过procfs可以查询到整个系统的信息,例如系统的CPU、内存等资源信息。这也是为什么Docker容器的procfs是以只读方式挂载的,否则修改procfs里的内核参数将会影响甚至破坏宿主机。还有内核syslog也是没有被隔离的,因此在容器内可以看到容器外其他进程产生的内核syslog。

Namespace的隔离非但是不完善的,甚至可以说是不可能完善的。这是共用内核导致的固有缺陷,并且未来Linux内核社区也不会对此做太多的改进。

安全策略 – Cgroup

Cgroup用于限制容器对CPU、内存等关键资源的使用,防止某个容器由于过度使用资源,导致宿主机或者其他容器无法正常运作。

  • 限制CPU

Docker能够指定一个容器的CPU权重,这是一个相对权重,与实际的处理速度无关。事实上,没有办法限制一个容器只可以获得1GHZ的CPU。每个容器默认的CPU权重是1024,简单的说,假设只有两个容器,并且这两个容器竞争CPU资源,那么CPU资源将在这两个容器间平均分配。如果其中一个容器启动时设置的CPU权重是512,那它相对于另一个容器只能得到一半的CPU资源,因此这两个容器可以得到的CPU资源分别是33.3%和66.6%。但如果另外一个容器是空闲的,第一个容器则会被允许使用100%的CPU。简而言之,CPU资源不是预先硬性分配好的,而是跟各个容器在运行时对CPU资源的需求有关。例如,需要为容器设置CPU权重为2000,命令如下:

$ docker run --rm -ti -c 2000 ubuntu bash 

另一方面,Docker也可以明确限制容器对CPU资源的使用上限

$ docker run --rm -ti --cpu-period=500000 --cpu-quota=250000 ubuntu /bin/bash 

上面的命令表示这个容器在每个0.5秒里最多只能运行0.25秒。除此之外,Docker还可以把容器的进程限定在特定的CPU上运行,例如将容器限定在0号和1号CPU上运行

$ docker run --rm -ti --cpuset-cpus=0,1 ubuntu bash 
  • 限制内存

除了CPU,内存也是应用不可或缺的一个资源,因此一般来说必须限制容器的内存使用量。限制命令如下

$ docker run --rm -ti -m 200M ubuntu bash 

这个例子将容器可使用的内存限制在200MB。不过事实并非如此简单,在系统发现内存不足时,会将部分内存置换到swap分区里,因此如果只限制内存使用量,可能会导致swap分区被用光。通过–memory-swap参数可以限制容器对内存和swap分区的使用,如果只是指定-m而不指定–memory-swap,那么总的虚拟内存大小(即memory加上swap)是-m参数的两倍。

  • 限制块设备I/O

对于块设备,因为磁盘带宽有限,所以对于I/O密集的应用,CPU会经常处于等待I/O完成的状态,也就是idle状态,这会造成其他应用可能也要等待那个应用的I/O完成,从而影响到其他的容器。Docker目前只能设置容器的I/O权重,无法限制容器的I/O读写速率的上限,但这个功能已经在开发之中了。现阶段用户可以通过写Cgroup文件来实现。

# 创建容器 $ docker run --rm -ti --name container1 ubuntu bash # 查询容器的写速率 dd if=/dev/zero of=testfile0 bs=8k mount=5000 oflag=direct 

下面通过修改相应的Cgroup文件来限制写磁盘的速度。

# 查找容器挂载的文件系统“/dev/mapper”的位置 $ mount|grep ContainerID # 查看容器挂载的文件系统中的文件(Path为上条命令取得的返回结果) # 返回结果“->”标记后会有一个新的路径,我们先称其为DeviceFilePath $ ls -l Path # 查询容器挂载的设备号 $ ls DeviceFilePath -l # 返回结果中“root disk”后面会有一串用“,”隔开的数字,假设是128和256 # 限制容器的写速度 $ sudo echo '128:256 10240000' >/sys/fs/cgroup/blkio/system.slice/docker-DockerID.scope/blkio.throttle.write_bps_device # 10240000是每秒可写入的最多的字节数。 

ulimit

ulimit是Linux系统中的一个指令,可以对某些类型的资源起到限制作用,包括core dump文件的大小、进程数据段的大小、可创建文件的大小、打开文件的数量、进程栈的大小、CPU时间、单个用户的最大线程数、进程的最大虚拟内存等。

在Docker1.6之前,Docker容器的ulimit设置,继承自Docker daemon。但是很多时候,对于单个容器来说,这样的ulimit实在太高了。在Docker1.6之后,可以设置全局默认的ulimit,如设置CPU时间

$ sudo docker daemon --default-ulimit cpu=1200 

或者在启动容器时,单独对其ulimit进行设置

$ docker run --rm -ti --ulimit cpu=1200 ubuntu bash 

进入容器后可以查看

# ulimit -t # 返回结果:1200 

容器组网

在接入容器隔离不足的情况下,将受信任的和不受信任的容器组网在不同的网络中,可以降低风险。关于Docker的各种网络模型及方案可以参考“Docker网络”

容器 + 全虚拟化

如果将容器运行在全虚拟化环境中(如在虚拟机中运行容器),这样就算容器被攻破,虚拟机还具有保护作用。目前一些安全需求很高的应用场景采用的就是这种方式,如公有云场景。

镜像签名

Docker可信镜像及升级框架(The Update Framework,TUF)是Docker 1.8所提供的一个新功能,可以校验镜像的发布者。当发布者将镜像push到远程仓库时,Docker会对镜像用私钥进行签名,之后其他人pull该镜像的时候,Docker就会用发布者的公钥来校验该镜像是否和发布者所发布的镜像一致,是否被篡改过,是否是最新版。 点击查看更多TUF相关内容 。

日志审计

Docker 1.6版本开始支持日志驱动,用户可以将日志直接从容器输出到如syslogd这样的日志系统中,通过docker –help可以看到Docker daemon支持log-driver参数,目前支持的类型有none、json-file、syslog、gelf和fluentd,默认的日志驱动是json-file。

除了在启动Docker daemon时可以指定日志驱动以外,也可以对单个容器指定驱动,如

$ docker run --rm -ti --log-driver="syslog" ubuntu bash 

通过 docker inspect ContainerID 可以看到容器使用了哪种日志驱动。另外,只有 json-file 支持 docker logs 命令, docker logs ContainerID

监控

在使用容器时,应注意监控容器的信息,以便及时补救。这些信息包括运行状态、容器的资源使用情况等。

查看容器的运行状态,命令: docker ps -a

容器的资源使用情况主要指容器对内存、网络I/O、CPU、磁盘I/O的使用情况等,命令: docker stats ContainerID

文件系统级防护

Docker可以设置容器的根文件系统为只读模式,只读模式的好处是即使容器与host使用的是同一个文件系统,也不用担心会影响甚至破环host的根文件系统。但需要注意的是,必须把容器里remount进程的文件系统能力禁掉,否则在容器内又可以把文件系统重新挂载为可写。用户甚至可以禁止容器挂载任何文件系统。

可读写挂载:

$ docker run --rm -ti ubuntu bash # echo "hello" >/home/test.txt # cat /home/test.txt 

只读挂载:

$ docker run --rm -ti --read-only ubuntu bash # echo "hello" >/home/test.txt # 返回结果:bash:/home/test.txt:Read-only file system 

capability

从2.2版开始,Linux有了capability的概念,它打破了Linux操作系统中超级用户/普通用户的概念,让普通用户也可以做只有超级用户才能完成的工作。capability可以作用在进程上,也可以作用在程序文件上。它与sudo不同,sudo可以配置某个用户可以执行某个命令或更改某个文件,而capability则是让程序拥有某种能力。

每个进程有三个和能力有关的位图: Inheritable(I)/Permitted(P)/Effective(E) ,我们可以通过 /proc/<PID>/status 来查看进程的capability。

命令:cat /proc/$$/status | grep Cap 结果: # 能够被当前进程执行的程序继承的capability。 CapInh: 0000000000000000 # 进程能够使用的能力,可以包含CapEff中没有的能力,这些能力是被进程自己临时放弃的,因此可以把CapEff看作是CapPrm的一个子集。 CapPrm: ffffffffffffffff # 当一个进程要进行某个特权操作时,操作系统会检查CapEff的对应位是否有效,而不再是检查进程的有效UID是否为0。 CapEff: ffffffffffffffff 

如需了解更多请查阅Linux手册。

Docker启动容器的时候,会通过白名单的方式来设置传递给容器的capability,默认情况下,这个白名单只包含CAP_CHOWN等少数的能力。用户可以通过 -–cap-add-–cap-drop 这两个参数来修改这个白名单。

$ docker run --rm -ti --cap-drop=chown ubuntu bash # chown 2.2/etc/hosts # 返回结果:chown:changing ownership of '/etc/hosts': Operation not permitted 

发现禁掉CAP_CHOWN能力后,在容器里就无法改变容器的所有者了。如果不禁掉则正常。如下

$ docker run --rm -ti ubuntu bash # chown 2.2/etc/hosts 

容器应遵循最小权限原则,尽量不要用–privileged参数,不需要的能力全部去掉,甚至禁掉所有的能力。

$ docker run --rm -ti --cap-drop=all ubuntu bash 

SELinux

操作系统中访问控制安全的发展:早期的操作系统几乎没有考虑安全问题,一个用户可以访问任何文件或资源,但很快出现了访问控制机制来增强安全性,其中主要的访问控制在今天被称为自主访问控制(DAC – Discretionary[dɪ’skrɛʃə’nɛri] Access Control)。DAC通常允许授权用户(通过其程序如一个shell)改变客体的访问控制属性,这样就可指定其他用户是否有权访问该客体。大部分DAC机制是基于用户身份访问控制属性的,通常表现为该访问控制列表机制。DAC的主要特性是,单个用户(通常指某个资源的属主)可以指定其他人是否能访问该资源。

但是,DAC也有其自身的安全脆弱性,它只约束了用户、同用户组内的用户、其他用户对文件的可读、可写、可执行权限,这对系统的保护作用非常有限。为了克服这种脆弱性,出现了强制访问控制(MAC – Mandatory[‘mændət(ə)rɪ] Access Control)机制,其基本原理是利用组织的安全策略来控制对客体的访问,且这种访问不被单个程序所影响。此项研究最早由军方资助,目的是保护机密政府部门数据的机密性。

SELinux(Security-Enhanced Linux)是美国国家安全局(NSA)对于强制访问控制的实现,它是Linux历史上最杰出的安全子系统。在这种访问控制体系的限制下,进程只能访问那些在它的任务中所需的文件。对于目前可用的Linux安全模块来说,SELinux功能最全面,而且测试最充分,它是基于对MAC 20年的研究基础上建立的。

SELinux定义了系统中每个用户、进程、应用和文件访问及转变的权限,然后使用一个安全策略来控制这些实体(即用户、进程、应用和文件)之间的交互,安全策略指定了如何严格或宽松的进行检查。另外,SELinux比较复杂。

SELinux跟内核模块一样,也有模块的概念,需要先根据规则文件编译出二进制模块,然后插入到内核中。在使用SELinux前,需要安装一些包,以Fedora 20为例,需要安装以下组件:

  • checkpolicy

  • libselinux

  • libsemanage

  • libsepol

  • policycoreutils

源码可以在 https://github.com/SELinuxProject/selinux 中找到,但建议不要自己来编译,太耗费时间,而且编译了也不见得好用。若自己开发SELinux策略,还需安装 selinux-policy-devel。

在Fedora 20中,可以直接用yum安装SELinux开发所需的工具

$ sudo yum -y install libselinux.x86_64 libselinux-devel.x86_64 libselinux-python.x86_64 libselinux-utils.x86_64 $ sudo yum -y install selinux-policy.noarch selinux-policy-devel.noarch selinux-policy-targeted.noarch $ sudo yum -y install crossfire-selinux.x86_64 libselinux-devel.i686 checkpolicy.x86_64 policycoreutils.x86_64 $ sudo yum -y install policycoreutils-devel.x86_64 selinux-policy-devel.noarch selinux-policy-targeted.noarch 

安装完成之后就可以学习一个Github上的例子,获取源码

$ git clone https://github.com/pcmoore/getpeercon_server.git 

查看策略文件

$ cd getpeercon_server $ cat selinux/gpexmple.te 

会发现返回一坨天文似的东西,想看懂还需要看一本《SELinux by Example》。查看测试程序的代码

$ cat src/getpeercon_server.c...... 

又是返回一坨。上面的代码中有创建tcp socket及bind tcp端口的动作,但是上面的策略文件中没有bind tcp端口的策略,不能成功的bind tcp端口。要测试该例子,首先需要创建SELinux模块

$ sudo make build $ sudo make install 

然后关闭SELinux,再插入新编译的模块,重启SELinux,并打上正确的标签

$ sudo setenforce 0 $ sudo semodule -i selinux/gpexmple.pp $ sudo setenforce 1 $ sudo restorecon /usr/bin/getpeercon_server 

之后运行getpeercon_server

$ getpeercon_server 8080 

就像多数事情一样不会一帆风顺,出错了,查看日志

$ cat /var/log/audit/audit.log...... 

根据日志中“success=no exit=-13”了解到getpeercon_server执行失败,SELinux阻止了getpeercon_server绑定端口。解决方法

$ cat /var/log/audit/audit.log | audit2allow -m local  

这里的audit2allow是用Python写的一个命令,用来处理日志,把日志中违反策略的动作记录转换成access vector。然后把这条命令复制输出到gpexmple.te中,然后编译插入操作,在运行

$ getpeercon_server 8080  

启动成功。感觉例子好复杂。但在Docker中使用SELinux却非常简单。Docker使用SELinux的前提是系统支持SELinux,SELinux功能已经打开,并且已插入了Docker的SELinux模块,目前RHEL 7、Fedora 20都已自带该模块。查看系统是否支持Docker的SELinux环境

$ sudo semodule -l |grep docker 

如果有Docker的SELinux模块(即返回类似docker 1.0.0的信息),说明该系统已经支持Docker的SELinux环境。Docker SELinux模块已经帮我们做了复杂的SELinux策略,我们只需在 Docker daemon 启动的时候加上 --selinux-enabled=true 选项就可以使用SELinux了。

$ sudo docker daemon --selinux-enabled=true 

另外也可以在启动容器时,使用 –-security-opt 选项对指定的文件做限制(前提是 Docker daemon 启动时加了 --selinux-enabled=true )

AppArmor

AppArmor也是一种MAC控制机制,其主要作用是设置摸个可执行程序的访问控制权限,可以限制程序读/写某个目录/文件,打开/读/写网络端口等。AppArmor是一个高效和易于使用的Linux系统安全特性,它对操作系统和应用程序进行了从内到外的保护,即使是0day漏洞和未知的应用程序漏洞所导致的攻击也可被识破。AppArmor安全策略可以完全定义个别应用程序所能访问的系统资源与各自的特权,它包含了大量的默认策略,并将先进的静态分析和基于学习的工具结合了起来,可以在很短的时间内,为非常复杂的应用制定AppArmor规则。配置文件官方文档。

Docker daemon在启动过程中会判断当前内核是否支持AppArmor,若支持,就创建默认的AppArmor配置文件/etc/apparmor.d/docker,并应用这个配置文件。启动容器时,在初始化过程中Docker会使用相应的AppArmor配置作用于容器。也可以使用–security-opt选项来指定作用于容器的AppArmor配置文件。

制定一个AppArmor规则,并应用到容器上。先拷贝一个模板

$ sudo cp /etc/apparmor.d/docker /etc/apparmor.d/container 

编辑/etc/apparmor.d/container,将

profile docker-default flags=(attach_disconnected,mediate_deleted){ 

改为

profile container-default flags=(attach_disconnected,mediate_deleted){ 

上面修改的主要是配置的名字,该名字在传给Docker时要用到,也可以修改配置文件中其他的内容(如在配置文件中加入“deny/etc/hosts rwklx,”一行,容器启动后执行“cat /etc/hosts”命令时会发现容器没有权限读取/etc/hosts的内容了)。使用这个配置

$ docker run --rm -ti --security-opt apparmor:container-default ubuntu bash 

在使用Docker的过程中,最好打开SELinux或AppArmor。目前在支持SELinux的系统上,Docker的SELinux是默认关闭的,需要在启动 Docker daemon 时加上 --selinux-enabled=true 参数。而在支持AppArmor的系统上,AppArmor的功能默认是开启的。

Seccomp

Seccomp(secure computing mode)是一种Linux内核提供的安全特性,它可以实现应用程序的沙盒机制,以白名单或黑名单的方式限制进程进行系统调用。

Seccomp首次于内核2.6.12版合入Linux主线。早期的Seccomp只支持过滤少数几个系统调用。较新版本的内核支持动态Seccomp策略,也就是seccomp-bpf,因为支持用BPF生成过滤规则,从而使Seccomp可以限制任意的系统调用,并且可以限制系统调用传入的参数。

Seccomp的使用

  1. 生成BPF形式的过滤规则;

  2. 调用prctl系统调用将规则传入内核。

在Docker容器启动的过程中,会对Seccomp设置一个默认的配置,但目前还不支持命令行参数做配置。点击了解更多Seccomp相关。

grsecurity

grsecurity提供了一个系统的内核patch,使Linux内核的安全性大大增强,并且它提供了一些工具让用户配置、使用这些安全特性。grsecurity可以用来控制资源访问权限。下面是一张关于grsecurity、SELinux和AppArmor的对比图。

原文  https://segmentfault.com/a/1190000005794220
正文到此结束
Loading...