众所周知,Docker使用namespace进行环境隔离、使用cgroup进行资源限制。但是在实际应用中,还是有很多企业或者组织没有使用namespace或者cgroup对容器加以限制,从而埋下安全隐患。本文定位于简单介绍namespace和cgroup的基本原理之后,通过具体配置和应用向读者展示如何应用这些技术保护docker容器安全,不过namespace和cgroup并不是万能的,他们只是保障Docker容器安全的多种方案中的一类而已。
我们可以给容器分配有限的资源,这有助于限制系统和恶意攻击者可用的系统资源。每个容器所能获取的组件有:
可通过使用namespace来实现限制资源。namespace就像一个“视图”,它只显示系统上所有资源的一个子集。这提供了一种隔离形式:在容器中运行的进程不能看到或影响其他容器中的进程或者宿主本身。
以下是一些常见的namespace类型实例。 Namespace例子
Cgroup CLONE_NEWCGROUP 限制root目录
IPC CLONE_NEWIPC System V IPC, POSIX消息队列
Network CLONE_NEWNET 网络设备、栈、端口等
Mount CLONE_NEWNS 挂载点
PID CLONE_NEWPID 进程ID
User CLONE_NEWUSER 用户和组ID
UTS CLONE_NEWUTS 主机名和NIS域名
Docker run 命令有几个参数和 namespace 相关:
IPC:
--ipc string IPC namespace to use
PID:
--pid string PID namespace to use
User:
--userns string User namespace to use
UTS:
--uts string UTS namespace to use
默认情况下,Docker守护程序在主机上以root用户身份运行。通过列出所有进程,你可以识别Docker守护程序运行的用户。
ps aux | grep docker
由于守护程序以root身份运行,因此启动的任何容器将具有与主机的root用户相同的安全上下文。
docker run --rm alpine id
这样时有安全风险的:如果root用户拥有的文件可从容器访问,则可以由正在运行的容器修改。
以下命令标识以root用户身份运行容器的风险。
首先,在我们的主机上创建touch命令的副本。
sudo cp /bin/touch /bin/touch.bak && ls -lha /bin/touch.bak
由于容器的/hos目录和宿主的/bin是同一个,因此可以从容器删除宿主上的文件,不信你试试。
docker run -it -v /bin/:/host/ alpine rm -f /host/touch.bak
结果,该命令被删的一干二净。
ls -lha /bin/touch.bak
在这种情况下,容器能够从主机删除触摸二进制文件。
可以通过更改用户和组上下文以及使用非特权用户运行的容器来规避以上风险。
docker run --user = 1000:1000 --rm alpine id
作为无特权用户,将无法删除二进制文件。
$ docker run -it -v /bin/:/host/ alpine rm -f /host/touch.bak
$ docker run --user=1000:1000 --rm alpine id
uid=1000 gid=1000
$ sudo cp /bin/touch /bin/touch.bak
$ docker run --user=1000:1000 -it -v /bin:/host/ alpine rm -f /host/touch.bak
rm: can't remove '/host/touch.bak': Permission denied
但是,如果我们在容器内部需要访问根目录,那么我们仍然会将自己暴露给前一个场景。这是namespace出现的原因。
Docker建议不要在启用namespace模式和禁用namespace模式之间来回切换Docker daemon,执行此操作可能会导致镜像权限出现问题。
namespace是Linux内核安全功能,该功能允许namespace或容器内的root用户访问主机上的非特权用户ID。
使用参数userns-remap启动Docker daemon时,将启用namespace。运行以下命令以修改Docker daemon设置并重新启动该进程。
curl https://gist.githubusercontent.com/BenHall/bb878c99d06a63cd8ed4d1c0a6941df4/raw/76136ffbca341846619086cfe40ab8e013683f47/daemon.json -o /etc/docker/daemon.json&& sudo service docker restart
使用cat /etc/docker/daemon.json查看设置
cat /etc/docker/daemon.json
{
"bip":"172.18.0.1/24",
"debug": true,
"storage-driver": "overlay",
"userns-remap": "1000:1000",
"insecure-registries": ["registry.test.training.katacoda.com:4567"]
}
重新启动后,你可以使用以下命令验证namespace是否到位
docker info | grep "Root Dir"
WARNING: No swap limit support
Docker Root Dir: /var/lib/docker/100000.100000
Docker将不再以root用户身份将文件存储在磁盘卷上。相反,所有内容都作为映射用户进行处理。 Docker Root Dir定义了Docker为映射用户存储数据的位置。
注意:在现有系统上启用此功能时,需要重新下载Docker Images。
启用namespace后,Docker Dameon将以其他用户身份运行。
ps aux | grep dockerd
启动容器时,容器内的用户将具有root权限。
docker run --rm alpine id
但是,用户将无法修改主机上运行的任何内容。
sudo cp / bin / touch /bin/touch.bak
docker run -it -v / bin /:/ host / alpine rm -f /host/touch.bak
与此前不同,我们的ps命令仍然存在。
ls -lha /bin/touch.bak
通过使用namespace,可以将Docker root用户分开,并提供比以前更强的安全性和隔离性。
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
$ sudo cp /bin/touch /bin/touch.bak
$ docker run -it -v /bin/:/host/ alpine rm -f /host/touch.bak
rm: can't remove '/host/touch.bak': Permission denied
$ ls -lha /bin/touch.bak
-rwxr-xr-x 1 root root 63K Aug 27 03:59 /bin/touch.bak
虽然cgroup控制进程可以使用多少资源,但命名空间还能控制进程的查看和访问权限。
启动容器时,将定义并创建网络接口。这为容器提供了唯一的IP地址和接口。
[root@host01 ~]# docker run -it alpine ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
14: eth0@if15: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:12:00:03 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.3/24 brd 172.18.0.255 scope global eth0
valid_lft forever preferred_lft forever
通过将命名空间更改为主机,而不是容器的网络与其接口隔离,该进程将可以访问主机网络接口。
[root@host01 ~]# docker run -it --net=host alpine ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP qlen 1000
link/ether 02:42:ac:11:00:11 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.17/16 brd 172.17.255.255 scope global enp0s3
valid_lft forever preferred_lft forever
inet6 fe80::b3ad:ecc4:2399:7a54/64 scope link
valid_lft forever preferred_lft forever
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
link/ether 02:42:cd:78:f0:22 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.1/24 brd 172.18.0.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::e9ad:a1a7:8b68:a0d1/64 scope link
valid_lft forever preferred_lft forever
5: veth158bc01@if4: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue master docker0 stateUP
link/ether 9e:bc:3d:01:53:95 brd ff:ff:ff:ff:ff:ff
inet6 fe80::ca3e:49ea:e1d0:8755/64 scope link
valid_lft forever preferred_lft forever
如果进程监听端口,它们将在宿主接口上被监听并映射到容器。
与网络一样,容器可以看到的进程也取决于它所属的命名空间。 通过更改pid命名空间,允许容器与超出其正常范围的进程进行交互。
第一个容器将在其进程名称空间中运行。 因此,它可以访问的唯一进程是在容器中启动的进程。
[root@host01 ~]# docker run -it alpine ps aux
PID USER TIME COMMAND
1 root 0:00 ps aux
通过将命名空间更改为主机,容器还可以查看系统上运行的所有其他进程。
[root@host01 ~]# docker run -it --pid=host alpine ps aux
PID USER TIME COMMAND
1 root 0:00 /usr/lib/systemd/systemd
2 root 0:00 [kthreadd]
4 root 0:00 [kworker/0:0H]
6 root 0:00 [mm_percpu_wq]
7 root 0:00 [ksoftirqd/0]
8 root 0:00 [rcu_sched]
9 root 0:00 [rcu_bh]
有时需要提供容器访问主机名称空间,例如调试工具,但被认为是不好的做法。这是因为你正在打破可能引入漏洞的容器安全模型。相反,如果需要,请使用共享命名空间来仅访问容器所需的命名空间。
第一个容器启动Nginx服务器。这将定义一个新的网络和进程命名空间。 Nginx服务器将自身绑定到新定义的网络接口的端口80。
docker run -d --name http nginx:alpine
其他容器现在可以使用语法容器重用此命名空间:<name>。 curl命令下面可以访问在localhost上运行的HTTP服务器,因为它们共享相同的网络接口。
docker run --net = container:http benhall / curl curl -s localhost
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
它还可以查看共享容器中的进程并与之交互。
docker run --pid=container:http alpine ps aux
PID USER TIME COMMAND
1 root 0:00 nginx: master process nginx -g daemon off;
6 100 0:00 nginx: worker process
7 root 0:00 ps aux
这对于调试工具很有用,例如strace。这允许你在不更改或重新启动应用程序的情况下为特定容器提供更多权限。
cgroup可为系统中所运行的任务或进程的用户群组分配资源,比如CPU事件、系统内存、网络带宽或者这些资源的组合。一般可以分为下面几种类型:
以下是一些常见的cgroup类型示例。
CGroups例子
--cpu-shares #限制cpu共享
--cpuset-cpus #指定cpu占用
--memory-reservation #指定保留内存
--kernel-memory #内核占用内存
--blkio-weight (block IO) #blkio权重
--device-read-iops #设备读iops
--device-write-iops #设备写iops
docker run中与cgroup相关的参数如下:
block IO:
--blkio-weight value Block IO (relative weight), between 10 and 1000
--blkio-weight-device value Block IO weight (relative device weight) (default [])
--cgroup-parent string Optional parent cgroup for the container
CPU:
--cpu-percent int CPU percent (Windows only)
--cpu-period int Limit CPU CFS (Completely Fair Scheduler) period
--cpu-quota int Limit CPU CFS (Completely Fair Scheduler) quota
-c, --cpu-shares int CPU shares (relative weight)
--cpuset-cpus string CPUs in which to allow execution (0-3, 0,1)
--cpuset-mems string MEMs in which to allow execution (0-3, 0,1)
Device:
--device value Add a host device to the container (default [])
--device-read-bps value Limit read rate (bytes per second) from a device (default [])
--device-read-iops value Limit read rate (IO per second) from a device (default [])
--device-write-bps value Limit write rate (bytes per second) to a device (default [])
--device-write-iops value Limit write rate (IO per second) to a device (default [])
Memory:
--kernel-memory string Kernel memory limit
-m, --memory string Memory limit
--memory-reservation string Memory soft limit
--memory-swap string Swap limit equal to memory plus swap: '-1' to enable unlimited swap
--memory-swappiness int Tune container memory swappiness (0 to 100) (default -1)
可以通过定义上限边界来帮助限制应用程序的内存泄漏或其他程序bug。
docker run -d --name mb100 --memory 100m alpine top
da4db4fd6b70501783c172b7459227c6c8e0426784acf1da26760d80eb2403b0
容器的内存使用可通过docker stats命令查看。
docker stats --no-stream
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
da4db4fd6b70 mb100 0.00% 440KiB / 100MiB 0.43% 6.21kB / 90B 1.06MB / 0B 1
虽然内存限制定义了设置的最大值,但CPU限制基于共享。这些份额是一个进程应该与另一个进程在处理时间上分配的权重。 如果CPU处于空闲状态,则该进程将使用所有可用资源。 如果第二个进程需要CPU,则将根据权重共享可用的CPU时间。
下面是启动具有不同共享权重的容器的示例。 --cpu-shares参数定义0-768之间的共享。 如果容器定义了768的份额,而另一个容器定义了256的份额,则第一个容器将具有50%的份额,而另一个容器具有25%的可用份额。 这些数字是由于CPU共享的加权方法而不是固定容量。 在第一个容器下方将允许拥有50%的份额。 第二个容器将限制在25%。
docker run -d --name c768 --cpuset-cpus 0 --cpu-shares 768 benhall/stress
docker run -d --name c256 --cpuset-cpus 0 --cpu-shares 256 benhall/stress
sleep 5
docker stats --no-stream
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
41fa6c06b148 c256 24.77% 736KiB / 737.6MiB 0.10% 2.1kB / 180B 0B / 0B 3
4555c9a0c612 c768 74.33% 732KiB / 737.6MiB 0.10% 2.19kB / 484B 0B / 0B 3
da4db4fd6b70 mb100 0.00% 444KiB / 100MiB 0.43% 12.7kB / 90B 1.06MB / 0B 1
docker rm -f c768 c256
有一点很重要,就是只要没有其他进程在,即便是定义了权重,启动的进程也能获得共享的100%的资源。
诸如读写IP的限制,可以按照参考文档配置测试,测试效果如上面的cpu和内存限制。