docker 容器的安全很大程度上依赖 linux 本身,因为是共享宿主机内核。
docker 安全评估主要考虑以下几个方面:
- linux 内核的命名空间(namespace)机制提供的容器隔离安全
- linux 控制组(cgroup)对容器资源的控制能力安全
- linux 内核的能力机制所带来的操作系统安全
- docker 程序(主要是服务器端)本身的抗攻击能力
- 其他安全增强机制的影响
Docker安全评估
- 命名空间隔离安全: docker run 启动一个容器时,后台会为容器创建一个独立的命名空间,
这是最基础的隔离,让容器作为一个独立的个体存在
[root@server1 ~]# docker run -it --name vm1 ubuntu
root@ed7c89bbbfe3:/# # ctrl + p + q 退出,让容器保持运行
[root@server1 ~]# docker inspect vm1 |grep Pid # 容器其实就是一个进程,查看他的pid
"Pid": 5279,
"PidMode": "",
"PidsLimit": 0,
For more details see ps(1).
[root@server1 ~]# ps ax | grep 5279
5279 pts/0 Ss+ 0:00 /bin/bash # 这时5279这个进程打开的bash
5346 pts/0 R+ 0:00 grep --color=auto 5279
[root@server1 ~]# cd /proc/5279/ns/ ## 进入它的命名空间
[root@server1 ns]# ls
ipc mnt net pid user uts # 这里就是命名空间中的内容
- 但是这种方式与传统虚拟机相比,隔离的不彻底,因为容器就是一个进程,那么多个容器就还 是共用宿主机的内核
- 在 linux 内核中,有些资源和对象不能被 namespace 化,如:时间,这样机不能保证完全隔离了
- 控制组资源控制安全: docker run 启动一个容器时,后台会为容器创建一个独立的控制组
策略集合
[root@server1 ns]# mount -t cgroup
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_prio,net_cls)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct,cpu)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
## 可以看到控制组的策略集合,
[root@server1 ~]# cd /sys/fs/cgroup/cpu/docker/
[root@server1 docker]# ls
cgroup.clone_children cgroup.procs cpuacct.usage cpu.cfs_period_us cpu.rt_period_us cpu.shares ed7c89bbbfe3ac5c6efef492b919eef7b108e655964b3e956ce197201a696e7e tasks
cgroup.event_control cpuacct.stat cpuacct.usage_percpu cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat notify_on_release
## 可以看到 cpu 资源控制组为 docker 容器分配的 cpu 资源
里面的 ed7c89bbbfe3ac5c6efef492b919eef7b108e655964b3e956ce197201a696e7e 就是我们刚才启动的一个容器随机生成的.
[root@server1 ed7c89bbbfe3ac5c6efef492b919eef7b108e655964b3e956ce197201a696e7e]# ls
cgroup.clone_children cgroup.procs cpuacct.usage cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_release
cgroup.event_control cpuacct.stat cpuacct.usage_percpu cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat tasks
而且里面的参数时完全继承上机目录的,也就系统中的资源
- linux cgroup 还提供了很多有用特性,确保容器可以公平分享主机内存、cpu 等资源
- 确保当容器内资源压力不会影响到宿主机和其他容器,在 DDos 方面必不可少
- 内核能力机制 : 是 linux 内核的一个强大特性,可提供细粒度的权限访问控制
大部分情况下,容器并不需要真正的 root 权限,只要少数能力即可
[root@server1 ~]# docker attach vm1 # 连上刚才的容器
root@ed7c89bbbfe3:/# id
uid=0(root) gid=0(root) groups=0(root) # 看出是超级用户,但是没有超户的权力
root@ed7c89bbbfe3:/# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default 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
10: eth0@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
root@ed7c89bbbfe3:/# ip link set down eth0
RTNETLINK answers: Operation not permitted #但是想要关闭网卡,不行,虽然是 root
-
docker 服务端的防护:确保只有可信的用户才能访问到 docker 服务;将容器的 root 用户映射到本地主机的非 root 用户,减轻容器和主机之间因权限提升而引起的安全问题;允许docker 服务端在非 root 权限下运行,利用安全可靠的子进程来代理执行需要特权的操作(子进程只允许在特定范围内操作)
-
其他安全特性:使用有增强安全特性的容器模板;用户可以定义更加严格的访问控制机制等
(比如文件系统挂载到容器内部时,设置只读)
容器资源控制
- linux Cgroups:组资源控制是限制一个进程组使用资源的上限,包括 cpu、内存、磁盘、带宽等
[root@server1 ~]# mount -t cgroup
cgroup on /sys/fs/cgroup/systemd type cgroup /lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup
cgroup on /sys/fs/cgroup/perf_event type cgroup
cgroup on /sys/fs/cgroup/hugetlb type cgroup
cgroup on /sys/fs/cgroup/devices type cgroup
cgroup on /sys/fs/cgroup/pids type cgroup
cgroup on /sys/fs/cgroup/memory type cgroup
cgroup on /sys/fs/cgroup/blkio type cgroup
cgroup on /sys/fs/cgroup/freezer type cgroup
cgroup on /sys/fs/cgroup/cpuset type cgroup
## 看到都挂载在/sys/fs/cgroup 下
[root@server1 ~]# cat /etc/security/limits.conf
## 我们在这里也可以设置进程数,内存等,但是不是很准确,因为还有 swap 分区,swap 分区也是可以使用的。
所以我们可以在 docker 启动是指定使用的内存,cpu 等:
cpu限制
## 使用 docker run --cpu --memory 进行指定就可以了
那我们如何在系统中进行设定那:
[root@server1 ~]# cd /sys/fs/cgroup/
[root@server1 cgroup]# ls
blkio cpu cpuacct cpu,cpuacct cpuset devices freezer hugetlb memory net_cls net_cls,net_prio net_prio perf_event pids systemd
# 这个目录下都是限制系统各项内容的,有磁盘IO,cpu 内存,设备等。
里面的子目录也叫子系统。这里面每个子系统都有docker容器的目录。
我们可以在在每个子系统下,为每个进程创建一个控制组。
[root@server1 cpu]# mkdir a1
[root@server1 cpu]# ls a1/
cgroup.clone_children cgroup.procs cpuacct.usage cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_release
cgroup.event_control cpuacct.stat cpuacct.usage_percpu cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat tasks
[root@server1 cpu]# cat cpu.rt_period_us
1000000
[root@server1 cpu]# cat a1/cpu.rt_period_us
1000000
# 我们在cpu子系统下建立一个控制组,可以看出子控制器和父级值是一样的,父级是针对所有控制器,子控制器的值可修改,针对某进程。
我们现在在操作系统层面控制(也可以在容器内)
[root@server1 cpu]# yum install libcgroup-tools.x86_64 -y #安装 cgroup 命令行工具
[root@server1 cpu]# cat cpu.cfs_period_us
100000 # cpu 调用周期,单位:微秒
[root@server1 cpu]# cat cpu.cfs_quota_us
-1 # 配额限制,-1 表示不限制
[root@server1 cpu]# cd a1/
[root@server1 a1]# echo 20000 > cpu.cfs_quota_us # 表示 100000 微秒的周期内,只能使用 20000,也就是 20%
[root@server1 a1]# cat cpu.cfs_quota_us
20000
验证:
[root@server1 a1]# dd if=/dev/zero of=/dev/null &
[1] 5796
# 这是一个无限循环的进程,不会占用磁盘和 IO,只会消耗 cpu
我们发现占用了几乎100的cpu,这是因为我们并没有将这个进程和这个限制关联起来,
所以:
[root@server1 a1]# echo 5796 > tasks
# 让进程 id 和 tasks 关联,tasks 里面是那个进程,这个资源组就控制的谁
再次top查看:
这次就限制到了20%。
那末我们在手动指定容器试一下:
[root@server1 a1]# docker run -it --name vm2 --cpu-period 100000 --cpu-quota 20000 ubuntu
## 指定配额为20000 CTRL +P+Q 先退出,在docker资源组中查看:
[root@server1 a1]# cd ../docker/
[root@server1 docker]# cd fc8692dd94d170ff83dd274e28049eb68a617733e91dda6145a171541b535101/
## 进入我们的容器VM2的目录
[root@server1 fc8692dd94d170ff83dd274e28049eb68a617733e91dda6145a171541b535101]# ls
cgroup.clone_children cgroup.procs cpuacct.usage cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_release
cgroup.event_control cpuacct.stat cpuacct.usage_percpu cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat tasks
[root@server1 fc8692dd94d170ff83dd274e28049eb68a617733e91dda6145a171541b535101]# cat cpu.cfs_quota_us
20000
查看,却是是20000,所以对容器的限制就是我们在开启是加上参数就行了
我们再再容器中用dd 做测试:
[root@server1 cpu]# docker attach vm2
root@fc8692dd94d1:/# dd if=/dev/zero of=/dev/null &
[1] 15
## ctrl +p +q 退出
可以看到,对容器里面的进程也是其效果的,和刚才在命令行中的dd 一样,都只占用了20%的cpu。
内存限制
容器可用内存包括:物理内存、swap 分区(操作系统也是)。
- 但是一旦切换到 swap 分区,性能就不能保证了,因为 swap 是物理硬盘,当然没有内存快’
- 容器的资源限制很简单 docker run -it --memory 256M --memory-swap=256M ubuntu 就可以了
操作系统层面的限制:
[root@server1 memory]# cd /sys/fs/cgroup/memory/
[root@server1 memory]# mkdir a2
[root@server1 memory]# cd a2
[root@server1 a2]# ls
cgroup.clone_children memory.kmem.failcnt memory.kmem.tcp.limit_in_bytes memory.max_usage_in_bytes memory.move_charge_at_immigrate memory.stat tasks
cgroup.event_control memory.kmem.limit_in_bytes memory.kmem.tcp.max_usage_in_bytes memory.memsw.failcnt memory.numa_stat memory.swappiness
cgroup.procs memory.kmem.max_usage_in_bytes memory.kmem.tcp.usage_in_bytes memory.memsw.limit_in_bytes memory.oom_control memory.usage_in_bytes
memory.failcnt memory.kmem.slabinfo memory.kmem.usage_in_bytes memory.memsw.max_usage_in_bytes memory.pressure_level memory.use_hierarchy
memory.force_empty memory.kmem.tcp.failcnt memory.limit_in_bytes memory.memsw.usage_in_bytes memory.soft_limit_in_bytes notify_on_release
[root@server1 a2]# cat memory.limit_in_bytes # 这里是内存的最大限制,默认是不限制的
9223372036854771712
那我们限制最大只能使用256M: 256*1024*1024=268435456
[root@server1 a2]# echo 268435456 > memory.limit_in_bytes
[root@server1 a2]# cat memory.limit_in_bytes
268435456
测试:
[root@server1 shm]# df -H
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/rhel-root 19G 3.0G 16G 16% /
devtmpfs 508M 0 508M 0% /dev
tmpfs 520M 0 520M 0% /dev/shm
#这个目录挂载的是内存的 1/2,当前主机是 1G 内存,那么这里就是 512M,我们用它占用内存
[root@server1 shm]# free -m
total used free shared buff/cache available
Mem: 991 131 356 12 502 689 # 内存当前可用为689M
Swap: 2047 0 2047 # swap 分区为2047M
进行截取:
[root@server1 shm]# cd /dev/shm/ # 进入这个目录
[root@server1 shm]# ls
[root@server1 shm]# dd if=/dev/zero of=bigfile bs=1M count=100 # 截取100M
100+0 records in
100+0 records out
104857600 bytes (105 MB) copied, 0.0557332 s, 1.9 GB/s
[root@server1 shm]# ls
bigfile
[root@server1 shm]# free -m
total used free shared buff/cache available
Mem: 991 129 258 112 603 592 #确实少了100
Swap: 2047 0 2047
[root@server1 shm]# dd if=/dev/zero of=bigfile bs=1M count=200
200+0 records in
200+0 records out
209715200 bytes (210 MB) copied, 0.125939 s, 1.7 GB/s # 截取200M
[root@server1 shm]# free -m
total used free shared buff/cache available
Mem: 991 129 158 212 703 491
Swap: 2047 0 2047
[root@server1 shm]# dd if=/dev/zero of=bigfile bs=1M count=300 # 截取300M
300+0 records in
300+0 records out
314572800 bytes (315 MB) copied, 0.164207 s, 1.9 GB/s
[root@server1 shm]# free -m
total used free shared buff/cache available
Mem: 991 130 70 312 790 392
Swap: 2047 1 2046
## 我们发现,刚刚明明设置了256M,这里却300M都可以使用了,
还是因为我们没有进行绑定。
##刚才装的 cgexec 命令,可以直接在命令行控制资源组
[root@server1 shm]# cgexec -g memory:a2 dd if=/dev/zero of=bigfile bs=1M
## memory和a2这个控制器进行绑定
[root@server1 shm]# free -m
total used free shared buff/cache available
Mem: 991 129 270 112 590 591 # 少了100M
Swap: 2047 0 2047
[root@server1 shm]# cgexec -g memory:a2 dd if=/dev/zero of=bigfile bs=1M count=300
300+0 records in # 截取300M
300+0 records out
314572800 bytes (315 MB) copied, 0.185631 s, 1.7 GB/s
[root@server1 shm]# free -m
total used free shared buff/cache available
Mem: 991 130 115 266 744 436 # 这里至少了253 M 确实限制再了256M之内
Swap: 2047 46 2001
[root@server1 shm]# ls
bigfile
但是这样的文件竟然创建成功了,并没有报错,而且我们发现swap分区少了36M,
这就说明:多出去的不能使用内存,所以使用了 swap,那我们怎么彻底限制哪?
[root@server1 shm]# cd /sys/fs/cgroup/memory/a2
[root@server1 a2]# cat memory.memsw.limit_in_bytes
9223372036854771712 # 我们发现swap分也是没有被限制的
[root@server1 a2]# echo 268435456 > memory.memsw.limit_in_bytes # 也限制为256M
-bash: echo: write error: Device or resource busy # 失败时因为我们刚才使用到了swap分区
[root@server1 a2]# rm -fr /dev/shm/bigfile
[root@server1 a2]# echo 268435456 > memory.memsw.limit_in_bytes
## 现在就可以了。改完后表示物理内存和 swap 一共使用不能超过 256M
#测试:
[root@server1 shm]# cgexec -g memory:a2 dd if=/dev/zero of=bigfile bs=1M count=300
Killed
发现这个就被杀掉了,说明限制成功
block io限制,对磁盘读写的限制
[root@server1 shm]# cd /sys/fs/cgroup/blkio/
[root@server1 blkio]# mkdir a3
[root@server1 blkio]# cd a3/
[root@server1 a3]# ls
blkio.io_merged blkio.io_service_bytes_recursive blkio.io_wait_time blkio.sectors blkio.throttle.read_iops_device blkio.weight notify_on_release
blkio.io_merged_recursive blkio.io_serviced blkio.io_wait_time_recursive blkio.sectors_recursive blkio.throttle.write_bps_device blkio.weight_device tasks
blkio.io_queued blkio.io_serviced_recursive blkio.leaf_weight blkio.throttle.io_service_bytes blkio.throttle.write_iops_device cgroup.clone_children
blkio.io_queued_recursive blkio.io_service_time blkio.leaf_weight_device blkio.throttle.io_serviced blkio.time cgroup.event_control
blkio.io_service_bytes blkio.io_service_time_recursive blkio.reset_stats blkio.throttle.read_bps_device blkio.time_recursive cgroup.procs
# blkio.throttle.read_bps_device 每秒读的数据量
# blkio.throttle.read_iops_device 每秒的 io 操作次数
## '设置每秒写入数据量为 1M'
[root@server1 a3]# fdisk -l
Device Boot Start End Blocks Id System
/dev/sda1 * 2048 2099199 1048576 83 Linux
/dev/sda2 2099200 41943039 19921920 8e Linux LVM
# 我们当前使用的磁盘是 /dev/sda
[root@server1 a3]# ll /dev/sda
brw-rw---- 1 root disk 8, 0 May 27 11:37 /dev/sda ##看到磁盘的主号和辅号(8 和 0)
[root@server1 a3]# echo "8:0 1048576" > blkio.throttle.write_bps_device
#1024 * 1024 = 1048576
[root@server1 ~]# cgexec -g blkio:a3 dd if=/dev/zero of=testfile bs=1M count=10
10+0 records in
10+0 records out
10485760 bytes (10 MB) copied, 0.00391746 s, 2.7 GB/s
## 没有生效,速率为2.7G/s
这是因为目前 block io 限制只对 direct io 有效(不使用文件缓存)
[root@server1 ~]# cgexec -g blkio:a3 dd if=/dev/zero of=testfile bs=1M count=10 oflag=direct
10+0 records in
10+0 records out
10485760 bytes (10 MB) copied, 10.0016 s, 1.0 MB/s # 生效了,耗时10s
那我们docker中怎样去限制哪?
在使用容器时也有相关参数,我们在使用时直接docker run 加上参数就好了:
[root@server1 a3]# docker run --help |grep device
--blkio-weight-device list Block IO weight (relative device weight) (default [])
--device list Add a host device to the container
--device-cgroup-rule list Add a rule to the cgroup allowed devices list
--device-read-bps list Limit read rate (bytes per second) from a device (default [])
--device-read-iops list Limit read rate (IO per second) from a device (default [])
--device-write-bps list Limit write rate (bytes per second) to a device (default [])
--device-write-iops list Limit write rate (IO per second) to a device (default [])
[root@server1 ~]# docker run -it --name vm1 --device-write-bps /dev/sda:1MB ubuntu #创建容器
root@36784d59d0f5:/# dd if=/dev/zero of=testfile bs=1M count=10 oflag=direct # 截取文件
10+0 records in
10+0 records out
10485760 bytes (10 MB) copied, 10.2338 s, 1.0 MB/s # 限制生效了
## ctrl + p + q 退出
# 那我们去操作系统层面去查看
[root@server1 ~]# cd /sys/fs/cgroup/blkio/docker/
[root@server1 docker]# cd 36784d59d0f5841ba3c22be228e97aa537f87eb39915a7568046bf7151c04139/
[root@server1 36784d59d0f5841ba3c22be228e97aa537f87eb39915a7568046bf7151c04139]# cat blkio.throttle.write_bps_device
8:0 1048576 # 和之前手动写入到 a3 中的一样
其它限制
[root@server1 ~]# cd /sys/fs/cgroup/freezer # 管控系统进程暂停与恢复的
[root@server1 freezer]# cd docker/36784d59d0f5841ba3c22be228e97aa537f87eb39915a7568046bf7151c04139/
[root@server1 36784d59d0f5841ba3c22be228e97aa537f87eb39915a7568046bf7151c04139]# cat tasks
6485 ## 查看进程号
[root@server1 36784d59d0f5841ba3c22be228e97aa537f87eb39915a7568046bf7151c04139]# cat freezer.state
THAWED ## 查看进程状态,THAWED表示活跃的
[root@server1 36784d59d0f5841ba3c22be228e97aa537f87eb39915a7568046bf7151c04139]# docker pause vm1
vm1 ## 冻结vm1
[root@server1 36784d59d0f5841ba3c22be228e97aa537f87eb39915a7568046bf7151c04139]# cat freezer.state
FROZEN ## 状态为冻结
[root@server1 36784d59d0f5841ba3c22be228e97aa537f87eb39915a7568046bf7151c04139]# docker attach vm1
You cannot attach to a paused container, unpause it first ## 尝试连接被拒绝
[root@server1 36784d59d0f5841ba3c22be228e97aa537f87eb39915a7568046bf7151c04139]# cat tasks
6485 ## 但是进程依然存在
[root@server1 36784d59d0f5841ba3c22be228e97aa537f87eb39915a7568046bf7151c04139]# ps ax
6485 pts/0 Ds+ 0:00 /bin/bash ## D 表示冻结,暂停
[root@server1 36784d59d0f5841ba3c22be228e97aa537f87eb39915a7568046bf7151c04139]# docker unpause vm1
vm1
[root@server1 36784d59d0f5841ba3c22be228e97aa537f87eb39915a7568046bf7151c04139]# cat freezer.state
THAWED
[root@server1 36784d59d0f5841ba3c22be228e97aa537f87eb39915a7568046bf7151c04139]# ps ax |grep bash
6485 pts/0 Ss+ 0:00 /bin/bash
[root@server1 36784d59d0f5841ba3c22be228e97aa537f87eb39915a7568046bf7151c04139]# docker attach vm1
root@36784d59d0f5:/# # 就可以连接了,然后退出
[root@server1 ~]# docker container prune ## 删除所有退出的容器