Docker的安全机制
Docker目前已经在安全方面做了一定的工作,包括Docker daemon在以TCP形式提供服务的同时使用传输层安全协议;在构建和使用镜像时会验证镜像的签名证书;通过cgroups及namespaces来对容器进行资源限制和隔离;提供自定义容器能力(capability)的接口;通过定义seccomp profile限制容器内进程系统调用的范围等。如果合理地实现上述安全方案,可以在很大程度上提高Docker容器的安全性。
1、Docker daemon 安全
Docker向外界提供服务主要有4种通信形式,默认是以Unix域套接字的方式来与客户端进行通信。这种方式相对于TCP形式比较安全,只有进入daemon宿主机所在机器并且有权访问daemon的域套接字才可以和daemon建立通信。
如果以TCP形式向外界提供服务,可以访问到daemon所在主机的用户都可能成为潜在的攻击者。同时,由于数据传输需要通过网络进行,数据可能被截获甚至修改。为了提高基于TCP的通信方式的安全性,Docker为我们提供了TLS ( Transport Layer Security)传输层安全协议。在Docker中可以设置--tlsverify来进行安全传输检验,通过--tlscacert(信任的证书)、--tlskey(服务器或者客户端秘钥)、--tlscert(证书位置)3个参数来配置。安全认证主要是在服务器端设置,客户端可以对服务端进行验证。客户端在访问daemon时只需要提供签署的证书,那么就可以使用Docker daemon服务。
2、镜像安全
Docker目前提供registry访问权限控制以保证镜像安全。另外,Docker从1.3版本开始就有了镜像数字签名功能,用以防止官方镜像被篡改或损坏,以此来保证官方镜像的完整性,但是镜像校验功能仅当访问官方V2 registry时才会生效,需要用户进行docker login登录。
2.1、Docker registry
目前Docker使用一个中心验证服务器来完成Docker registry的访问权限控制,每一个Docker客户端对registry进行pull/push操作的时候都会经过如下6个步骤。
(1)客户端尝试对registry发起push/pull操作。
(2)如果registry的访问需要认证,registry就会返回一个含有如何完成认证challenge的401Unauthorized HTTP响应。
(3)客户端向认证服务器请求一个Bearer token.
(4)认证服务器返回给客户端一个加密的Bearer token,用来代表客户端被授权的访问权限。
(5)客户端再次尝试用头部嵌有Bearer token的请求向原来的registry发起请求。
(6) registry验证客户端请求中的Bearer token及其包含的授权空间权限。如果正确,便建立与客户端的push/pull会话。
2.2、验证校验和
镜像校验和用来保证镜像的完整性,以预防可能出现的镜像破坏。Docker registry下的每一个镜像都对应拥有自己的manifest文件以及该文件本身的签名。其中的信息包括镜像所在的命名空间、镜像在此仓库下对应的标签、镜像校验方法及校验和、镜像形成时的运行信息以及manifest文件本身的签名。
下面以镜像拉取过程docker pull为例来分析镜像校验和如何起作用:
- 获取镜像tag或者diges晰对应的manifest文件,根据manifest的类型分别处理,下面以当前版本中默认的schema2为例。
- 根据manifest内容计算digest,如果是通过digest进行镜像的拉取操作,便验证计算结果与命令行传入的digest是否一致。
- 从manifest中提取镜像ID判断该镜像是否已经存在,若存在则结束。
- 根据镜像ID(即镜像配置文件的digest拉取镜像的配置文件,计算该配置文件内容的digest并验证与镜像ID是否一致。
- 根据manifest中引用的镜像层描述结构从底至顶顺序交由下载管理器下载镜像层。将下载完成的镜像层压缩包解压后注册至layerStore中,返回一个layer结构用于描述该镜像层。
- 注册过程中与镜像安全有关的部分便是根据镜像文件tar包计算校验和(即该镜像层的difflD ),最后存入layer结构中返回。
- 镜像校验和计算策略:镜像本质上是一个tar格式文件包。tar包是一个单独的文件,tar包的每一个文件在tar包中都是以一个File Entry形式存在,每一个File Entry又包含Header和Body。每个File Entry的Header以及Body都会加入镜像的校验和计算。首先单独计算每一个File Entity的Header和Body校验值,然后把所有文件校验和进行哈希计算,最后计算结果作为镜像的校验和。
- 最后遍历所有layer,根据每一个镜像层的difflD按顺序组合成rootfs的DiffIDs,并与镜像配置文件中的diffIDs域中的内容对比,如果一致则根据镜像配置文件在ImageStore中创建该镜像;否则返回一个rootfs不匹配的错误。
在Docker pull镜像的过程中进行了多次根据内容哈希的验证。如果在命令行中用digest拉取镜像,则会验证拉取manifest的digest(一种根据manifest内容计算的校验和)与传入的digest是否一致;在根据manifest中镜像ID拉取镜像配置文件后,会根据配置文件内容生成digest并验证与镜像ID是否一致;在下载manifest中引用的镜像层后,会根据镜像文件计算出校验和diffID,并与镜像配置文件中记录的difflD验证对比。每一步不可靠的网络传输后都会计算校验和与前一步的可靠结果进行验证,这些校验过程保证了镜像内容的可靠性。
3、内核安全
3.1、cgroups资源限制
容器本质上是进程,cgroups的存在就是为了限制宿主机上不同容器的资源使用量,避免单个容器耗尽宿主机资源而导致其他容器异常。
3.2、namespace资源隔离
为了使容器处在独立的环境中,Docker用namespace技术来隔离容器,使容器与容器之间、容器与宿主机之间相互隔离。
4、容器之间的网络安全
Docker daemon指定--icc标志的时候,可以禁止容器与容器之间通信,主要通过设定iptables规则实现。
5、Docker容器能力限制
什么是能力呢?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:允许创建RAW和PACKET套接字。
- MKNOD:允许使用mknod堆键指定文件。
- SYS_REBOOT:允许使用reboot或者kexec_load。kexec_load功能是加载新的内核作为reboot重新启动所需内核。
- SYS_CHROOT:允许使用chroot。
- KILL:允许发送信号。
- NET_BIND_SERVICE:允许绑定常用端口号(端口号小于1024 )。
- AUDIT_WRITE:允许审计日志写入。
5.1、削弱能力
可以通过在docker run时使用--cap-drop参数来削弱该容器的相应能力。
5.2、增加能力
可以通过在docker run时使用--cap-add参数来增加该容器的相应能力。
6、seccomp
seccomp(secure computing mode)是Linux的一种内核特性,可用于限制进程能够调用的系统调用(system call )的范围,从而减少内核的攻击面,被广泛用于构建沙盒。
6.1、使用seccomp
使用seccomp的前提是Docker构建时已包含seccomp,并且内核中的CONFIG_SECCOMP已开启。可使用如下方法检查内核是否支持seccomp:
代码语言:txt复制 $cat /boot/config-`uname -r` | grep CONFIG_SECCOMP=
CONFIG_SECCOMP=y
6.2、seccomp profile
在Docker中,我们通过为每个容器编写json格式的seccomp profile来实现对容器中进程系统调用的限制。Docker也提供了默认seccomp profile供所有容器使用,默认seccomp profile片段如下:
代码语言:txt复制 {
"defaultAction":"SCMP_ACT_ERRNO",
"architectures":[
"SCMP_ARCH_X86_64",
"SCMP_ARCH_X86",
"SCMP_ARCH_X32"
],
"syscalls":[
{
"name":"accept",
"action":"SCMP_ACT_ALLOW"
"args":[]
},
{
"name":"accept4",
"action":"SCMP_ACT_ALLOW"
"args":[]
},
}
其中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安全问题
1、磁盘资源限制问题
容器本质上是一个进程,通过通过镜像层叠的方式来构建容器的文件系统。当需要改写文件时,把改写的文件复制到最顶层的读写层,其本质上还是在宿主机文件系统的某一目录下存储这些信息。所有容器的rootfs最终存储在宿主机上。所以,极有可能出现一个容器把宿主机上所有的磁盘容量耗尽的情况,届时其他容器将无法进行文件存储操作,所以有必要对容器的磁盘使用量进行限制。
2、容器逃逸问题
3、容器DoS攻击与流量限制问题
目前,在公网上的DoS攻击(deny-of-service,拒绝服务攻击)预防已经有很成熟的产品,这对传统网络有比较好的防御效果,但是随着虚拟化技术的兴起,攻击数据包可能不需要通过物理网卡就可以攻击同一个宿主机下的其他容器。所以传统DoS预防措施对容器之间的DoS攻击没有太大效果。
默认的Docker网络是网桥模式,所有容器连接到网桥上。容器通过veth pair技术创建veth pair网卡,然后将其中一端放入容器内部并且命名为eth0,另外一张网卡留在宿主机网络环境中。容器内网卡发出的数据包都会发往宿主机上对应网卡,再由物理网卡进行转发。同理,物理网卡收到的数据根据地址会相应发送到不同的容器内。实际上所有容器在共用一张物理网卡。如果在同一宿主机中的某一个容器抢占了大部分带宽,将会影响其他容器的使用,例如大流量的容器内下载程序会影响其他交互式应用的访问。
4、超级权限问题
Docker安全问题的解决
目前来看,Docker通过一些额外的工具来加强安全。比如,使用SELinux限制进程访问的资源;使用quota等技术限制容器磁盘使用量;使用traffic controller技术对容器的流量进行控制。通过这些工具的配合来加强Docker的安全。
1、SELinux
- SELinux概述
SELinux是由内核实现的MAC ( Mandatory Access Control,强制访问控制),可以说SELinux就是一个MAC系统。SELinux为每一个进程设置一个标签,称为进程的域,为文件设置标签,称为类型。每一标签由User、 Role、Type和Level这4部分组成。
User: SELinux用户是由权限构成的集合,而非Linux用户。系统在登录的时候会为Linux用户匹配一个SELinux用户,通过semanage login -l 可以看到Linux用户和SELinux用户的映射。
Type: Type是SELinux访问控制的基础,描述进程所能访问的资源类型。常见文件资源的类型有blk_file, che_file, dir, fd, fifo_file, fie, filesystem, lnk_file和sock_file等,它们分别用于标识块文件、字符文件、目录、文件描述符文件、fifo文件、文件系统、链接和套接字文件等。容器文件一般表示为svirt_sandbox_file_t或者svirt_lxc_file_t。常见域有很多,不同类型的容器的需求不一样,容器域一般使用svirt_lxc_net_t来标示其域,也可以自己为容器定义域。
Role:角色是一些类型的组合,是用户和类型的过渡。一个用户可以有多个角色。一个角色可以使用不同类型。
Level:定义更加具体的权限,可以有两种选择,一种是MLS(多层级安全),另外一种是MCS(多级分类安全)。
- SELinux的三种模式
SELinux提供了如下3种工作模式:
Enforcing : SELinux策略被强制执行,根据SELinux策略来拒绝或者是通过操作。
Permissive: SELinux策略并不会执行,原本在Enforcing式下应该被拒绝的操作,在该模式下只会触发安全事件日志记录,而不会拒绝此操作的执行。
Disabled: SELinux被关闭,SELinux不会执行任何策略。
- SELinux三种访问控制方式
Type Enforcement:类型强制,是SELinux下的主要访问控制机制。
Role-Based Access Control(RBAC):基于SELinux用户,注意是SELinux用户,不是普通Linux用户,如果想知道自己对应的Linux用户对应什么SELinux,通过semanage login -l命令可以查看用户。
Multi-Level Security (MLS):多级分类安全,也就是我们所指定的level标签。
类型强制访问控制
在SELinux中,所有访问都必须是明确授权的,即默认情况下未授权的访问都会被拒绝。SELinux是对现有的以用户或用户组来进行文件读、写和执行的安全增强,并不是替换掉原有的安全认证体系。简单来说,SELinux是在以用户为中心的经典安全体系之后的第二道屏障。主体和客体的级别都已经明确,那它们是如何关联起来呢?SELinux采用的是策略,在策略中明确指定规则。一条SELinux规则由以下4部分组成:
- 源类型:指的是进程的类型,称为域。
- 目标类型:指的是被进程访问的客体的类型,称为类型。
- 客体类别:指定允许访问的客体类别。
- 权限:指定源类型可以对目标类型所做的操作。
比如:
代码语言:txt复制 allow sshd_console_device_t:chr_file { ioctl write getattr lock append open };
这一条策略规则,其源类型是sshd_t的类型,允许访问console_device_t类型客体。源类型可以在客体执行的权限是ioctl, write, getatr, lock, append, open等操作。通过这条规则定义了主体与客体之间的权限控制联系。
- 为什么在Docker中使用SELinux?
SELinux把所有进程和文件都打上标签。进程之间相互隔离,SELinux策略控制进程如何访问资源,也就是限制容器如何去访问资源。
SELinux策略是全局的,它不是针对具体用户设定,而是强制整个系统去遵循,使攻击者很难突破。
减少提权攻击的风险,如果一个进程被攻陷,攻击者将会获得该进程的所有权限,访问该进程能访问的权限。比如Apache的httpd进程被攻陷,那么它仅能访问httpd所能访问的文件,而无法去访问其他目录的文件(如/home, /etc/passwd等目录就不行),防止了更为严重的危害。
2、磁盘限额
Docker目前提供--storge-opt=[]来进行磁盘限制,不过此选项目前仅仅支持Device Mapper文件系统的磁盘限额。其他几种存储引擎都还不支持。由于目前cgroups没有对磁盘资源进行限制,Linux磁盘限额使用的quota技术主要是基于用户和文件系统的,基于进程或者目录磁盘限额还是比较麻烦。下面提供几种可能解决方案去实现容器磁盘限额:
- 为每一个容器创建一个用户,所有用户共用宿主机上的一块磁盘。通过限制用户在这块磁盘上的使用量来限定容器的磁盘使用量。不过磁盘限额仅仅对普通用户有用,对超级用户没有限制。
- 选择支持可以对某一个目录进行限额的文件系统支持,比如XFS可以支持用户、用户组、目录、项目等形式对磁盘使用量进行限制。
- 让Docker定期检查每一个容器的磁盘使用量,这是最差的一种方法,对Docker本身的性能也会造成影响。
- 创建虚拟文件系统,此文件系统仅供某一个容器使用。
3、宿主机内容器流量限制
Docker已经为容器的资源限制做了许多工作,但是在网络带宽方面却没有进行限制,这就可能导致一些安全隐患,尤其是使用Docker构建容器云时,可能存在多租户共同使用宿主机资源的情况,这种问题就显得尤为突出,极有可能出现诸如容器内Dos攻击等危害。无限制的大流量访问会破坏容器的实时交互能力,所以需要对容器流量进行限制。
- trafic controller概述
traffic controlle是Linux的流量控制模块,其原理是为数据包建立队列,并且定义了队列中数据包的发送规则,从而实现在技术上对流量进行限制、调度等控制操作。
traffc controller中的流量控制队列分为两种:无类队列和分类队列。
- 无类队列就是对进入网卡的数据进行统一对待,无类队列能够接受数据包并对网卡流量整形,但是不能对数据包进行细致划分,无类队列规定主要有PFIFO_FAST, TBF和SFQ等,无类队列的流量整形手段主要是排序、限速以及丢包。
- 分类队列则是对进入网卡的数据包根据不同的需求以分类的方式区分对待。数据包进入分类队列后,通过过滤器对数据包进行分类,过滤器返回一个决定,这个决定指向某一个分类,队列就根据这个返回的决定把数据包发送到相应的某一类队列中排队。每个子类可以再次使用自己的过滤器对数据进一步的分类,直到不需要分类为止,数据包最终会进入相关类的队列中排队。
traffic controller流量控制方式分为4种。
- SHAPING:流量被限制时,它的传输速率就被控制在某个值以下,限制阈值可以远小于有效宽,这样可以平滑网络的突发流量,使网络更稳定,SHAPING方式适用于限制外出配流量。
- SCHEDULING:通过调度数据包传输的优先级数据,可以在带宽范围内对不同的传输流按照优先级分配,适用于限制外出的流量。
- POLICING: SHAPING用于处理向外流量,而POLICING用于处理接收到数据,对数据流量进行限制。
- DROPPING:如果流量超过设置的阈值就丢弃数据包,向内向外皆有效。
- 无类队列的使用
无类队列的使用方法比较简单,TBF ( Token Bucket Filter)是无类队列中比较常用的一种队列,TBF只是对数据包流量进行SHAPING,并不做SCHEDULING。如果你只是简单地限制网卡的流量,这会是一个很高效的方式。使用无类队列进行流量限制比较简单,Linux下对流量进行限制的命令工具是tc,命令如下所示。
代码语言:txt复制 tc disc add dev eth1 root handle 1:0 tbf rate 128kbit burst 1000 latency 50ms
其中各字段含义如下:
代码语言:txt复制 - tc disc add dev eth1表示在设备eth1上添加队列;
- root表示根节点,没有父亲节点;
- handle z:0表示队列句柄;
- tbf表示使用无类队列TBF;
- rate 128kbit:速率是128kbit;
- burst 1000:桶尺寸为1000;
- latency 50ms:数据包最多等待50ms。
- 分类队列的使用
如果需要对数据包进一步细分,对不同类型数据进行区别对待,分类队列就非常适合。CBQ( Class Based Queue)是一种比较常用的分类队列。在分类队列中多了类和过滤器两个概念。通过过滤器把数据包划分到不同的类里面,再递归地处理这些类。下面以一个简单例子来描述分类队列的使用。
假设我们有一个如下的场景:主机上有一张带宽为100Mbit/s的物理网卡,在这台主机上开启了3个服务:ftp服务、snmp服务以及http服务,我们需要对这3种服务的带宽进行限制,那么可以进行如下操作:
首先建立一个根队列:
代码语言:txt复制 tc disc add dev eth0 root handle 1:0 cbq bandwidth 100Mbit avpkt 1000 cell 8
然后在此队列下建立3个类:
代码语言:txt复制 tc class add dev eth0 parent 1:0 classic 1:1 cbq bandwidth 100Mbit rate 5Mbit weight 0.5Mbit prio 5 cell 8 avpkt 1000
tc class add dev eth0 parent 1:0 classic 1:2 cbq bandwidth 100Mbit rate 10Mbit weight 0.5Mbit prio 5 cell 8 avpkt 1000
tc class add dev eth0 parent 1:0 classic 1:3 cbq bandwidth 100Mbit rate 15Mbit weight 0.5Mbit prio 5 cell 8 avpkt 1000
接下来再在3个类下建立队列或者对类进一步划分:
代码语言:txt复制 tc qdisc add dev eth0 parent 1:1 handle 10:0
tc qdisc add dev eth0 parent 1:2 handle 20:0
tc qdisc add dev eth0 parent 1:3 handle 30:0
最后再为根队列建立3个过滤器:
代码语言:txt复制 tc filter add dev eth0 parent 1:0 protocol ip prio 1 u32 math ip sport 20 Oxfffff flowid 1:1
tc filter add dev eth0 parent 1:0 protocol ip prio 1 u32 math ip sport 161 Oxfffff flowid 1:2
tc filter add dev eth0 parent 1:0 protocol ip prio 1 u32 math ip sport 80 Oxfffff flowid 1:3
由此可见,我们为根队列创建了3个分类,分别对ftp, snmp以及http这3种服务的数据包进行限制,其余数据包将不受影响。
- 在Docker中使用traffic controller
前面提到过Docker会通过veth pair术创建一对虚拟网卡对,一张放在宿主机网络环境中,一张放在容器的namespace里。如果我们需要对容器的流量进行限制,那只需要在宿主机的veth网卡上进行流量限制,将traffic controller中的dev指定为veth*。在创建容器时添加此规则,如果你不需要容器之间在三层和四层间通信,指定icc参数可以禁止容器间直接通信。如果需要容器之间进行直接通信或者需要对不同容器的流量进行限制,就需要预防同一台宿主机上容器之间进行Dos攻击,此时可以采用traffic controller容器对容器网卡流量进行限制,这在一定程度上可以减轻Dos攻击危害。
4、GRSecurity内核安全增强工具
同一台宿主机上的容器是共享内核、内存、磁盘以及带宽等,所有容器都在共享宿主机的物理资源,所以Linux内核提供了namespace来进行资源隔离,通过cgroups来限制容器的资源使用。但是在内存安全问题上仍有很多问题,比如C/C 等非内存安全的语言,并不会去检查数组的边界,程序可能会超越边界,而破坏相邻的内存区域。因此需要一些内存破坏的防御工作,去补充namespace和cgroups。GRSecurity是一个对内核的安全扩展,通过智能访问控制来阻止内存破坏,预防0day漏洞等。GRSecurity对用户提供了丰富的安全限制,可以提供内存破坏防御、文件系统增强等各式各样的防御。
5、关于fork炸弹
fork炸弹(fork bomb)是一种利用系统调用fork(或其他等效的方式)进行的服务阻断攻击的手段。简单来说,所谓fork炸弹就是以极快的速度创建大量进程(进程数呈以2为底数的指数增长趋势),并以此消耗系统分配予进程的可用空间使进程表饱和,从而使系统无法运行新程序。
另一方面,由于fork炸弹程序所创建的所有实例都会不断探测空缺的进程槽并尝试取用以创建新进程,因而即使在某进程终止后也基本不可能运行新进程。而且,fork炸弹生成的子程序在消耗进程表空间的同时也会占用CPU和内存,从而导致系统与现有进程运行速度放缓,响应时间也会随之大幅增加,以致于无法正常完成任务,从而使系统的正常运作受到严重影响。
预防方案如下:
- 通过ulimit限制最大进程数目
说起进程数限制,大家可能知道ulimit的nproc这个配置:当调用fork创建一个进程时,如果该UID用户的进程数之和大于等于进程的RLIMIT NPROC值时,fork调用将会失败返回。
遗憾的是,默认情况下,Docker容器中启动的进程是root用户下的,而ulimit的nproc参数无法对超级用户进行限制。所以,准确地说,目前在Docker中无法使用ulimit来限制fork炸弹问题。
- 限制内核内存使用
前面提到过,fork炸弹的一大危害是它会消耗掉一系列的内核资源,比如进程表、内核内存等。其中,由于内核内存资源永远保存在内存中而不会交换到swap区,所以fork炸弹可以轻而易举地形成对系统的Dos攻击。
不过,这同时也就意味着我们可通过限制进程的内核内存资源使用来限制fork炸弹。事实上,很多内核开发者都建议使用kmem(即Cgroup的memory.kmem.limitin bytes)来限制fork炸弹。
- cgroup pid子系统
Linux内核有一个特性叫作cgroup pids子系统,这个子系统将可以允许用户配置在一定条件下直接拒绝fork调用、以及增加了任务计数器子系统等功能,从而完美解决fork炸弹的问题。