容器实现的是进程上下文环境的隔离,而不是对整个系统进行虚拟化。通过管控风险可以防止由于软件缺陷而导致的错误行为,也可以防止由于消耗资源过量而导致计算机无响应的攻击类行为。容器可以确保软件仅使用分配的计算资源并且访问受限的数据。
计算机系统的物理资源也是有限的,所以为了解决以上问题,需要隔离进程和有限供应资源。而构建隔离高度隔离的系统的部分工作就包括为各个容器提供资源配额。
内存限制是可以对容器的最基本的限制,内存限制规定了容器内的进程能够使用的最大内存。内存限制用于确保容器不能分配完所有的系统内存。可以在docker create/run命令时使用-m或者--momory标识选项来设置限制。
具体格式如下:
docker run -d --name test --memory 256m --env MYSQL_ROOT_PASSWORD=test mysql
其中256后面的m还有其他选项,可以选择b(字节)、k(千字节)、m(兆字节)、g(吉字节)。
内存限制不是预定配置或保留配置。内存限制只是防止过度使用内存的保护措施。在设置内存配额之前时,应该考虑运行的软件是否可以在设置的内存配额范围内运行且系统是否支持内存配额功能。
在考虑设置的内存配额时倾向于先应该考虑高估实际需求,之后根据试验结果或者错误信息进行调整。另一种选择时在具有实际数据量的容器中运行软件,并使用docker stat 命令查看实际使用了多少内存。
在考虑系统是否支持内存配额功能时。设置的内存配额可以大于系统的可用内存。在具有交换空间(扩展到磁盘的虚拟内存)的主机上,可以为容器设置大于可用物理内存的内存配额。如上情况下,系统的内存限制就等同于容器的内存配额,在容器运行时就相当于并没有设置内存限制。
cpu在进程运行时和内存同等重要,但在运行时缺少cpu资源的结果时会导致进程性能下降。Docker提供两种方式对cpu进行限额。
首先,可以指定一个容器相比于其他容器的权重,linux 根据权重信息来确定某个容器可以占用的cpu资源的百分比(该百分比是指容器能够使用的所有cpu总和的百分比)。设置方法如下所示:
docker run --name cputest -d -P \
--memory 512m \
--cpu-shares 512 \ #设置该容器可以设置的cpu份额并确定其相对权重
wordpress
此方法的限制的例子如下:
根据设置的容器的总额判断该容器占用的部分。
cpu份额与内存限制的不同之处在于,cpu份额仅在程序争用cpu资源是才会被强制执行。如果其他的进程和容器处于闲置状态,可以占用超出设置份额限制的cpu。这种方法可以确保不浪费cpu资源,也能保证需要cpu资源的进程在资源紧张时,可以获取相应的cpu份额。设置cpu份额的目的主要是为了防止一个或者一组进程完全的消耗cpu资源,而不降低这些进程的性能。
其次,可以利用cpus选项提供一种限制容器可使用的cpu总量的方法,通过配置linux完全公平调度程序,示例如下:
docker run -d --name wordpress \
--momery 512m
--cpus 0.75
wprdpress
cpu在运行时,会涉及进程得上下文切换。进程的上下文切换是指从执行一个进程变为执行另一个进程。在某些情况下,确保关键进程永远不会在同一组cpu内核上执行是非常必要的,所以,可以使用--cpuset-cpus标识选项来限制容器仅在一组特定的cpu内核上运行。
docker run -d --name cpu \
--cpuset-cpus 0 \ #限制容器运行的cpu集合0上
wordpress
如上示例:
0还可以切换为 「0、1、2:包含cpu前三个核心的列表」
「0~2:包含cpu前三个核心的范围」
设备是控制资源的最后一种资源类型,但更类似于授权给并不是限制。linux系统中有很多物理设备,比如usb驱动器、鼠标、摄像头、硬盘驱动器等。默认情况下,容器可以访问某些物理设备,对于其他不能默认访问的主机设备,需要docker为容器创建专用设备才能访问。
例:假如容器中正在运行某个需要访问摄像头设备的程序,运行时将如下所示:
docker run -d --name photo \
--device /dev/video0:/dev/video0 \ #该设备需要位于/dev/video0中
ubuntu
--device的意思是提供宿主机设备文件与容器内的映射关系。该标识选项可以被设置很多次。
Linux提供了一些在同一计算机上运行的进程之间共享内存的工具。这种形式的进程间通信将以内存级速度进行。当网络性能或其他性能影响进程间通信时,通常使用共享内存的通信方式可以解决以上问题。
docker创建容器时默认为每个容器创建唯一的IPC(进程间通信)命名空间。Linux IPC命名空间则把共享内存单元划分为命名的共享内存块、信号量以及消息队列。IPC命名空间可以防止容器中的进程访问主机或其他容器的内存。
如果应用程序需要与不同容器中的程序通过共享内存进行通信,可以使用--ipc标识进行将他们的IPC命名空间整合在一起。该标识选线采用容器的模式进行工作在与另一个目标容器相同的IPC命名空间中创建新容器。
命令示例如下:
docker run -d --name abc --ipc share centos
docker run -d --nmame def --ipc container:abc centos
tips:
可以使用--ipc=host选项可以让容器与主机之间共享内存,但在目前的docker发行版本中共享主机内存非常困难,因为目前与docker默认容器的安全状态相矛盾。
Docker默认是以镜像元数据的用户身份启动容器。通常为root用户。这样会造成一些问题,比如:root用户对容器几乎拥有全部的访问权限,而以root用户身份运行的所有进程都拥有这些权限,但是如果某一个进程存在错误,可能会造成整个容器的损坏。但有时确实需要运用root用户启动容器,例如:需要在容器中运行系统管理软件。
在创建容器之前,查看默认情况下使用哪个用户创建容器。默认情况下用户由镜像指定,所以:
docker image inspect 镜像名
docker inspect --format "{{.Config.User}}" 镜像名
如果查看结果为空,则容器默认以root用户运行。如果不为空,则有如下两种情况:一是镜像得创建者,专门指定了运行时得用户,二是用户在创建容器时设置了运行时得用户。
如上查看结果会导致问题:在镜像被用来创建容器的启动入口处,运行用户可能会发生改变,如上命令返回的即是容器创建时依赖的镜像的初始配置。如上问题解决方法:查看镜像内部,即进行简单的实验以确定默认用户。
docker run --rm --entrypoint "" busybox:1.29 whoami
docker run --rm --entrypoint "" busybox:1.29 id
在创建容器时更改运行时的用户,可以避免默认用户的问题。但需要在创建容器时更改运行时的用户 ,用户需要提前存在于使用的镜像时,但大多数情况下,镜像作者为了缩小镜像,不会创建过多的用户存在于镜像本身,所以需要查看哪些用户可以被使用,如下:
docker run --rm busybox:1.29 awk -F: '$0=$1' /etc/passwd
确定可以使用的用户后,就可以根据可使用的用户进行创建容器:
docker run --rm \
--user nobody \
busybox:1.29 id
实际运用时,这个标识选项可以接受任何用户与组的结对参数。且-u或--user可以接受使用名称指定用户,也可以使用ID进行指定。如下示例
docker run --rm \
-u nobody:nogroup \
busybox:1.29 id
docker run --rm -u 10000:20000 busybox:1.29 id
第二种命令启动时可以新建一个新的容器,并同时将运行的用户和运行时组设置为容器中不存在的用户和组。 如上情况时,所有的ID不会被解析为用户名或组名,但是所有文件权限都被设置为好像用户和组确实存在一样的作用。
如果是从自己的仓库中提取镜像或者构建自己的镜像时,那么镜像中的用户尽量自己设置。
在设置运行时地用户时,了解了容器内地用户如何与主机系统上的用户共享相同地用户ID。接下来的情况下,需要了解他们二者之间的交互方式。进行这种交互的主要原因在于卷中文件的文件访问权限问题。
一个例子:
以上示例显示:只要文件的所有者相同,那么卷中文件的访问权限在容器内部也适用。但同时也体现出用户ID空间在主机和容器间是共享的。
所以,除非希望容器可以访问某个文件,否则不要将这个文件以卷的方式挂载到容器中。
如上示例,我们已经了解了文件权限在主机和容器用户之间的交互方式,但因此,也可以解决比如,该如何处理写入卷的日志文件的权限。
第一个方法,是卷,卷已经在Docker之Docker容器数据卷这个文章中具体的介绍过了。但使用此方法,依旧需要考虑文件所有权和访问权限问题。
另一个方法是管理用户ID。
具体操作有两种方式:一种是提前编辑镜像,将用户ID设置为后续真正运行容器的用户ID;另一种是直接使用所需的用户ID和组ID。
Linux的用户(USR)命名空间能够将一个命名空间中的用户映射到另一个命名空间中的用户。用户命名空间的操作类似于进程标识符(PID)命名空间,其容器UID和GID是从主机的默认用户列表中划分出去的。
默认情况下,Docker容器不使用USR命名空间,这意味着以用户ID(数字而非名称)运行的容器,对主机文件拥有的权限与主机上拥有相同ID的用户的权限相同。在容器中,有效的文件系统一旦被挂载成功,在容器内进行的更改都将最终被保留再容器的文件系统内,但这会影响在容器之间或容器与主机之间共享文件的卷。
为容器启用用户命名空间之后,容器的UID将被映射到主机上的一系列非特权UID。操作人员可通过为Linux中的主机定义subuid和subgid映射并配置Docker守护进程的userns-remap选项来激活用户的命名空间的重映射功能。映射的主要目的是确定主机上的用户ID如何与容器命名空间中的用户ID对应。例如:UID重映射任务被配置为将容器UID映射到以主机UID5000开始,直到后1000个UID这一区间。结果是,容器中的UID0会被映射到主机的UID5000,容器中的UID1会被映射到UID5001,依此类推,直到1000个UID被映射完。从Linux的角度看,由于UID5000是非特权用户,没有修改主机系统文件的权限,因此大大降低了在容器中以uid=0运行程序的风险,即使容器中的进程从主机处获取了文件或其他资源,该进程也将作为重新映射的UID运行,并没有权限对获取的资源执行越权操作,除非操作人员授予权限。
用户命名空间重映射对于解决诸如读写卷的文件权限问题特别有用。可以加强应用程序的安全性。
Docker可以调整容器的授权以使用操作系统的各项功能。在linux中,这些功能授权被称为功能集。功能集的特点是:每当进程尝试进行初始系统调用(例如打开网络套接字)时,系统都会检查进程的功能集是否包含所需的功能。
创建新容器时,Docker几乎会删除功能集中的所有功能,只保留那些对于运行大多数应用程序必需且安全的功能。这进一步将运行进程与操作系统的管理功能区分开。提供给Docker容器的默认功能集已经合理地减少了一些功能,但有时还需要增加或减少一部分功能。通过在docker run或docker create使用--cap-drop标识选项,可以从容器中删除功能;通过在docker run或docker create使用--cap-add标识选项,可以从容器中添加功能。在使用过程中,可以多次使用--cap-add和--cap-drop选项进行增加或删除标识选项。
打印计算机上运行地容器化进程地默认功能集
[root@iZuf65upqet3c8ul8v6xqhZ ~]# docker run --rm -u nobody ubuntu:16.04 /bin/bash -c "capsh --print | grep net_raw"
Unable to find image 'ubuntu:16.04' locally
16.04: Pulling from library/ubuntu
58690f9b18fc: Pull complete
b51569e7c507: Pull complete
da8ef40b9eca: Pull complete
fb15d46c38dc: Pull complete
Digest: sha256:0f71fa8d4d2d4292c3c617fda2b36f6dabe5c8b6e34c3dc5b0d17d4e704bd39c
Status: Downloaded newer image for ubuntu:16.04
Current: = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap+i
Bounding set =cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
这两个选项用于构建合适地容器,并使其中地进程具有足够安全且正确地功能集。如果想知道容器中是否添加或删除了某些功能,可以检查容器并打印输出.HostConfig.CapAdd和.HostConfig.CapDrop成员属性。
当需要在容器中运行系统管理任务时,可以授予容器对计算机的访问特权。特权容器能保持容器的文件系统和网络隔离特性,具有对共享内存和主机设备的完全访问权限,同时拥有完整的系统功能。
特权容器在大部分情况下用于系统管理。在某些普通容器不能进行的操作时,特权容器是最好的选择。
如果在某些情况下只能通过降低特权容器的隔离程度才能解决问题,可以使用--privileged标识选项在docker create或docker run时。使用--privileged之后,特权容器仍被部分隔离。容器的网络命名空间仍将有效。如果希望完全拆除网络命名空间,可以将命令与--nethost表示选项结合在一起使用。
Docker使用合理的默认配置和配套完善的工具集来简化操作并推广最佳实践方案。大多数现代Linux内核都启用了seccomp配置文件,而Docker默认的secomp配置文件会阻止大多数程序不需要的40多个内核系统调用。
Docker提供了--security-opt标识选项,用于指定配置Linux的secomp和Linux安全模块(Linux security Modules,LMS)的功能选项。docker run或docker create时通过多次设置--security-opt标识选项,可以传递多个值。
seccomp负责配置某个进程可以激活的Linux系统调用。Docker默认的seccomp配置文件在默认情况下会阻止所有系统调用,然后显式地允许260个以上地系统调用被大多数程序安全使用。其中44个被seccomp阻止的系统调用对于普通程序来说是不必要的或不安全的,并且不能被命名空间使用。如果默认的seccomp配置文件过于严格或过于宽松,那么可以将自定义的配置文件指定为安全选项。
docker run --rm -it \
--security-opt seccomp= \
ubuntu:16.04 sh
在以上命令中,
Linux安全模块(LSM)是用来作为Linux操作系统和安全程序接口层的框架。AppArmor和SELinux作为LSM的供应者,两者都提供强制访问控制或MAC(系统定义访问规则),并且可以替换标准的Linux自由访问控制(由文件所有者定义访问规则)。
LSM安全规则可以采用下面七种格式之一:
- 为防止容器在启动后获得新的特权,可以使用no-new-privileges。
- 要设置SELinux用户标签,可以使用label=user:
格式,其中 是用于标签的用户名。 - 要设置SELinux角色标签,可以使用label=role:
格式,其中 是应用于容器中进程的角色名。 - 要设置SELinux类型标签,可以使用label=type:
格式,其中 是容器中进程的类型名。 - 要设置SELinux级别标签,可以使用label=level:
格式,其中 是容器中的进程应运行的级别。级别按照低-高对的形式指定。如果只提供低级别缩写,SELINUX会将范围解释为单级别 - 要禁用容器的SELINUX标签限制,可以使用label=disable格式。
- 要将AppArmor配置文件应用到容器,可以使用apparmor=
格式,其中 是要使用的AppArmor配置文件的名称。
以上内容得到,SELinux是一种标记系统。一组称为上下文的标签被应用于每个文件和系统对象,而一组相似的标签则被应用于每个用户和进程。 运行时,当进程尝试与文件或系统资源进行交互时,江金城上下文标签集与系统中的规则一一比对,比对的结果决定了是允许还是阻止进程与资源之间的交互。
对于docker来说,最安全的策略是构成最孤立的容器,并根据实际情况逐步减弱那些限制条件。docker通过使用默认的容器构造达到了最佳效果:提供合理的默认配置,而不会影响用户的工作效率。
默认情况下,Docker容器不是隔离最彻底的组件,而且Docker也不需要增强默认的隔离状态。但是需要的情况,Docker却允许减弱容器的隔离程度甚至去除隔离。
对于应用程序,最好的方法是隔离运行程序的风险。首先,确保应用程序以权限受到限制的用户身份运行,这样,即使出现问题,应用程序也无法更改计算机上的文件。其次,限制浏览器的系统功能,这样可以确保系统配置更安全。接下来,限制应用程序可以使用的CPU和内存额度,限制应用程序的资源占用有助于保留资源给系统,从而在关键时刻保证系统的响应速度。最后,最好可以将可以访问的设备列出白名单,使自己的设备不被窥探。
高层的系统服务与应用程序有所不同,运行时计算机必须确保他们已启动并处于运行状态。大多数情况下,他们需要有操作系统的访问特权才能正常运行。
低层的系统服务控制着设备或系统网络协议栈之类的组件。他们通常需要有系统组件的访问特权。低层的系统服务很少在底层的系统服务在容器中运行。所以,最好的例外是短期运行的容器配置。