还没对Docker加以限制?埋下的安全隐患了解一下

林伟壕 2019-07-26 10:11:00
作者介绍

林伟壕,腾讯高级工程师,专注于企业SDL、SecDevOps建设。目前从事安全风险评估与代码审计,曾在国内大型电信运营商与顶尖游戏公司从事运维、安全体系建设工作。

 

众所周知,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守护程序运行的用户。

 

 

ps aux | grep 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

 

结果,该命令被删的一干二净。

 

 

ls -lha /bin/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设置并重新启动该进程。


 

使用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将以其他用户身份运行。

 

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

 

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和内存限制。

 

>>>>

参考资料

 

  • Docker容器使用cgroups限制资源使用

    https://www.cnblogs.com/sammyliu/p/5886833.html

  • Docker使用Linux namespace隔离容器的运行环境

    https://www.cnblogs.com/sammyliu/p/5878973.html

最新评论
访客 2024年04月08日

如果字段的最大可能长度超过255字节,那么长度值可能…

访客 2024年03月04日

只能说作者太用心了,优秀

访客 2024年02月23日

感谢详解

访客 2024年02月20日

一般干个7-8年(即30岁左右),能做到年入40w-50w;有…

访客 2023年08月20日

230721

活动预告