docker 容器的安全很大程度上依赖 linux 本身,因为是共享宿主机内核
docker 安全评估主要考虑以下几个方面:
当docker run 启动一个容器时,后台会为容器创建一个独立的命名空间,这是最基础的隔离,让容器作为一个独立的个体存在。
运行一个容器
[root@server1 ~]# docker run -it --name vm1 busybox
运行后放入后台,过滤vm1的pid
[root@server1 ~]# docker inspect vm1 | grep Pid
"Pid": 8906,
"PidMode": "",
"PidsLimit": 0,
容器其实就是一个进程,查看它的pid,而且它的namespace就在/proc/2214/ns里。
但是这种方式与传统虚拟机相比,隔离的不彻底,因为容器就是一个进程,那么多个容器就还是共用宿主机的内核。
在 linux 内核中,有些资源和对象不能被 namespace 化,如:时间
docker run 启动一个容器时,后台会为容器创建一个独立的控制组策略集合
mount -t cgroup 可以查看到被挂接到/sys/fs/cgroup里
可以查看到docker容器的cpu分配
[root@server1 ns]# cd /sys/fs/cgroup/cpu/docker/
[root@server1 docker]# ls
cgroup.clone_children
cgroup.event_control
cgroup.procs
cpuacct.stat
cpuacct.usage
cpuacct.usage_percpu
cpu.cfs_period_us
cpu.cfs_quota_us
cpu.rt_period_us
cpu.rt_runtime_us
cpu.shares
cpu.stat
e067390a97abd9a788e6bd91031e95cea22b3f03a44d82dea3100226294e2502
notify_on_release
tasks
这里面的“e067390a97abd9a788e6bd91031e95cea22b3f03a44d82dea3100226294e2502”就是容器,是随机生成的字符串,
可以看到这个字符串里的内容就是继承上级的,也就是系统中的内容
[root@server1 docker]# ls e067390a97abd9a788e6bd91031e95cea22b3f03a44d82dea3100226294e2502/
cgroup.clone_children cpuacct.usage_percpu cpu.shares
cgroup.event_control cpu.cfs_period_us cpu.stat
cgroup.procs cpu.cfs_quota_us notify_on_release
cpuacct.stat cpu.rt_period_us tasks
cpuacct.usage cpu.rt_runtime_us
linux cgroup 还提供了很多有用特性,确保容器可以公平分享主机内存、cpu 等资源,
确保当容器内资源压力不会影响到宿主机和其他容器,在 DDos 方面必不可少。
内核能力机制是 linux 内核的一个强大特性,可提供细粒度的权限访问控制
大部分情况下,容器并不需要真正的 root 权限,只要少数能力即可
开启之前放入后台容器
[root@server1 docker]# docker container attach vm1
/ # id #查看到目前的身份是root
uid=0(root) gid=0(root) groups=10(wheel)
/ # ip link set down eth0 #但是没有钱权限关闭eth0网卡
ip: SIOCSIFFLAGS: Operation not permitted
确保只有可信的用户才能访问到 docker 服务,将容器的 root 用户
映射到本地主机的非 root 用户,减轻容器和主机之间因权限提升而引起的安全问题,允许docker 服务端在非 root 权限下运行,利用安全可靠的子进程来代理执行需要特权的操作(子进程只允许在特定范围内操作)
使用有增强安全特性的容器模板;用户可以定义更加严格的访问控制机制等(比如文件系统挂载到容器内部时,设置只读)
linux Cgroups:是限制一个进程组使用资源的上限,包括 cpu、内存、磁盘、带宽等
mount -t cgroup,查看到都挂载在/sys/fs/cgroup 下,df 也可以看
可以在docker run 启动时指定使用的内存、cpu等,使用docker run --help查看帮助
在 /sys/fs/cgroup/ 里面的子目录也叫子系统,限制系统的各项性能在每个子系统下,为每个容器创建一个控制组
root@server1 docker]# cd /sys/fs/cgroup/
[root@server1 cgroup]# ls
blkio cpu,cpuacct freezer net_cls perf_event
cpu cpuset hugetlb net_cls,net_prio pids
cpuacct devices memory net_prio systemd
并且如果在cpu目录里创建一个随机目录,里面会自动生成一些文件和cpu里的是差不多的,因为会自动从父级目录拷贝过去
并且子控制器和父级值是一样的,父级是针对所有控制器,子控制器的值可修改,针对某进程
[root@server1 cpu]# cat cpu.rt_period_us
1000000
[root@server1 cpu]# cat test1/cpu.rt_period_us
1000000
安装cgroup 命令行工具
[root@server1 cpu]# yum install -y libcgroup-tools.x86_64
[root@server1 cpu]# cat cpu.cfs_period_us #这是cpu的调用周期,单位是微妙
100000
[root@server1 cpu]# cat cpu.cfs_quota_us #cpu的配额限制,-1表示不限制
-1
[root@server1 cpu]# echo 20000 > test1/cpu.cfs_quota_us #更改test1里的数值,表示 100000 微秒的周期内,只能使用 20000
测试
[root@server1 cpu]# dd if=/dev/zero of=/dev/null & #执行一个无限循环的进程,不会消耗磁盘和io,只消耗cpu
[root@server1 cpu]# top
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
10945 root 20 0 107996 620 524 R 97.3 0.1 0:51.21 dd
让进程 id 和 test1/tasks 关联
[root@server1 cpu]# echo 10945 > test1/tasks
[root@server1 cpu]# top #使用top查看就可以看到限制在了20%左右
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
10945 root 20 0 107996 620 524 R 20.0 0.1 2:59.55 dd
运行容器指定参数限制,cpu调用周期100000微妙,配额限制20000
[root@server1 cpu]# docker run -it --name vm1 --cpu-period 100000 --cpu-quota 20000 busybox
进入运行的容器的cpu管理的子系统
[root@server1 cpu]# cd docker/20caadfc6e260a01c77cc266834bc18a9bf1c3612fe3ef926f7fed9ba1c976da/
[root@server1 20caadfc6e260a01c77cc266834bc18a9bf1c3612fe3ef926f7fed9ba1c976da]# cat cpu.cfs_quota_us
20000 #和我们运行时设置的一样
可以在容器里测试使用无限循环命令,查看top里cpu使用率是否限制在20%
容器可用内存包括:物理内存、swap 分区(操作系统也是)
但是一旦切换到 swap 分区,性能就不能保证了,因为 swap 是物理硬盘,当然没有内存快,
容器的内存限制很简单 docker run -it --memory 256M --memory-swap=256M ubuntu 就j解决了
[root@server1 ~]# docker run -it --name vm2 --memory 256M --memory-swap=256M busybox
同样找到内存限制的目录
在这里创建test2目录,修改这里面的文件数值
[root@server1 memory]# mkdir test2
[root@server1 memory]# cd test2/
[root@server1 test2]# ls
cgroup.clone_children memory.memsw.failcnt
cgroup.event_control memory.memsw.limit_in_bytes
cgroup.procs memory.memsw.max_usage_in_bytes
memory.failcnt memory.memsw.usage_in_bytes
memory.force_empty memory.move_charge_at_immigrate
memory.kmem.failcnt memory.numa_stat
memory.kmem.limit_in_bytes memory.oom_control
memory.kmem.max_usage_in_bytes memory.pressure_level
memory.kmem.slabinfo memory.soft_limit_in_bytes
memory.kmem.tcp.failcnt memory.stat
memory.kmem.tcp.limit_in_bytes memory.swappiness
memory.kmem.tcp.max_usage_in_bytes memory.usage_in_bytes
memory.kmem.tcp.usage_in_bytes memory.use_hierarchy
memory.kmem.usage_in_bytes notify_on_release
memory.limit_in_bytes tasks
memory.max_usage_in_bytes
更改内存限额
[root@server1 test2]# cat memory.limit_in_bytes #这个文件里的数值是内存限额,默认是不限制
9223372036854771712
重写如限定的数值256M
[root@server1 test2]# echo 268435456 > memory.limit_in_bytes
因为内存限额单位是字节,1Mb=1024Kb=1024*1024字节,所以数值是268435456
测试限制效果
[root@server1 test2]# df -h
tmpfs 496M 54M 443M 11% /dev/shm #这个目录下大概是1/2主机的内存,当前主机是 1G 内存
[root@server1 test2]# free -m #查看到可用的内存为512M
total used free shared buff/cache available
Mem: 991 235 408 66 347 512
在/dev/shm设备里,占用100M空间
[root@server1 test2]# cd /dev/shm
[root@server1 shm]# dd if=/dev/zero of=bigfile bs=1M count=100
100+0 records in
100+0 records out
104857600 bytes (105 MB) copied, 0.0541689 s, 1.9 GB/s
[root@server1 shm]# free -m #可用的内存减少了100M
total used free shared buff/cache available
Mem: 991 235 307 166 447 412
如果没有绑定进程 id 和 test12 ,就不会出现限制效果,就算占用300M也是可以的
所以来进行绑定再测试
删除之前的bigfile ,重新截取文件,指定限制之后创建300M的文件时可以看到到,内存的数值减少了255,但是swap分区也减少里45,也就是说内存被限制的时候,去使用了swqp的大小
[root@server1 shm]# free -m
total used free shared buff/cache available
Mem: 991 235 407 66 347 512
Swap: 1023 0 1023
[root@server1 shm]# cgexec -g memory:test2 dd if=/dev/zero of=bigfile bs=1M count=300
300+0 records in
300+0 records out
314572800 bytes (315 MB) copied, 0.156305 s, 2.0 GB/s
[root@server1 shm]# free -m
total used free shared buff/cache available
Mem: 991 236 152 320 602 257
Swap: 1023 45 978
对于这种多出去的不能使用内存,所以使用了 swap怎么去彻底限制
[root@server1 shm]# cd /sys/fs/cgroup/memory/test2/
[root@server1 test2]# cat memory.memsw.limit_in_bytes
9223372036854771712 #这是内存总的限制,现在无法直接更改因为刚才创建的bigfile使用里swap。所以需要删除bigfile
[root@server1 test2]# rm -f /dev/shm/bigfile
[root@server1 test2]# echo 268435456 > memory.memsw.limit_in_bytes #重写如数值也是256M,这样物理内存和 swap 一共不能超过 256M
[root@server1 test2]# cd -
/dev/shm
[root@server1 shm]# cgexec -g memory:test2 dd if=/dev/zero of=bigfile bs=1M count=300
Killed #操作被彻底限制了,大于256M的内存使用都是不可执行的
io的配置再/sys/fs/cgroup/blkio/目录里
[root@server1 shm]# cd /sys/fs/cgroup/blkio/
/sys/fs/cgroup/blkio/ #每秒读的数据量
#blkio.throttle.read_iops_device #每秒io的操作次数
来测试每秒写入数据量为 1M
查看磁盘信息
[root@server1 blkio]# fdisk -l #查看系统使用的磁盘名称
[root@server1 blkio]# ll /dev/vda #查看磁盘信息
brw-rw----. 1 root disk 252, 0 Jun 4 09:39 /dev/vda #看到磁盘的主号和辅号(252 和 0)
在/sys/fs/cgroup/blkio/新建目录test3,更改限制数值
[root@server1 blkio]# mkdir test3
[root@server1 blkio]# cd test3/
[root@server1 test3]# echo "252:0 1048576" > blkio.throttle.write_bps_device #1M=1048576=1024K*1024字节
[root@server1 test3]# cd ~
[root@server1 ~]# cgexec -g blkio:test3 dd if=/dev/zero of=testfile bs=1M count=10 #测试写入10M
10+0 records in
10+0 records out
10485760 bytes (10 MB) copied, 0.00496151 s, 2.1 GB/s
写入10M发现是成功的,这是因为 目前 block io 限制只对 direct io 有效(不使用文件缓存)
加参数使只对direct生效
[root@server1 ~]# cgexec -g blkio:test3 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.0014 s, 1.0 MB/s #耗时10s,限制生效了
通过docker run --help | grep device 查看对磁盘读写的限制参数
[root@server1 blkio]# docker run --help | grep device
--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 vm2 --device-write-bps /dev/vda:1MB ubuntu #限制在磁盘vda里写入限制是1M
root@f8bed1ba861c:/# 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.0018 s, 1.0 MB/s #写入10M耗时为10s
root@f8bed1ba861c:/#
ctrl + p + q 容器退出到后台
切换到运行的容器目录里
[root@server1 ~]# cd /sys/fs/cgroup/blkio/docker/f8bed1ba861c32d6ae9240b29839fad05575b7e6ae8167caf668c840f60c0fa6/
[root@server1 f8bed1ba861c32d6ae9240b29839fad05575b7e6ae8167caf668c840f60c0fa6]# cat blkio.throttle.write_bps_device
252:0 1048576 #这里的限制和我们在系统里设置的数值是一样的
[root@server1 ~]# cd /sys/fs/cgroup/freezer/
[root@server1 freezer]# cd docker/f8bed1ba861c32d6ae9240b29839fad05575b7e6ae8167caf668c840f60c0fa6/ #切换到刚才开启的容器的目录下
[root@server1 f8bed1ba861c32d6ae9240b29839fad05575b7e6ae8167caf668c840f60c0fa6]# cat tasks
6425 #查看进程号
[root@server1 f8bed1ba861c32d6ae9240b29839fad05575b7e6ae8167caf668c840f60c0fa6]# cat freezer.state
THAWED #查看状态,THAWED表示活跃的
[root@server1 freezer]# docker container pause vm2 #冻结vm2
vm2
[root@server1 freezer]# cat docker/f8bed1ba861c32d6ae9240b29839fad05575b7e6ae8167caf668c840f60c0fa6/freezer.state
FROZEN #状态变为冻结了
[root@server1 freezer]# docker container attach vm2 #也不能进入了
You cannot attach to a paused container, unpause it first
上面查看到的容器的进程号是6425,查看相关的进程
[root@server1 freezer]# ps ax | grep 6425
6425 pts/0 Ds+ 0:00 /bin/bash #此进程 D 表示冻结,暂停
解冻状态
[root@server1 freezer]# docker container unpause vm2 #解冻vm2
vm2
[root@server1 freezer]# ps ax | grep 6425
6425 pts/0 Ss+ 0:00 /bin/bash #状态变为S ,S表示休眠就是没有生效冻结
[root@server1 freezer]# docker container attach vm2 #可以正常进入vm2了
root@f8bed1ba861c:/#
[root@server1 freezer]# docker container prune
此命令可删除所有退出的容器
正常运行一个容器限制它的内存大小为256M,但是在容器看到的总内存和主机上看到的是一样的,这样就没有做到完全的隔离,也属于不安全的范畴,需要进行安全加固
[root@server1 freezer]# docker run -it --rm --memory 256MB --memory-swap 256MB ubuntu
root@8393353e33c7:/# free -m
total used free shared buffers cached
Mem: 991 815 175 320 0 489
-/+ buffers/cache: 325 665
Swap: 1023 5 1018
root@8393353e33c7:/# [root@server1 freezer]#
[root@server1 freezer]# free -m
total used free shared buff/cache available
Mem: 991 245 190 320 555 248
Swap: 1023 5 1018
之所以能看到容器里的内存大小和宿主机的一样是因为它的信息都是共享宿主机的
这里需要使用lxcfs文件系统工具进行解决,lxcfs的rpm包可以从网上自行下载
安装,运行
[root@server1 ~]# yum install -y lxcfs-2.0.5-3.el7.centos.x86_64.rpm
[root@server1 ~]# lxcfs /var/lib/lxcfs & #后台运行
#var/lib/lxcfs 是 lxcfs 默认数据目录
[root@server1 ~]# cd /var/lib/lxcfs/
[root@server1 lxcfs]# ls
cgroup proc #cgroup 是资源限制,proc 是系统信息
[root@server1 lxcfs]# ls proc/ #proc目录里有各种的限制,cpu、磁盘状态、内存信息、swap等
cpuinfo diskstats meminfo stat swaps uptime
我们可以将proc目录直接挂载到容器里,将我们需要的限制在目录中设置好。
运行容器挂载里面的所有文件,然后在容器里看到的就是限制过的内存大小了
[root@server1 ~]# docker run -it --name vm1 -m 256M \
> -v /var/lib/lxcfs/proc/cpuinfo:/proc/cpuinfo \
> -v /var/lib/lxcfs/proc/diskstats:/proc/diskstats \
> -v /var/lib/lxcfs/proc/meminfo:/proc/meminfo \
> -v /var/lib/lxcfs/proc/swaps:/proc/swaps \
> -v /var/lib/lxcfs/proc/stat:/proc/stat \
> -v /var/lib/lxcfs/proc/uptime:/proc/uptime \
> ubuntu
root@6c817fab0c89:/# free -m
total used free shared buffers cached
Mem: 256 3 252 317 0 0
-/+ buffers/cache: 3 252
Swap: 256 0 256
有时候需要容器具备更多权限,比如操作内核模块,控制 swap 分区,修改 MAC 地址等内容的时候,需要容器里的用户有不同的权限去操作
例如需要在容器的网卡去添加ip时,虽然是超户身份但是无法去操作
root@6c817fab0c89:/# ip addr add 172.170.0.10/24 dev eth0
RTNETLINK answers: Operation not permitted
就需要在运行容器时添加相应的参数
[root@server1 ~]# docker run --help | grep priv #查看权限参数
--privileged Give extended privileges to this
默认情况下这个权限是被禁止的
开启特权功能运行容器
[root@server1 ~]# docker run -it --name vm2 --privileged=true ubuntu #运行容器时开启特权功能
root@dd156cb88659:/# ip addr add 172.17.0.10/24 dev eth0 #添加ip成功
root@dd156cb88659:/# ip addr show eth0
16: eth0@if17: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
inet 172.17.0.10/24 scope global eth0
valid_lft forever preferred_lft forever
查看对比两个容器的特权设置
[root@server1 ~]# docker inspect vm2 | grep Pri
"Privileged": true, #开启
[root@server1 ~]# docker inspect vm1 | grep Pri
"Privileged": false, #关闭
默认情况下这个权限比较大,基本上接近宿主机的超户权限,为了防止容器用户的权限滥用,需要增加机制,只提供给容器必要的权限
在这个网址里可以看到linux的man文档:http://man7.org/linux/man-pages/man7/capabilities.7.html
可以看到 capability 的权限查询
参数 --cap-add 添加可允许的权限
[root@server1 ~]# docker run -it --name vm3 --cap-add=NET_ADMIN ubuntu #仅添加可操作网络的权限,其他的不开启
root@e4deadf8eb4c:/# ip addr add 172.17.0.11/24 dev eth0
root@e4deadf8eb4c:/# ip addr
18: eth0@if19: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:04 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.4/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
inet 172.17.0.11/24 scope global eth0
valid_lft forever preferred_lft forever
[root@server1 ~]# docker inspect vm3 | grep Pri
"Privileged": false, #可以看到它的特权是关闭的,但是可以添加ip
查看容器的信息里我们加入的权限
[root@server1 ~]# docker inspect vm3 | less #在里面找到"CapAdd"
"CapAdd": [
"NET_ADMIN" #可以看到添加的权限
],
也可以使用字典的方式查看对应的值
[root@server1 ~]# docker inspect -f {{.HostConfig.CapAdd}} vm3
[NET_ADMIN]
[root@server1 ~]# docker inspect -f {{.HostConfig.Privileged}} vm3
false
保证镜像安全:
使用安全的基础镜像,删除镜像中的 setuid 和 setgid 权限(suid、sgid);
启用 docker 内容信任(证书认证);
最小安装原则;
对镜像进行安全漏洞扫描,镜像安全扫描器(Clair);
容器使用非 root 运行。
保证容器安全:
对 docker 主机进行安全加固;
限制容器之间的网络流量(某个容器流量升高会对其他容器或主机有影响);
配置 docker 守护程序的 TLS 身份验证;
启用用户命名空间支持(userns-remap 参数,在容器第一次启动前就要加上,写在/etc/docker/daemon.json 文件里,还需要对 docker 数据目录做权限配置 /var/lib/docker);
限制容器内存使用量;
适当设置容器 CPU 优先级,/sys/fs/cgroup/cpu/cpu.shares 这个文件里。
docker安全遗留问题说明:
默认情况下只有 6 个 namespace,很多主要的内核子系统是没有命名空间的,如 SELinux,Cgroup,/sys 下的文件系统,/proc/sys,/proc/sysrq-trigger 等
[root@server1 ~]# docker inspect vm3 | grep Pid
"Pid": 12053,
[root@server1 ~]# cd /proc/12053/ns #切换到一个运行的docker进程 的命名空间里
[root@server1 ns]# ls #这里的就是以后的命令空间
ipc mnt net pid user uts
但是设备文件是没有命名空间的,例如:/dev/mem(进程和物理地址的映射),/dev/sd*文件系统设备,内核模块等