.. include:: ../../sumaccess.rst =================== Docker与容器安全 =================== 容器安全是目前Docker社区极为关注的一个问题,Docker能否大规模用于生产环境,尤其是公有云环境, 关键就在于Docker是否能提供安全的环境。我们将围绕Docker目前的安全机制、使用Docker过程中可能存在的 安全问题以及如何增强Docker安全这三个方面,一起来探讨Docker与容器安全的问题。 Docker的安全机制 ================= Docker目前已经在安全方面做了一定的工作,包括Docker daemon在以TCP形式提供服务的同时使用传输层安全协议; 在构建和使用镜像时会验证镜像的签名证书;通过cgroups及namespaces来对容器进行资源限制和隔离; 提供自定义容器能力(capability)的接口;通过定义seccomp profile限制容器内进程系统调用的范围等。 如果合理地实现上述安全方案,可以在很大程度上提高Docker容器的安全性。 Docker daemon安全 --------------------- Docker向外界提供服务主要有以下形式:默认是以Unix域套接字的方式来与客户端进行通信。这种方式相对于TCP形式比较安全, 只有进入daemon宿主机所在机器并且有权访问daemon的域套接字才可以和daemon建立通信。 如果以TCP形式向外界提供服务,可以访问到daemon所在主机的用户都可能成为潜在的攻击者。同时,由于数据传输需要通过网络进行, 数据可能被截获甚至修改。为了提高基于TCP的通信方式的安全性,Docker为我们提供了TLS(Transport Layer Security)传输层安全协议 。在Docker中可以设置 ``--tlsverify`` 来进行安全传输检验,通过 ``--tlscacert`` (信任的证书)、 ``--tlskey`` (服务器或者客户端秘钥)、 --tlscert(证书位置)3个参数来配置。安全认证主要是在服务器端设置,客户端可以对服务端进行验证。客户端在访问daemon时只需 要提供签署的证书,那么就可以使用Docker daemon服务。 镜像安全 ---------- Docker目前提供registry访问权限控制以保证镜像安全。另外,Docker从1.3版本开始就有了镜像数字签名功能, 用以防止官方镜像被篡改或损坏,以此来保证官方镜像的完整性,但是镜像校验功能仅当访问官方V2 registry时才会生效, 需要用户进行docker login登录。 Docker registry访问控制 +++++++++++++++++++++++++++++ 目前Docker使用一个中心验证服务器来完成Docker registry的访问权限控制,每一个Docker客户端对registry进行pull/push操作的时候 都会经过如下6个步骤。 1. 客户端尝试对registry发起push/pull操作。 #. 如果registry的访问需要认证,registry就会返回一个含有如何完成认证challenge的401 Unauthorized HTTP响应。 #. 客户端向认证服务器请求一个Bearer token。 #. 认证服务器返回给客户端一个加密的Bearer token,用来代表客户端被授权的访问权限。 #. 客户端再次尝试用头部嵌有Bearer token的请求向原来的registry发起请求。 #. registry验证客户端请求中的Bearer token及其包含的授权空间权限。如果正确,便建立与客户端的push/pull会话。 验证校验和 ++++++++++ 镜像校验和用来保证镜像的完整性,以预防可能出现的镜像破坏。Docker registry下的每一个镜像都对应拥有自己的manifest文件以及 该文件本身的签名。其中的信息包括镜像所在的命名空间、镜像在此仓库下对应的标签、镜像校验方法及校验和、 镜像形成时的运行信息以及manifest文件本身的签名。 在Docker pull镜像的过程中进行了多次根据内容哈希的验证。如果在命令行中用digest拉取镜像,则会验证拉取manifest的digest (一种根据manifest内容计算的校验和)与传入的digest是否一致;在根据manifest中镜像ID拉取镜像配置文件后, 会根据配置文件内容生成digest并验证与镜像ID是否一致;在下载manifest中引用的镜像层后,会根据镜像文件计算出校验和diffID, 并与镜像配置文件中记录的diffID验证对比。每一步不可靠的网络传输后都会计算校验和与前一步的可靠结果进行验证, 这些校验过程保证了镜像内容的可靠性。 内核安全 ---------- 内核为容器提供两种技术cgroups和namespace,分别对容器进行资源限制和资源隔离,使容器仿佛是在使用一台独立主机环境。 cgroups资源限制 +++++++++++++++++ 容器本质上是进程,cgroups的存在就是为了限制宿主机上不同容器的资源使用量,避免单个容器耗尽宿主机资源而导致其他容器异常。 namespace资源隔离 +++++++++++++++++++ 为了使容器处在独立的环境中,Docker使用namespace技术来隔离容器,使容器与容器之间、容器与宿主机之间相互隔离。 Docker对uts、ipc、pid、network、mount这5种namespace有完整的支持,而Docker 1.10版本的发布又增加了对user namespace的支持。 在Docker 1.10版本中,只要用户在启动Docker daemon的时候指定了--userns-remap,那么当用户运行容器时, 容器内部的root用户并不等于宿主机内的root用户,而是映射到宿主上的普通用户。除了上述资源之外,还有诸多系统资源未进行隔离, 如/proc和/sys信息未完成隔离,SELinux、time、syslog和/dev设备等信息均未隔离。 可见在内核安全方面,Docker距离真正的安全还有一定的距离。 容器之间的网络安全 ------------------ Docker daemon指定 ``--icc`` 标志的时候,可以禁止容器与容器之间通信,主要通过设定iptables规则实现。 Docker容器能力限制 ----------------------- docker run参数中提供了容器能力配置的接口,可以在创建容器时在容器默认能力的基础上对容器的能力进行增加或者减少,配置命令如下: .. code:: bash Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...] Run a command in a new container Options: ...... --cap-add list Add Linux capabilities --cap-drop list Drop Linux capabilities 什么是能力呢?Linux超级用户权限划分为若干组,每一组代表了所能执行的系统调用操作,以此来切割超级用户权限。 比如NET_RAW表示用户可以创建原生套接字。如果是root用户,但是被剥夺了这些能力,那么依旧无法执行系统调用。 这样做的好处是可以分解超级用户所拥有的权限。对于普通用户,有时需要使用超级用户权限的部分能力, 但是为了安全又不便把该普通用户提升为超级用户,此时可以考虑为该用户增加一些能力,但不需要赋予其所有超级用户权限。 Docker正是使用这种方法在更细的粒度上限制容器进程所能使用的系统调用。 容器默认拥有的能力包括CHOWN、DAC_OVERRIDE、FSETID、FOWNER、MKNOD、NET_RAW、SETGID、SETUID、SETFCAP、SETPCAP、 NET_BIND_SERVICE、SYS_CHROOT、KILL和AUDIT_WRITE,其中较为主要的几个作用如下: **CHOWN** 允许任意更改文件UID以及GID。 **DAC_OVERRIDE** 允许忽略文件的读、写、执行访问权限检查。 **FSETID** 允许文件修改后保留setuid/setgid标志位。 **SETGID** 允许改变进程组ID **** **SETUID** 允许改变进程用户ID **SETFCAP** 允许向其他进程转移或者删除能力 **NET_RAW** 允许向其他进程转移或者删除能力。 **MKNOD** 允许使用mknod创建指定文件 **SYS_REBOOT** 允许使用reboot或者kexec_load。kexec_load功能是加载新的内核作为reboot重新启动所需内核 **SYS_CHROOT** 允许使用chroot。 **KILL** 允许发送信号 **NET_BIND_SERVICE** 允许绑定常用端口号(端口号小于1024) **AUDIT_WRITE** 允许审计日志写入 削减能力 +++++++++++++ 可以通过命令削减Docker容器进程的能力,假设不需要使用SETGID、SETUID的能力,可以执行如下操作。 .. code:: bash $ docker run --rm --name test1 --cap-drop SETUID --cap-drop SETGID -d ubuntu:trusty sleep 600 削减了这两个能力以后,容器进程试图调用setuid、setgid时会执行失败。 可以通过docker inspect命令查看容器的进程cid: .. code:: bash $ yum install libcap-ng-utils -y # 安装pscap工具 $ docker inspect test1 | grep "Pid" "Pid": 109728, "PidMode": "", "PidsLimit": null, $ pscap |grep 109728 $ pscap|grep sleep 109709 109728 root sleep chown, dac_override, fowner, fsetid, kill, setpcap, net_bind_service, net_raw, sys_chroot, mknod, audit_write, setfcap 可以看到,此时容器的进程能力中已经没有SETGID和SETUID能力了。 增添能力 +++++++++++++ 如果容器需要使用默认能力之外的能力,可以通过在docker run时使用--cap-add参数来增加能力,如给容器增加一个允许修改系统时间的命令 .. code:: bash $ docker run -d --name ntpd --rm --cap-add SYS_TIME ntpd/ntpd sleep 600 同上,查看进程的能力: .. code:: bash $ docker inspect ntpd |grep Pid "Pid": 110655, "PidMode": "", "PidsLimit": null, $ pscap |grep 110655 110655 110697 root s6-supervise chown, dac_override, fowner, fsetid, kill, setgid, setuid, setpcap, net_bind_service, net_raw, sys_chroot, sys_time, mknod, audit_write, setfcap 可以看到容器中已经增加了sys_time能力,可以修改系统时间了。 seccomp ---------- 从Docker 1.10版本开始,Docker安全特性中增加了对seccomp的支持。seccomp(secure computing mode)是Linux的一种内核特性, 可用于限制进程能够调用的系统调用(system call)的范围,从而减少内核的攻击面,被广泛用于构建沙盒。 使用seccomp ++++++++++++ 使用seccomp的前提是Docker构建时已包含seccomp,并且内核中的CONFIG_SECCOMP已开启。可使用如下方法检查内核是否支持seccomp: .. code-block:: bash :linenos: :emphasize-lines: 2,3 $ cat /boot/config-`uname -r` |grep CONFIG_SECCOMP CONFIG_SECCOMP_FILTER=y CONFIG_SECCOMP=y .. NOTE:: Docker目前使用seccomp 2.2.1版本,该版本仅在如下发行版中得到支持:Debian 9“Stretch”、Ubuntu 15.10 “Wily”、Fedora 22、CentOS 7和Oracle Linux 7。 在Ubuntu 14.04上启用Docker的seccomp功能,需要下载最新的Docker Linux Binary。 seccomp profile +++++++++++++++++++ 在Docker中,我们通过为每个容器编写json格式的seccomp profile来实现对容器中进程系统调用的限制。 Docker也提供了默认seccomp profile供所有容器使用,默认的 seccomp 配置文件为使用 seccomp 运行容器提供了一个合理的设置, 并禁用了大约 44 个超过 300+ 的系统调用。它具有适度的保护性,同时提供广泛的应用兼容性。默认的 Docker 配置文件可以在官网 [#]_ 找到。 默认seccomp profile片段如下: .. code:: bash { "defaultAction": "SCMP_ACT_ERRNO", "archMap": [ { "architecture": "SCMP_ARCH_X86_64", "subArchitectures": [ "SCMP_ARCH_X86", "SCMP_ARCH_X32" ] }, { "architecture": "SCMP_ARCH_AARCH64", "subArchitectures": [ "SCMP_ARCH_ARM" ] }, { "architecture": "SCMP_ARCH_MIPS64", "subArchitectures": [ "SCMP_ARCH_MIPS", "SCMP_ARCH_MIPS64N32" ] }, { "architecture": "SCMP_ARCH_MIPS64N32", "subArchitectures": [ "SCMP_ARCH_MIPS", "SCMP_ARCH_MIPS64" ] }, { "architecture": "SCMP_ARCH_MIPSEL64", "subArchitectures": [ "SCMP_ARCH_MIPSEL", "SCMP_ARCH_MIPSEL64N32" ] }, { "architecture": "SCMP_ARCH_MIPSEL64N32", "subArchitectures": [ "SCMP_ARCH_MIPSEL", "SCMP_ARCH_MIPSEL64" ] }, { "architecture": "SCMP_ARCH_S390X", "subArchitectures": [ "SCMP_ARCH_S390" ] } ], "syscalls": [ { "names": [ "accept", "accept4", "access", "adjtimex", ... ] } ] } 其中name是系统调用的名称,action是发生系统调用时seccomp的操作,args是系统调用的参数限制条件。 seccomp profile包含3个部分: - 默认操作(default Action) - 系统调用所支持的Linux架构(architectures) - 系统调用具体规则(syscalls) 在seccomp profile规则中,可定义以下5种行为来对进程的系统调用行为做出响应。 **SCMP_ACT_KILL** 当进程调用某系统调用,内核会发出SIGSYS信号终止该进程,该进程不会接收到这个信号。 **SCMP_ACT_TRAP** 当进程调用某系统调用,该进程会接收到SIGSYS信号,并根据该信号改变自身的行为。 **SCMP_ACT_ERRNO** 当进程调用某系统调用,系统调用失败,进程会接收到返回值,该返回值与Linux内核的errno对应。 **SCMP_ACT_TRACE** 当进程调用某系统调用,进程会被跟踪。 **SCMP_ACT_ALLOW** 进程系统调用被允许。 为具备广泛应用场景的Docker容器提供放之四海皆准的默认系统调用限制规则是一件难度很高的事情。当前Docker提供的默认seccomp profile只能限制少量“问题系统调用”,也就是64位Linux内核提供的313个系统调用中的44个。而限制更多的系统调用则可能会影响容器 化应用的正常功能。 使用seccomp profile ++++++++++++++++++++++ 默认情况下,Docker运行容器时会使用默认的seccomp profile。可将unconfined传入docker run的security-opt seccomp 选项禁用默认的seccomp profile: .. code:: bash $ docker run --rm -d --security-opt seccomp:unconfined hello-world 也可通过docker run的security-opt seccomp选项,使用特定的seccompprofile: .. code:: bash $ docker run --rm -d --security-opt seccomp:/path/to/seccomp/profile.json hello-world 理想状况下,不同的Docker容器应根据自身的运行需要,定义个性化的seccomp profile,限制运行时的系统调用。 但定制化seccomp profile并非易事,普通程序员可能为了防止容器内的应用出现难以预料的故障而放弃限制更多的系统调用。 Docker安全问题 ================ Docker在安全方面已经做了不少工作,但Docker安全问题仍然非常多,社区对此极为关注。本节将从资源限制、容器逃逸、 容器内网络攻击以及超级权限等问题来描述可能出现的安全问题。 磁盘资源限制问题 ------------------ 容器本质上是一个进程,通过通过镜像层叠的方式来构建容器的文件系统。当需要改写文件时,把改写的文件复制到最顶层的读写层, 其本质上还是在宿主机文件系统的某一目录下存储这些信息。所有容器的rootfs最终存储在宿主机上。所以,极有可能出现一个容器把宿 主机上所有的磁盘容量耗尽的情况,届时其他容器将无法进行文件存储操作,所以有必要对容器的磁盘使用量进行限制。 后面再介绍对磁盘容量进行限制的方法。 容器逃逸问题 -------------- 容器的安全问题一直制约着容器技术的进一步发展,在全虚拟化和半虚拟化中,每一个租户都独立运行一个内核, 比Docker使用操作系统虚拟化更安全。操作系统虚拟化指的是共享内核、内存、CPU以及磁盘等,所以容器的安全问题特别突出, 其中尤以容器逃逸问题最为著名。当容器从监狱(jail)中逃出时,所有容器以及宿主机都将受到威胁。容器逃逸的情况很多, 目前已经发生多次容器逃逸的事件,下面为大家介绍2014年的一个案例,该案例通过open_by_handle_at调用暴力扫描宿主机文件系 统获取宿主机敏感文件,以此达到逃逸效果。 这个案例即著名的shocker.c程序,下面对这个程序如何逃逸做出分析。 .. code:: int open_by_handle_at(int mount_fd, struct file_handle *handle, int flags): 其中传入的参数含义如下: - file_handle: 描述了一个文件或者目录。file_handle是本程序的关键所在,在64位操作系统中大小为8个字节,前面4个字节为文件inode号。 - mount_fd:指向某一文件系统中文件或目录的文件描述符,此文件系统为file_handle所描述的文件或目录所在的文件系统。 下面大概讲述下逃逸的过程: 1. 程序首先打开从宿主机文件系统挂载到容器内的某一文件,以获取宿主机的文件描述符引用。这个引用作为open_by_handle_at的第一个参数。注意,之后的所有扫描操作都会在宿主机的文件系统上进行,而非容器自身的rootfs。 #. 构造根目录/的file_handle类型的数据,在大部分文件系统中根目录的inode编号为2,所以在f_handle中指定节点编号。 #. 打开file_handle所描述的目录,首先通过open_by_handle_at返回此目录的内部描述符,然后打开此文件描述符,遍历此目录下的所有文件。比较目录的名字和所要查找文件的目录,比如需要查找/etc/shadow,首先去匹配etc字符串,然后匹配shadow文件。找到对应的子目录就记录其inode号。 #. 递归执行查找操作。在上一步已经查找到了etc目录的inode号,此时重新构造file_handle结构体。后面4个字节采用暴力的方式来设置,从0到0xffffffff进行遍历,然后再通过open_by_handle方法来打开这个目录或者文件,如果可以打开,说明后面4个字节设置正确,新的file_handle已经扫描完成,重新构造file_handle,此时再次回到上一步。 #. 递归结束条件。查找“/”字符,如果已经到最后一个文件,比如/etc/shadow上,那么将会查找失败,此时将结束递归。将输入的最后找到文件file_handle描述符输出到oh参数中,此时需要查找的文件的file_handle结构已经构造完成。 #. 在第(5)步结束时,已经获取到目标文件/etc/shadow的file_handle,通过open_by_handle来构造目标文件的文件描述符,注意,此时构造出来的文件描述符将指向宿主机的/etc/shadow文件描述符,而非容器内的/etc/shadow文件。 #. 在第(6)步中已经获取了宿主机目标文件的文件描述符,此时可以对目标文件进行任何操作,如果需要对目标文件进行其他操作,需要在open_by_handle_at时指定对应的参数。 至此,至此容器已经成功逃逸,宿主机目标文件已经被成功读取。再来梳理一下这个过程,它主要是利用open_by_handle_at调用, 通过构造file_handle来获取目标文件的inode索引号,然后利用获得的inode索引再次构造file_handle,并调用open_by_handle_at函数, 如此循环缩小范围,最终读取目标文件。简单来说,就是Docker从宿主机上挂载了dockerinit到容器内, 导致程序可以调用open_by_handle_at去扫描宿主机的文件系统。 分析此次逃逸的问题,归根结底还是在于没有禁止open_by_handle_at的能力,通过man capabilities查找帮助文档, 可以看到对应的能力为CAP_DAC_READ_SEARCH。 这个问题在Docker 1.0版本之前都会出现,因为Docker采用黑名单形式来限制容器的能力,只禁止列出来的部分能力, 所以会出现通过open_by_handle_at进行暴力扫描的问题。而在1.0版本之后采用白名单的形式来限制容器的能力, 会给出一份默认的能力清单,除清单上的能力外,其他均禁止。 容器DoS攻击与流量限制问题 ------------------------- 目前,在公网上的DoS攻击(deny-of-service,拒绝服务攻击)预防已经有很成熟的产品,这对传统网络有比较好的防御效果, 但是随着虚拟化技术的兴起,攻击数据包可能不需要通过物理网卡就可以攻击同一个宿主机下的其他容器。 所以传统DoS预防措施对容器之间的DoS攻击没有太大效果。 默认的Docker网络是网桥模式,所有容器连接到网桥上。容器通过veth pair技术创建veth pair网卡,然后将其中一端放入容器内部并且 命名为eth0,另外一张网卡留在宿主机网络环境中。容器内网卡发出的数据包都会发往宿主机上对应网卡,再由物理网卡进行转发。 同理,物理网卡收到的数据根据地址会相应发送到不同的容器内。实际上所有容器在共用一张物理网卡。如果在同一宿主机中的某一个容器 抢占了大部分带宽,将会影响其他容器的使用,例如大流量的容器内下载程序会影响其他交互式应用的访问。 超级权限问题 ------------- Docker在0.6版本的时候给容器引入了超级权限,可以在docker run时加上 ``--privileged`` 参数,使容器获得超级权限。 下面来看看 ``--privileged`` 之后做了什么?Docker首先去检测docker run时是否指定了 ``--privileged`` 标志, 如果指定就调用setPrivileged这个操作。setPrivileged函数完成两件事情,一是获取所有能力赋值给容器, 二是扫描宿主机所有设备文件挂载到容器内。 首先执行container.Capabilities = capabilities.GetAllCapabilities() 获取超级用户权限的所有能力,然后将其设置为container 的能力。之后再执行hostDeviceNodes, err :=devices.GetHostDeviceNodes() 获取宿主机的设备文件, 将其设置为MountConfig参数传入。下面分别来解释一下这两个函数做了什么。 下面分别来解释一下这两个函数做了什么。 GetAllCapabilities做了什么 +++++++++++++++++++++++++++++ GetAllCapabilities()函数相当于把超级用户的权限全部赋值给当前容器,也就是当前容器不再受超级权限能力的限制。 - 执行普通测试容器,并根据容器pid查找/proc/pid/status的capabilities位图 .. code-block:: bash :linenos: $ docker run --rm --name con1 -d ubuntu:trusty sleep 60 d12203b00017215abed14fa27b88ce18739a529c41b8fa9f865e53b1567adf02 $ docker inspect con1 |grep Pid "Pid": 123121, "PidMode": "", "PidsLimit": null, $ cat /proc/123121/status ...... CapInh: 00000000a80425fb CapPrm: 00000000a80425fb CapEff: 00000000a80425fb CapBnd: 00000000a80425fb ...... - 执行特权容器,再查看capabilities位图 .. code-block:: bash :linenos: $ docker run --rm --name con2 -d --privileged ubuntu:trusty sleep 600 d085b7d84e2c6ecc1404f8d04af5fcd667c2bcc5f24a440dbdc9330b1384a21b $ docker inspect con2 |grep Pid "Pid": 123918, "PidMode": "", "PidsLimit": null, $ cat /proc/123918/status ...... CapInh: 0000001fffffffff CapPrm: 0000001fffffffff CapEff: 0000001fffffffff CapBnd: 0000001fffffffff ...... GetHostDeviceNodes做了什么 ++++++++++++++++++++++++++++ GetHostDeviceNodes将会获取宿主机目录下的所有设备文件,并将其设置到容器。 查看下Docker容器没有--privlieged参数时,即普通权限下/dev目录下的文件。 .. code:: bash $ docker run --rm --name con1 -it ubuntu root@aa17f5e0140c:/# ls /dev console core fd full mqueue null ptmx pts random shm stderr stdin stdout tty urandom zero root@aa17f5e0140c:/# exit exit 相比之下,再来看看加了 ``--privileged`` 参数时,即超级权限下的/dev目录文件。 .. code:: bash $ docker run --rm --name con2 --privileged -it ubuntu root@59d245107dc9:/# ls /dev agpgart hwrng ppp snapshot tty16 tty3 tty43 tty57 uhid vcsa4 autofs input ptmx snd tty17 tty30 tty44 tty58 uinput vcsa5 bsg kmsg pts sr0 tty18 tty31 tty45 tty59 urandom vcsa6 ...... 可以看到,加了特权后,宿主机所有设备文件都挂载在容器内。 .. WARNING:: ``--privilege`` 参数给的权限太多,使用 ``--privileged`` 时需要慎重考虑。如果需要挂载某个特定的设备,可以通过 --device=/dev/sdc:/dev/xvdc:rwm操作,来把容器需要使用的设备定向挂载到容器,而不是把宿主机的全部设备挂载到容器上。 此外,可以通过--add-cap和--drop-cap这两个Docker参数来对容器的能力进行调整,以最大限度地保证容器使用的安全。 Docker安全的解决方案 ====================== Docker通过一些额外的工具来加强安全。比如,使用SELinux限制进程访问的资源;使用quota等技术限制容器磁盘使用量;使用traffic controller技术对容器的流量进行控制。通过这些工具的配合来加强Docker的安全。 SELinux --------- SELinux是由内核实现的MAC(Mandatory Access Control,强制访问控制),可以说SELinux就是一个MAC系统。 SELinux为每一个进程设置一个标签,称为进程的域,为文件设置标签,称为类型。每一标签由User、Role、Type和Level这4部分组成。 在Docker中使用SELinux呢?原因可简单总结为以下3点: - SELinux把所有进程和文件都打上标签。进程之间相互隔离,SELinux策略控制进程如何访问资源,也就是限制容器如何去访问资源。 - SELinux策略是全局的,它不是针对具体用户设定,而是强制整个系统去遵循,使攻击者很难突破。 - 减少提权攻击的风险,如果一个进程被攻陷,攻击者将会获得该进程的所有权限,访问该进程能访问的权限。 比如Apache的httpd进程被攻陷,那么它仅能访问httpd所能访问的文件,而无法去访问其他目录的文件(如/home、/etc/passwd等目录 就不行),防止了更为严重的危害。 虽然SELinux功能强大,但是注意SELinux不是一个杀毒软件,不能替换防火墙、密码等其他安全体系。SELinux不是对现有的安全 体制进行替换,而是增添一道严格的防线而已。 磁盘限额 ---------- Docker目前提供--storge-opt=[]来进行磁盘限制,不过此选项目前仅仅支持Device Mapper文件系统的磁盘限额。 其他几种存储引擎都还不支持。由于目前cgroups没有对磁盘资源进行限制,Linux磁盘限额使用的quota技术主要是基于用户和文件系统的, 基于进程或者目录磁盘限额还是比较麻烦。下面提供几种可能解决方案去实现容器磁盘限额。 - 为每一个容器创建一个用户,所有用户共用宿主机上的一块磁盘。通过限制用户在这块磁盘上的使用量来限定容器的磁盘使用量。不过磁盘限额仅仅对普通用户有用,对超级用户没有限制。 - 选择支持可以对某一个目录进行限额的文件系统支持,比如XFS可以支持用户、用户组、目录、项目等形式对磁盘使用量进行限制。 - 让Docker定期检查每一个容器的磁盘使用量,这是最差的一种方法,对Docker本身的性能也会造成影响。 - 创建虚拟文件系统,此文件系统仅供某一个容器使用。 这里介绍宿主机使用xfs文件系统,且Docker存储驱动为overlay2的一个解决方法 XFS文件系统限制Docker容器占用磁盘空间大小 ++++++++++++++++++++++++++++++++++++++++++ overlay2.size是在 17.07.0-ce 中引入的: Add overlay2.size daemon storage-opt [#]_ 首先,将默认的/var/lib/docker设置为一块单独的设备,可以用fdisk磁盘划分,也可以直接挂载一块已有的空白块设备,并且保证这块设备的文件系统为XFS格式,具体过程不在赘述 假设块设备为/dev/sdb .. code:: bash $ lsblk|grep sdb sdb 8:16 0 100G 0 disk $ blkid /dev/sdb # 查看uuid /dev/sdb: UUID="cc5933d9-4d94-479f-beed-ecff6c483893" TYPE="xfs" $ vim /etc/fstab # 加入下面这行 UUID=cc5933d9-4d94-479f-beed-ecff6c483893 /var/lib/docker xfs rw,pquota 0 0 # uuid与上面保持一致 $ mount -a # 挂载块设备到指定目录 $ cat /proc/mounts |grep sdb # 确认挂载 /dev/sdb /var/lib/docker xfs rw,relatime,attr2,inode64,prjquota 0 0 然后修改docker的daemon.json文件并重启docker .. code-block:: bash :linenos: :emphasize-lines: 14-18 $ cat /etc/docker/daemon.json { "insecure-registries": [ ...... ], "registry-mirrors": [ "https://xxxxxx.mirror.aliyuncs.com" ], "log-opts": { "max-size": "100m", "max-file": "1" }, "ip-forward": true, "storage-driver": "overlay2", "storage-opts": [ "overlay2.override_kernel_check=true", "overlay2.size=1G" ] } 启动并进入测试容器,查看限制 .. code-block:: bash :linenos: :emphasize-lines: 4 $ docker run --rm -it ubuntu /bin/bash root@4385da9fb7bc:/# df -h Filesystem Size Used Avail Use% Mounted on overlay 1.0G 8.0K 1.0G 1% / tmpfs 64M 0 64M 0% /dev tmpfs 32G 0 32G 0% /sys/fs/cgroup shm 64M 0 64M 0% /dev/shm /dev/sdb 100G 5.7G 95G 6% /etc/hosts tmpfs 32G 0 32G 0% /proc/acpi tmpfs 32G 0 32G 0% /proc/scsi tmpfs 32G 0 32G 0% /sys/firmware 宿主机内容器流量限制 --------------------- Docker已经为容器的资源限制做了许多工作,但是在网络带宽方面却没有进行限制,这就可能导致一些安全隐患,尤其是使用Docke r构建容器云时,可能存在多租户共同使用宿主机资源的情况,这种问题就显得尤为突出,极有可能出现诸如容器内Dos攻击等危害。 无限制的大流量访问会破坏容器的实时交互能力,所以需要对容器流量进行限制。 trafic controller概述 +++++++++++++++++++++++ traffic controller是Linux的流量控制模块,其原理是为数据包建立队列,并且定义了队列中数据包的发送规则, 从而实现在技术上对流量进行限制、调度等控制操作。 traffic controller中的流量控制队列分为两种:无类队列和分类队列。 **无类队列** 无类队列就是对进入网卡的数据进行统一对待,无类队列能够接受数据包并对网卡流量整形,但是不能对数据包进行细致划分, 无类队列规定主要有PFIFO_FAST、TBF和SFQ等,无类队列的流量整形手段主要是排序、限速以及丢包。 **分类队列** 分类队列则是对进入网卡的数据包根据不同的需求以分类的方式区分对待。数据包进入分类队列后,通过过滤器对数据包进行分类, 过滤器返回一个决定,这个决定指向某一个分类,队列就根据这个返回的决定把数据包发送到相应的某一类队列中排队。 每个子类可以再次使用自己的过滤器对数据进一步的分类,直到不需要分类为止,数据包最终会进入相关类的队列中排队。 traffic controller流量控制方式分为4种。 **SHAPING** 流量被限制时,它的传输速率就被控制在某个值以下,限制阈值可以远小于有效带宽,这样可以平滑网络的突发流量,使网络更稳定, SHAPING方式适用于限制外出的流量。 **SCHEDULING** 通过调度数据包传输的优先级数据,可以在带宽范围内对不同的传输流按照优先级分配,适用于限制外出的流量。 **POLICING** SHAPING用于处理向外流量,而POLICING用于处理接收到数据,对数据流量进行限制。 **DROPPING** 如果流量超过设置的阈值就丢弃数据包,向内向外皆有效。 在Docker中使用traffic controller ++++++++++++++++++++++++++++++++++++ 前面提到过Docker会通过veth pair技术创建一对虚拟网卡对,一张放在宿主机网络环境中,一张放在容器的namespace里。 如果我们需要对容器的流量进行限制,那只需要在宿主机的veth **网卡上进行流量限制,将traffic controller中的dev指定为veth** 。 在创建容器时添加此规则,如果你不需要容器之间在三层和四层间通信,指定icc参数可以禁止容器间直接通信。 如果需要容器之间进行直接通信或者需要对不同容器的流量进行限制,就需要预防同一台宿主机上容器之间进行Dos攻击, 此时可以采用traffic controller容器对容器网卡流量进行限制,这在一定程度上可以减轻Dos攻击危害。 GRSecurity内核安全增强工具 -------------------------- 同一台宿主机上的容器是共享内核、内存、磁盘以及带宽等,所有容器都在共享宿主机的物理资源, 所以Linux内核提供了namespace来进行资源隔离,通过cgroups来限制容器的资源使用。但是在内存安全问题上仍有很多问题, 比如C/C++等非内存安全的语言,并不会去检查数组的边界,程序可能会超越边界,而破坏相邻的内存区域。 因此需要一些内存破坏的防御工作,去补充namespace和cgroups。GRSecurity是一个对内核的安全扩展, 通过智能访问控制来阻止内存破坏,预防0day漏洞等。GRSecurity对用户提供了丰富的安全限制,可以提供内存破坏防御、 文件系统增强等各式各样的防御。 关于fork炸弹 --------------- 众所周知,fork炸弹(fork bomb)是一种利用系统调用fork(或其他等效的方式)进行的服务阻断攻击的手段。简单来说, 所谓fork炸弹就是以极快的速度创建大量进程(进程数呈以2为底数的指数增长趋势),并以此消耗系统分配予进程的可用空间使进程表饱和, 从而使系统无法运行新程序。 另一方面,由于fork炸弹程序所创建的所有实例都会不断探测空缺的进程槽并尝试取用以创建新进程, 因而即使在某进程终止后也基本不可能运行新进程。而且,fork炸弹生成的子程序在消耗进程表空间的同时也会占用CPU和内存, 从而导致系统与现有进程运行速度放缓,响应时间也会随之大幅增加,以致于无法正常完成任务,从而使系统的正常运作受到严重影响。 这个攻击手段在容器云中尤为凸显:毕竟容器中运行着的应用是用户自己上传的,它完全可以就是一个fork炸弹; 而不同用户的容器往往会共用一个宿主机,再加上容器本身在内核层面隔离性的不足,使得一旦fork炸弹被触发, 能够带来的影响往往是灾难性的。 所以fork炸弹的应对从一开始就是很受社区关注的(比如issue 6479)。不过到目前为止,现有的方案都不算完美解决。 通过ulimit限制最大进程数目 +++++++++++++++++++++++++++ 说起进程数限制,大家可能知道ulimit的nproc这个配置:当调用fork创建一个进程时,如果该UID用户的进程数之和大于等于进程的 RLIMIT_NPROC值时,fork调用将会失败返回。 由于ulimit在1.6.0以上的Docker中已经被支持,所以用户可以直接使用docker run --ulimit来为每个容器单独配置ulimit参数了 (比如nproc=2)。可遗憾的是,正如上面所说,nproc是一个以用户为管理单位的设置选项,即它调节的是属于一个用户UID的最大进程数之和。 这是nproc与其他ulimit选项的一个显著的不同点。 这就意味着这个限制是对于该容器首进程所属的用户(以UID区分)下所有的容器进程有效的,而并不是像我们预想中那样能够分别 对每个容器里能够创建的进程数做限制,比如: .. code:: bash # 我们使用daemon用户启动3个容器,并设置允许的最大进程数为3 $ docker run --name con1 -d -u daemon --ulimit nproc=3 busybox top 588aae0637753a5091c2f55e7536971f9452764b4904b8e74805e18852de0af7 $ docker run --name con2 -d -u daemon --ulimit nproc=3 busybox top 18c9d5bc7557e0d1da7d085be35df9ae6df6cebd8ba44e329d4aad57d6b1122a $ docker run --name con3 -d -u daemon --ulimit nproc=3 busybox top 9a39ecf3b5df84a7516e7dcc9d5dd81cc703b7dcabc42503c4b678f6771de4a5 $ docker run --name con4 -d -u daemon --ulimit nproc=3 busybox top 028973c2508f9a8ff462e9f749304cc594ea9d400afc992231b5dfcd171e9446 $ docker logs con4 standard_init_linux.go:219: exec user process caused: resource temporarily unavailable 上述例子中,我们指定使用daemon用户来在容器中启动top进程,结果启到第3个容器的时候就报错了。而实际上,我们本来想限制的是: 在每个容器里,用户最多只能创建3个进程。另外,默认情况下,Docker在容器中启动的进程是root用户下的, 而ulimit的nproc参数无法对超级用户进行限制。所以,准确地说,目前在Docker中无法使用ulimit来限制fork炸弹问题。 限制内核内存使用 +++++++++++++++++++ 前面提到过,fork炸弹的一大危害是它会消耗掉一系列的内核资源,比如进程表、内核内存等。其中,由于内核内存资源永远保存 在内存中而不会交换到swap区,所以fork炸弹可以轻而易举地形成对系统的Dos攻击 不过,这同时也就意味着我们可通过限制进程的内核内存资源使用来限制fork炸弹。事实上,很多内核开发者都建议使用kmem (即Cgroup的memory.kmem.limit_in_bytes)来限制fork炸弹,这的确有效,但是同时也带来了如下个问题。 kmem不是仅用来存储进程相关信息的,它还保存了一些诸如文件系统相关、内核加密等内核数据。这就意味这简单粗暴地限制死 kmem的使用很可能会对其他正常的操作产生影响。 cgroup pid子系统 +++++++++++++++++++ 目前,Linux内核有一个还在开发中的特性叫作cgroup pids子系统,这个子系统将可以允许用户配置在一定条件下直接拒绝fork调用、以 及增加了任务计数器子系统等功能,从而完美解决fork炸弹的问题,所以非常值得期待。 总体来看,Docker自身已经提供了很多安全机制,在使用Docker的时候,需要充分利用Docker已有的安全机制, 在多用户环境下则尤其需要注意Docker的安全问题。Docker目前仍然只适于运行可信应用程序,如果需要运行任意代码, 安全很难得到保证,可以通过利用SELinux、GRSecurity等工具来增强容器安全。 .. rubric:: 注释 .. [#] https://github.com/moby/moby/blob/master/profiles/seccomp/default.json .. [#] https://github.com/moby/moby/pull/32977 .. include:: ../../comment.rst