对于构建安全的应用程序来讲,如何准确的识别安全边界(security boundaries)至关重要,安全边界也被称作是可信边界(trust boundary)。在软件系统中,我们可以将系统划分为不同的安全区域,区域之间就形成了安全边界,比如笔者在负责某区域性银行的营销中台交付工作的时候,整个智能营销平台分为互联网可见区,DMZ区,生产区等,我们需要在安全边界上设置相应的权限,来让不同的安全区的组件,应用可以相互访问。
有些时候这些安全边界需要人为的来设定,就类似于在Linux操作系统上,系统管理员可以为收到安全保护的文件设置组权限一样,规定哪些人可以修改,读取对应的文件。
在容器化部署的场景下,容器实例天然就是安全边界,运行在容器实例中的是我们的业务代码,我们一般情况下不允许容器中的代码访问容器外的服务和数据,但是如果的确需要,也可以通过挂载数据卷的形式,来让外部的数据在容器进程内部可见(关于kubernetes中如何挂载数据卷的详细信息,可以参考笔者和Kubernetes存储相关的文章)。
从原理上看,恶意攻击者和目标之间的安全边界的数量越多,整个系统越安全,因为恶意攻击者必须攻破所有的安全边界上的安全措施,才能达到自己的目的。笔者在容器安全模型(上)这篇文章中介绍了威胁模型,攻击者可以选择其中的集中攻击的首选,来达到自己窃取数据,瘫痪系统的目的,具体的步骤如下:
1,首先恶意攻击者通过扫描容器化部署的应用,发现镜像中部署的代码使用了一个有高危漏洞的三方依赖包,这依赖包可以用来执行远程代码。
2,恶意攻击者基于第一步发现的漏洞成功攻破了容器实例,但是因为容器实例中没有有价值的数据,因此恶意攻击者必须执行逃逸攻击。也就是需要能够跳出容器的边界,来访问其他容器中的数据,或者宿主机。容器逃逸攻击可能由不安全的配置造成,假设黑客成功了完成了逃逸,那么他现在就可以访问宿主机了。
3,恶意攻击者在宿主机上做的第一件事情就是试图获取root权限,如果我们的容器实例以root权限运行,那么获取root权限对于恶意攻击者来说简直易如反掌。
4,恶意攻击者有了宿主机的root权限后,基本上可以干任何事情了。特别是在Kubernrtes平台上,恶意攻击者可以访问API Server,可以访问Kuberntes中保存在Secret中的隐私数据,比如获取到数据库的登陆信息后,可以直接访问数据库中的电商系统用户名和密码的信息。
如上边的步骤所示,如果我们在每个组件之间都设置了安全边界,并对其进行加固,那么我们容器化部署的应用程序就能极大的降低被攻破的可能性,敏感数据就不会轻易的被窃取。
对于容器安全来说,我们必须特别考虑应用被从内部攻破的风险,特别是在容器化部署的场景下,相同的物理机上可能运行了两个完全不同的应用,你必须时刻提防”nosiy neighbor“的问题,共享机器资源的这种方式在容器化部署中也称作是multitenancy,这种方式对我们容器化部署应用的安全模型提出了巨大的挑战(你的时刻提防邻居pod上运行的是黑客程序啊)。
在多租户(multitenant)环境下,不同用户或者租户的应用程序运行在一组共享的硬件资源上。由于不同的用户可能属于同一个组织,也可能完全不认识,属于不同的组织,我们需要在不同的应用实例之间提供较强的安全边界,防止相互影响。多租户这个概念不是新发明的,从大型机时代就被广泛使用了,比如在大型机时代,多个用户可以并行使用同一台大型机来处理自己的任务,而每个用户会分配一定比例的CPU时间,内存和存储资源。
而大型机上的这种资源共享的概念和我们今天在阿里云或者其他云平台上看到的资源分配方式没有本质的区别。如果读者有阿里云的账号,可以在阿里云上通过IAAS服务来租用CPU,内存和存储空间,以及其他中间件服务。阿里云也提供ECS的服务,我们直接可以在阿里云上租用虚拟机,通过租用的虚拟机来运行自己的业务,这就是多租户的场景。
注:多租户这个概念其实也被过载了,笔者在某头部航司的业务中台项目上,数据库规范中有一个叫租户ID的字段,用来承载南航后续多租户业务场景。这种多租户本质上是软件的多租户共享,类似于SAAS的概念,而本文中的多租户主要指的硬件资源,请读者务必注意。
共享机器是另外一种多租户,比如一台机器创建多个账号,管理员为每个账号分配不同的权限,并且有对应的文件夹访问权限。这种模式在容器化场景非常普遍,因为多个容器会运行在同一台虚拟机或者物理机上,如果虚拟机上有docker deamon,那么可以运行docker命令的用户就可以做很多破坏性操作。在企业级部署平台上,特别是多住户的情况下,一个应用程序和另外一个应程序的VM是分开,很少出现在多租户情况下混部的情况。
坦白讲,VM是一种隔离程度更加彻底的虚拟化机制,因为运行在一个虚拟机中的用户,是无法通过底层的hypervisor访问另外一台VM中的数据,大白话说就是VM中的用户是无法窃取另外一台VM中运行的数据。但是我们并不能说VM提供了绝对的安全性。因为安全涉及到CIA,其中A代表可用性,而VM也存在noisy neighbor的问题,一台机器上的某个VM将网络资源给占用干净了,那么其他应用的可用性就会受到影响。
对于政府类型的项目,金融类型的项目,即便是在VM这种隔离机制下, 也需要考虑如何来保障数据不会发生泄漏,因此我们一般需要考虑做物理隔离。比如阿里巴巴提供了专有云服务,企业可以选择在自己的数据中心部署一套专有云平台,来确保绝对的物理隔离。比如多个国企类型的客户案例上,都采用了专有云的部署方式。一般情况下专有云会更加安全,因为能够访问专有云资源的账号,都是企业经过授权和批准的账号,因此会更加安全。当然公共云上也是非常安全,因为公共云客户的VM都是专用的,因此不存在nosiy neighbor的安全风险。
容器和VM比起来,隔离程度不如VM,但是容器提供了另外一种”隔离性“(读过笔者关于容器的多篇原理文章的同学应该知道,容器本质上没有任何隔离性可言,这也是笔者加引号的原因)。但是作为系统的管理人员,我们一般不会把别人应用的容器和我们的应用部署在同一台虚拟机上,能够部署上的都肯定是有互相信任的应用程序。但是中国有句古话啊,防人之心不可无,因此我们也需要有安全的机制确保两个应用之间不会相互影响。
在Kuberntes平台上,我们可以使用namespace来做逻辑隔离,namespace可以用来给不同的团队,不同的环境(测试,开发,uat等)提供资源的隔离。比如我们在某头部航空企业的业务中台项目上,就是使用namespace来隔离测试和生产环境的容器应用,虽然说笔者强烈不统一这种逻辑隔离(生产和非生产,因为ETCD是同一套,因此压测环境可能对生产造成影响)。但是考虑到客户的实际情况,只有一朵云,只能从流程层面保障生产环境的稳定性。
注:命名空间这个词最近几年随着容器化的发展也过载了。在Kuberntes平台上,命名空间是个抽象的资源划分概念,大白话说就是资源的逻辑隔离。而读过笔者的容器本质文章的同学,应该知道支持容器的三个pillar是:命名空间,cgroups和chroot。在Linux操作系统中,命名空间是操作系统层面的功能(机制),用来隔离进程可以看到的系统资源,是容器实现的基础。
在Kuberntes平台上,我们可以使用RBAC(基于角色的权限控制)机制来限制不同用户能够访问的资源和组件的范围,不过大家需要注意的是,在Kuberntes上,RBAC能够控制的是API Server暴露出来的资源。而运行在POD中的容器实例(比如Docker运行时)相互之前是通过容器的安全边界来保证安全,即便从Kubernetes的角度看,这些容器实例属于不同的命名空间。最坏的情况是,如果恶意攻击者能够从一个容器中逃逸,那么Kuberntes提供的RBAC就成摆设了。
最后我们来看看托管场景下的安全考虑。阿里云提供了很多托管服务,比如我们可以在阿里云上开通一台RDS for MYSQL的数据库服务器实例,几分钟后,一台可以用来开发和生产用的数据库就ready了。对于用户来说,不需要承担运维等繁重的工作,这些专业的工作就留给阿里云的TAM团队来负责就好了。托管服务也已经扩展到了容器化部署领域,比如阿里巴巴提供的ACK服务,开通之后,我们配合EDAS服务就可以比较轻松的部署和管理自己基于微服务架构的分布式应用程序,而不用担心底层的硬件以及运维工作。
ACK可以节约我们很多宝贵的时间,并且我们可以随时基于业务的流量进行扩容和缩容,使用硬件资源就如同使用水电煤一样,按需付费。并且阿里云提供了不同的虚拟机选项,如果我们对自己的应用安全非常看重,那么建议购买单独的VM来承载自己的应用程序,你可不想在公共云上老是考虑nosiy neighbor的问题。
好了,对不同的安全边界和潜在的安全风险进行了深入的分析和讨论之后,我们最后来看看几个非常重要的安全原则,笔者强烈建议大家在自己实际的项目中,多从这些角度去思考自己所负责系统具体有哪些安全风险需要mitigation,当然也欢迎大家补充和更新。
- 最小权限原则(least privilege原则),最小权限原则大家应该能够耳熟能详了。具体来说就是给应用程序或者用户最小的能够满足他们完成任务的权限集合。这个原则很好理解,笔者就不多费口舌了,但是这个原则有一个很重要的变种,就是最小数据原则。在API设计中非常常用,笔者在某能源行业的项目上,对设备资源管理中心设计的API进行评审中,这是一个基本的原则需要设计人员遵守。最小数据原则就是接口不应该返回请求者未请求的数据字段。很多开发人员偷懒,接口返回全量的业务数据,乍一看好像没有什么问题,但是如果数据库中增加了一个包含敏感信息的字段,这个字段会被作为json数据返回给客户端,会造成数据泄漏。因为可能都没有人会考虑到要修改接口。
- 安全防御需要纵深,体系化。笔者在这两篇文章说介绍过很多安全风险以及预防的措施,读者应该能看到这些安全手段其实作用于不同的组件,和层级。因此我们在考虑自己项目上的安全方案的时候,一定要考虑多级防御体系,如果恶意攻击者突破了一层安全措施,多级的安全体系可以讲我们的损失降到最低,这是一种防御性编程和设计的思路。
- 缩减攻击面原则。架构设计和软件设计领域有一个通用的准则,复杂的系统更加容易受到攻击。这句话如何理解呢?首先我们的很多架构设计原则,系统设计原则,方法论(比如DDD),核心其实就是为了降低复杂性。而复杂的系统涉及的组件多,组件多了攻击面就大,因此我们说复杂的系统更加容易受到攻击,缓解的手段主要包括:a,减少系统对外暴露的接口,缩减受攻击的窗口大小;b,减少访问系统的用户数量;c,降级代码的复杂度
- 控制爆炸半径原则。控制爆炸半径要解决的是当系统被攻破后,通过控制手段来尽量的降低损失。容器是这个理念的体现,我们将微服务的实例部署到容器中,容器就充当了微服务的安全边界。
- 职责划分原则。不同的用户和不同的外部系统使用不同的账号和服务账号来访问资源,这种方式可以做到更加细粒度的权限控制,整个系统的安全性会得以提升。
读到这里,大家可能会问,这些原则具体改如何落地呢?笔者会在后续的文章中详细介绍这些原则如何在容器化部署的场景下具体落地,几天我们先来简单介绍一下:
- 最小权限原则下,我们可以给容器设置最小的外部资源访问权限,或者根本不给权限,来降级逃逸攻击的风险。
- 安全防御需要纵深原则下,容器作为最小的安全边界,我们可以通过各种手段来加强。
- 缩减攻击面原则下,我们将应用程序拆分成粒度合适的微服务,每个微服务都可以设置自己的安全措施,整体上来看,整个应用的安全性提升了。
- 控制爆炸半径原则下,由于容器化部署下容器实例是最小的安全边界,因此我们通过在容器上加固安全控制,来确保某个容器被攻破后,不会影响整个平台的安全和稳定。
- 职责划分原则下,证书只挂载到哪些需要使用安全凭证的系统,并且我们可以使用初始容器这样的技术,来把安全证书的访问限制在一定的时间和空间内,降低安全信息被泄漏的风险。另外可以考虑使用服务网格,把安全相关的配置和管理交给代理容器来完成。
虽然说这些原则听过起来都很振奋人心,但是如笔者在多个场合说的,这些原则描述的都是结果,能不能拿到这个结果,取决于我们是否对代码和镜像进行了安全扫描,并解决了高危风险?;是否对系统做了安全配置?;是否进行了权限控制?;是否对返回的数据做了过滤,不返回客户端没有请求的字段?等等,因此笔者会在或许的几篇文章中,着重介绍how的问题,希望大家能持续关注!