本文由 dbaplus 社群授权转载。
众所周知,Docker 使用 Namespace 进行环境隔离、使用 CGroup 进行资源限制。但是在实际应用中,还是有很多企业或者组织没有使用 Namespace 或者 CGroup 对容器加以限制,从而埋下安全隐患。
本文将简单介绍 Namespace 和 CGroup 的基本原理,再通过具体配置和应用向读者展示如何应用这些技术保护 Docker 容器安全,不过 Namespace 和 CGroup 并不是万能的,他们只是保障 Docker 容器安全的多种方案中的一类而已。
一、Namespace
1、概述
我们可以给容器分配有限的资源,这有助于限制系统和恶意攻击者可用的系统资源。每个容器所能获取的组件有:
可通过使用 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
复制代码
2、确定当前 Docker 用户
默认情况下,Docker 守护程序在主机上以 root 用户身份运行。通过列出所有进程,你可以识别 Docker 守护程序运行的用户。
由于守护程序以 root 身份运行,因此启动的任何容器将具有与主机的 root 用户相同的安全上下文。
docker run --rm alpine id
复制代码
这样是有安全风险的:如果 root 用户拥有的文件可从容器访问,则可以由正在运行的容器修改。
3、删除文件
下面让我们看看用 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
复制代码
结果,该命令被删的一干二净。
在这种情况下,容器能够从主机删除触摸二进制文件。
4、更改容器用户
可以通过更改用户、组上下文以及使用非特权用户运行的容器来规避以上风险。
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 出现的原因。
5、启用用户 Namespace
Docker 建议不要在启用 Namespace 模式和禁用 Namespace 模式之间来回切换 Docker daemon,执行此操作可能会导致镜像权限出现问题。
Namespace 是 Linux 内核安全功能,该功能允许 Namespace 或容器内的 root 用户访问主机上的非特权用户 ID。
6、任务
使用参数 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。
7、Namespace 保护
启用 Namespace 后,Docker dameon 将以其他用户身份运行。
启动容器时,容器内的用户将具有 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 命令仍然存在。
通过使用 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
复制代码
8、使用网络 Namespace
虽然 CGroup 可以限制进程使用的资源,但还需要 Namespace 控制进程的访问权限。
1)例子
启动容器时,将定义并创建网络接口。这为容器提供了唯一的 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
复制代码
如果进程监听端口,它们将在宿主接口上被监听并映射到容器。
9、使用 Pid 命名空间
与网络一样,容器可以看到的进程也取决于它所属的命名空间。更改 Pid 命名空间,将允许容器与超出其正常范围的进程进行交互。
1)例子
第一个容器将在其进程名称空间中运行。因此,它可以访问的唯一进程是在容器中启动的进程。
[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]
复制代码
10、共享命名空间
有时需要提供容器访问主机命名空间,如调试工具,但这被认为是不安全的做法。这是因为你正在打破可能引入漏洞的容器安全模型。
相反,如果需要,请使用共享命名空间来仅访问容器所需的命名空间。
1)例子
第一个容器启动 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
1、概述
CGroup 可为系统中所运行的任务或进程的用户群组分配资源,比如 CPU 事件、系统内存、网络带宽或者这些资源的组合。一般可以分为下面几种类型:
Resource limitation: 限制资源使用,比如内存使用上限以及文件系统的缓存限制。
Prioritization: 优先级控制,比如:CPU 利用和磁盘 IO 吞吐。
Accounting: 一些审计或一些统计,主要目的是为了计费。
Control: 挂起进程,恢复执行进程。
以下是一些常见的 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)
复制代码
2、定义内存限制
可以通过定义上限边界来帮助限制应用程序的内存泄漏或其他程序 bug。
1)例子
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
复制代码
3、定义 CPU 份额
虽然内存限制定义了设置的最大值,但 CPU 限制基于共享。这些份额是一个进程应该与另一个进程在处理时间上分配的权重。
如果 CPU 处于空闲状态,则该进程将使用所有可用资源。如果第二个进程需要 CPU,则将根据权重共享可用的 CPU 时间。
1)例子
下面是启动具有不同共享权重的容器的示例。
–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%的资源。
4、其他限制
诸如读写 IP 的限制,可以按照参考文档配置测试,测试效果如上面的 cpu 和内存限制。
作者介绍:
林伟壕,腾讯高级工程师,专注于企业 SDL、SecDevOps 建设。目前从事安全风险评估与代码审计,曾在国内大型电信运营商与顶尖游戏公司从事运维、安全体系建设工作。
原文链接:
https://mp.weixin.qq.com/s?__biz=MzI4NTA1MDEwNg==&mid=2650779454&idx=2&sn=16ddb02eaea3c708bb23439feef077e8&chksm=f3f91aabc48e93bdb2ff5f40de779c010345c0842dff85f214e811e77ead6fe40230df69eae4&scene=27#wechat_redirect
评论