Docker作为一种轻量级的容器化平台,已经成为了现代软件开发和运维的重要组成部分。本指南将主要深入探讨Docker的安全性问题及其底层实现机制,帮助读者理解如何安全地使用Docker,并采取相应的措施来保护容器化环境免受潜在威胁。
一、Docker安全
1、内核命名空间
Docker容器和LXC容器很相似,所提供的安全特性也差不多。当用dockerrun启动一个容器时,在后台Docker为容器创建了一个独立的命名空间和控制组集合。命名空间提供了最基础也是最直接的隔离,在容器中运行的进程不会被运行在主机上的进程和其它容器发现和作用。
每个容器都有自己独有的网络栈,说明它们不能访问其他容器的sockets或接口。不过,如果主机系统上做了相应的设置,容器可以像跟主机交互一样的和其他容器交互。当指定公共端口或使用links来连接2个容器时,容器就可以相互通信了(可以根据配置来限制通信的策略)。
从网络架构的角度来看,所有的容器通过本地主机的网桥接口相互通信,就像物理机器通过物理交换机通信一样。实际上,命名空间的想法和设计提出的时间要更早,最初是为了在内核中引入一种机制来实现OpenVZ的特性。而OpenVZ项目早在2005年就发布了,其设计和实现都已经十分成熟。
2、控制组
控制组是Linux容器机制的另外一个关键组件,负责实现资源的审计和限制。提供了很多有用的特性;以及确保各个容器可以公平地分享主机的内存、CPU、磁盘IO等资源;当然,更重要的是,控制组确保了当容器内的资源使用产生压力时不会连累主机系统。
虽然控制组不负责隔离容器之间相互访问、处理数据和进程,它在防止拒绝服务(DDOS)攻击方面是必不可少的。尤其是在多用户的平台(比如公有或私有的PaaS)上,控制组十分重要。例如,当某些应用程序表现异常的时候,可以保证一致地正常运行和性能。
3、服务端防护
运行一个容器或应用程序的核心是通过Docker服务端。Docker服务的运行目前需要root权限,因此其安全性十分关键。
首先确保只有可信的用户才可以访问Docker服务。Docker允许用户在主机和容器间共享文件夹,同时不需要限制容器的访问权限,这就容易让容器突破资源限制。例如,恶意用户启动容器的时候将主机的根目录/映射到容器的/host目录中,那么容器理论上就可以对主机的文件系统进行任意修改了。
但是事实上几乎所有虚拟化系统都允许类似的资源共享,而没法禁止用户共享主机根文件系统到虚拟机系统。将会造成很严重的安全后果。因此,当提供容器创建服务时(例如通过一个web服务器),要更加注意进行参数的安全检查,防止恶意的用户用特定参数来创建一些破坏性的容器。
为了加强对服务端的保护,Docker的RESTAPI(客户端用来跟服务端通信)在0.5.2之后使用本地的Unix套接字机制替代了原先绑定在127.0.0.1上的TCP套接字,因为后者容易遭受跨站脚本攻击。现在用户使用Unix权限检查来加强套接字的访问安全。
用户仍可以利用HTTP提供RESTAPI访问。建议使用安全机制,确保只有可信的网络,或证书保护机制(例如受保护的stunnel和ssl认证)下的访问可以进行。此外,还可以使用HTTPS和证书来加强保护。
最近改进的Linux命名空间机制将可以实现使用非root用户来运行全功能的容器。这将从根本上解决了容器和主机之间共享文件系统而引起的安全问题。
终极目标是改进2个重要的安全特性:
- 将容器的root用户映射到本地主机上的非root用户,减轻容器和主机之间因权限提升而引起的安全问题;
- 允许Docker服务端在非root权限(rootless模式)下运行,利用安全可靠的子进程来代理执行需要特权权限的操作。这些子进程将只允许在限定范围内进行操作,例如仅仅负责虚拟网络设定或文件系统管理、配置操作等。
最后,建议采用专用的服务器来运行Docker和相关的管理服务(例如管理服务比如ssh监控和进程监控、管理工具nrpe、collectd等)。其它的业务服务都放到容器中去运行。
4、内核能力机制
能力机制(Capability)是Linux内核一个强大的特性,可以提供细粒度的权限访问控制。Linux内核自2.2版本起就支持能力机制,它将权限划分为更加细粒度的操作能力,既可以作用在进程上,也可以作用在文件上。
例如,一个Web服务进程只需要绑定一个低于1024的端口的权限,并不需要root权限。那么它只需要被授权net_bind_service能力即可。此外,还有很多其他的类似能力来避免进程获取root权限。
默认情况下,Docker启动的容器被严格限制只允许使用内核的一部分能力。
使用能力机制对加强Docker容器的安全有很多好处。通常,在服务器上会运行一堆需要特权权限的进程,包括有ssh、cron、syslogd、硬件管理工具模块(例如负载模块)、网络配置工具等等。容器跟这些进程是不同的,因为几乎所有的特权进程都由容器以外的支持系统来进行管理。
ssh访问被主机上ssh服务来管理;cron通常应该作为用户进程执行,权限交给使用它服务的应用来处理;日志系统可由Docker或第三方服务管理;硬件管理无关紧要,容器中也就无需执行udevd以及类似服务;网络管理也都在主机上设置,除非特殊需求,容器不需要对网络进行配置。
从上面的例子可以看出,大部分情况下,容器并不需要“真正的”root权限,容器只需要少数的能力即可。为了加强安全,容器可以禁用一些没必要的权限。完全禁止任何mount操作;禁止直接访问本地主机的套接字;禁止访问一些文件系统的操作,比如创建新的设备、修改文件属性等;禁止模块加载。
这样,就算攻击者在容器中取得了root权限,也不能获得本地主机的较高权限,能进行的破坏也有限。默认情况下,Docker采用白名单机制,禁用必需功能之外的其它权限。当然,用户也可以根据自身需求来为Docker容器启用额外的权限。
5、其他安全特性
除了能力机制之外,还可以利用一些现有的安全机制来增强使用Docker的安全性,例如TOMOYO,AppArmor,Seccomp,SELinux,GRSEC等。
Docker当前默认只启用了能力机制。用户可以采用多种方案来加强Docker主机的安全,例如:
- 在内核中启用GRSEC和PAX,这将增加很多编译和运行时的安全检查;
- 通过地址随机化避免恶意探测等。并且,启用该特性不需要Docker进行任何配置。可以使用一些有增强安全特性的容器模板,比如带AppArmor的模板和Redhat带SELinux策略的模板。这些模板提供了额外的安全特性;
- 用户可以自定义访问控制机制来定制安全策略。
跟其它添加到Docker容器的第三方工具一样(比如网络拓扑和文件系统共享),有很多类似的机制,在不改变Docker内核情况下就可以加固现有的容器。
二、Docker底层实现
1、基本架构
Docker采用了C/S架构,包括客户端和服务端。Docker守护进程(Daemon)作为服务端接受来自客户端的请求,并处理这些请求(创建、运行、分发容器)。
客户端和服务端既可以运行在一个机器上,也可通过socket或者RESTfulAPI来进行通信。
Docker守护进程一般在宿主主机后台运行,等待接收来自客户端的消息。Docker客户端则为用户提供一系列可执行命令,用户用这些命令实现跟Docker守护进程交互。
2、命名空间
命名空间是Linux内核一个强大的特性。每个容器都有自己单独的命名空间,运行在其中的应用都像是在独立的操作系统中运行一样。命名空间保证了容器之间彼此互不影响。
(1)pid命名空间
不同用户的进程就是通过pid命名空间隔离开的,且不同命名空间中可以有相同pid。所有的LXC进程在Docker中的父进程为Docker进程,每个LXC进程具有不同的命名空间。同时由于允许嵌套,因此可以很方便的实现嵌套的Docker容器。
(2)net命名空间
有了pid命名空间,每个命名空间中的pid能够相互隔离,但是网络端口还是共享host的端口。网络隔离是通过net命名空间实现的,每个net命名空间有独立的网络设备,IP地址,路由表,/proc/net目录。这样每个容器的网络就能隔离开来。Docker默认采用veth的方式,将容器中的虚拟网卡同host上的一个Docker网桥docker0连接在一起。
(3)ipc命名空间
容器中进程交互还是采用了Linux常见的进程间交互方法(interprocesscommunication-IPC),包括信号量、消息队列和共享内存等。然而同VM不同的是,容器的进程间交互实际上还是host上具有相同pid命名空间中的进程间交互,因此需要在IPC资源申请时加入命名空间信息,每个IPC资源有一个唯一的32位id。
(4)mnt命名空间
类似chroot,将一个进程放到一个特定的目录执行。mnt命名空间允许不同命名空间的进程看到的文件结构不同,这样每个命名空间中的进程所看到的文件目录就被隔离开了。同chroot不同,每个命名空间中的容器在/proc/mounts的信息只包含所在命名空间的mountpoint。
(5)uts命名空间
UTS(“UNIXTime-sharingSystem”)命名空间允许每个容器拥有独立的hostname和domainname,使其在网络上可以被视作一个独立的节点而非主机上的一个进程。
(6)user命名空间
每个容器可以有不同的用户和组id,也就是说可以在容器内用容器内部的用户执行程序而非主机上的用户。
3、控制组
控制组(cgroups)是Linux内核的一个特性,主要用来对共享资源进行隔离、限制、审计等。只有能控制分配到容器的资源,才能避免当多个容器同时运行时的对系统资源的竞争。
控制组技术最早是由Google的程序员在2006年提出,Linux内核自2.6.24开始支持。
控制组可以提供对容器的内存、CPU、磁盘IO等资源的限制和审计管理。
4、联合文件系统
联合文件系统(UnionFS)是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下(uniteseveraldirectoriesintoasinglevirtualfilesystem)。
联合文件系统是Docker镜像的基础。镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。另外,不同Docker容器就可以共享一些基础的文件系统层,同时再加上自己独有的改动层,大大提高了存储的效率。
Docker中使用的AUFS(AdvancedMulti-LayeredUnificationFilesystem)就是一种联合文件系统。AUFS支持为每一个成员目录(类似Git的分支)设定只读(readonly)、读写(readwrite)和写出(whiteout-able)权限,同时AUFS里有一个类似分层的概念,对只读权限的分支可以逻辑上进行增量地修改(不影响只读部分的)。
Docker目前支持的联合文件系统包括OverlayFS,AUFS,Btrfs,VFS,ZFS和DeviceMapper。
各Linux发行版Docker推荐使用的存储驱动。
在可能的情况下,推荐使用overlay2存储驱动,overlay2是目前Docker默认的存储驱动,以前则是aufs。你可以通过配置来使用以上提到的其他类型的存储驱动。
5、容器格式
最初,Docker采用了LXC中的容器格式。从0.7版本以后开始去除LXC,转而使用自行开发的libcontainer,从1.11开始,则进一步演进为使用runC和containerd。
6、网络
Docker的网络实现其实就是利用了Linux上的网络命名空间和虚拟网络设备(特别是vethpair)。
(1)基本原理
首先,要实现网络通信,机器需要至少一个网络接口(物理接口或虚拟接口)来收发数据包;此外,如果不同子网之间要进行通信,需要路由机制。
Docker中的网络接口默认都是虚拟的接口。虚拟接口的优势之一是转发效率较高。Linux通过在内核中进行数据复制来实现虚拟接口之间的数据转发,发送接口的发送缓存中的数据包被直接复制到接收接口的接收缓存中。对于本地系统和容器内系统看来就像是一个正常的以太网卡,只是它不需要真正同外部网络设备通信,速度要快很多。
Docker容器网络就利用了这项技术。它在本地主机和容器内分别创建一个虚拟接口,并让它们彼此连通(这样的一对接口叫做vethpair)。
(2)创建网络参数
Docker创建一个容器的时候,会执行如下操作:
- 创建一对虚拟接口,分别放到本地主机和新容器中;
- 本地主机一端桥接到默认的docker0或指定网桥上,并具有一个唯一的名字,如veth65f9;
- 容器一端放到新容器中,并修改名字作为eth0,这个接口只在容器的命名空间可见;
- 从网桥可用地址段中获取一个空闲地址分配给容器的eth0,并配置默认路由到桥接网卡veth65f9。
- 完成这些之后,容器就可以使用eth0虚拟网卡来连接其他容器和其他网络。
可以在dockerrun的时候通过–net参数来指定容器的网络配置,有4个可选值:
- –net=bridge这个是默认值,连接到默认的网桥;
- –net=host告诉Docker不要将容器网络放到隔离的命名空间中,即不要容器化容器内的网络。此时容器使用本地主机的网络,它拥有完全的本地主机接口访问权限。容器进程可以跟主机其它root进程一样可以打开低范围的端口,可以访问本地网络服务比如D-bus,还可以让容器做一些影响整个主机系统的事情,比如重启主机。因此使用这个选项的时候要非常小心。如果进一步的使用–privileged=true,容器会被允许直接配置主机的网络堆栈;
- –net=container:NAME_or_ID让Docker将新建容器的进程放到一个已存在容器的网络栈中,新容器进程有自己的文件系统、进程列表和资源限制,但会和已存在的容器共享IP地址和端口等网络资源,两者进程可以直接通过lo环回接口通信;
- –net=none让Docker将新容器放到隔离的网络栈中,但是不进行网络配置。之后,用户可以自己进行配置。
(3)网络配置细节
用户使用–net=none后,可以自行配置网络,让容器达到跟平常一样具有访问网络的权限。通过这个过程,可以了解Docker配置网络的细节。
首先,启动一个/bin/bash容器,指定–net=none参数。
$dockerrun-i-t--rm--net=nonebase/bin/bash root@63f36fc01b5f:/#
在本地主机查找容器的进程id,并为它创建网络命名空间。
$dockerinspect-f'{{.State.Pid}}'63f36fc01b5f 2778 $pid=2778 $sudomkdir-p/var/run/netns $sudoln-s/proc/$pid/ns/net/var/run/netns/$pid
检查桥接网卡的IP和子网掩码信息。
$ipaddrshowdocker0 21:docker0:... inet172.17.42.1/16scopeglobaldocker0 ...
创建一对“vethpair”接口A和B,绑定A到网桥docker0,并启用它。
$sudoiplinkaddAtypevethpeernameB $sudobrctladdifdocker0A $sudoiplinksetAup
将B放到容器的网络命名空间,命名为eth0,启动它并配置一个可用IP(桥接网段)和默认网关。
$sudoiplinksetBnetns$pid $sudoipnetnsexec$pidiplinksetdevBnameeth0 $sudoipnetnsexec$pidiplinkseteth0up $sudoipnetnsexec$pidipaddradd172.17.42.99/16deveth0 $sudoipnetnsexec$pidiprouteadddefaultvia172.17.42.1
以上,就是Docker配置网络的具体过程。
当容器结束后,Docker会清空容器,容器内的eth0会随网络命名空间一起被清除,A接口也被自动从docker0卸载。
此外,用户可以使用ipnetnsexec命令来在指定网络命名空间中进行配置,从而配置容器内的网络。
-
广告合作
-
QQ群号:707632017