本次直播视频精彩回顾,戳这里!
直播涉及到的PPT,戳这里!
演讲嘉宾简介:
朱一聪(花名:一聪),2017年6月加入阿里云MongoDB组,目前主要参与阿里云MongoDB云数据库内核相关的开发。
以下内容根据演讲嘉宾视频分享以及PPT整理而成。
本次的分享主要分为以下三个部分:
一、阿里云网络安全策略
二、鉴权
三、Role-Based Access Control
回顾2017年,发生了一个比较著名的事件--MongoDB赎金事件,也暴露出MongoDB存在很大的安全问题。事件的截图如下图所示,可以看到,黑客非常嘲讽地在用户的数据库内留言并索要赎金。
这个事件主要归责自然是行径恶劣的黑客,但实际上黑客攻击的手段并不高明,他们只是扫描了在互联网公网上能访问到的MongoDB,并且仅仅扫描了MongoDB默认的端口—27017,然后就直接连进了目标MongoDB。同时他们发现这并不是个例,有很多MongoDB都可以通过这种方式连上。
从客观上来说,用户和社区自己也负有一定责任:首先,把一个数据库直接开放到公网上是非常不安全的,这就相当于打开家门让大家进来;其次,还使用着默认的端口;最要命的是还没有开启鉴权。MongoDB官方默认不开启鉴权的设定确实不太合理,但它也有一些历史的原因的考虑。用户在自己使用的时候,建议是一定要开启鉴权。
一、阿里云网络安全策略
对于阿里云的MongoDB的用户来说,不存在上述的这些问题:首先,阿里云的MongoDB就是默认开启鉴权的。其次,阿里云的MongoDB本身在阿里云的网络上会带有以下安全的措施:
1、默认公网无法访问。就算有人申请了可公网访问的ip,它也有白名单机制的保护,即它只接受你指定过的来自于ECS的访问。
2、阿里云现在大部分产品是支持VPC的,这就代表即使是在阿里云内网跟其他网络是隔离的,用户处于自己的专有虚拟网络下。 所以在网络上阿里云的MongoDB是相对比较安全的。
云MongoDB的一些安全措施主要包含四个方面:
1、鉴权[Authentication]
2、授权[Authorization]
3、审计日志
4、TLS/SSL
审计日志其实是云MongoDB才提供的一个功能。对于MongoDB来说,它的社区版是不提供这个功能的,只有企业版才提供这个功能。从安全性来讲,这个功能的好处在于,万一真的有人恶意的操作了数据库,例如黑客通过一些间接手段直接把用户的用户名、密码给盗取了,那黑客的异常操作和访问是会有记录的,这就相当于他犯罪留下了证据,让用户日后也有迹可查。
TLS/SSL的是指在连接数据库的时候不是使用明文而是进行加密处理。这个能力MongoDB社区版是支持的,企业版在这个方面更强,云MongoDB由于一些原因暂时还没有开放,但未来也可能支持这个功能。
另外两块是鉴权和授权。它们的英文单词在拼写上非常接近,但实际上却是两个完全不同的意思。通俗一点来说,鉴权可以认为是识别[你的客户端]。在MongoDB中鉴权是以user为单位的,鉴权的真正含义是指鉴定这个user有没有资格、或者说认不认可这个user来连上服务器。而授权的含义是指定义这个user用户可以在服务器上做哪些动作。
二、鉴权
MongoDB的鉴权主要分为两种类型,一种是外部鉴权,就是用户以客户端访问mongodb时的鉴权。还有一种是内部鉴权,是指replSet的节点之间的相互鉴权和sharding模式下,mongos和shard节点之间鉴权。对于云MongoDB来说,它的外部鉴权和内部鉴权都是默认开启的。
鉴权方式也主要有两种。一种是在连接客户端的时候,添加一个参数用于开启鉴权,把用户名、密码代入进去。另一种是在连接上之后,调用db.auth()方法进行鉴权。
在云MongoDB上两种都支持,用法也十分简单。有一点需要注意的是鉴权这个行为是对于一个db的某个用户而言的,所以大家一定要注意db和user的匹配关系。在现实案例中,很多用户之所以无法访问数据库,往往是因为用户的用户名和用户指定的数据库不匹配导致的。比如下图中MongoDB默认给用户的超级权限用户root,其对应的数据库是admin,只有两者匹配才能成功访问,这个需要大家注意一下。
接下来看看MongoDB的鉴权策略,前面提到过,MongoDB是支持多种鉴权方式的,目前推荐的一种方式是SCRAM-SHA-1,并且在云MongoDB上也是采用的这种方式。以往一种可行的方式是直接把用户名、密码(无论是明文还是加密过的hash值)放在客户端进行比较,如果跟客户端保存的那个密码一致,即认为检验通过。相对于这种方法,SCRAM-SHA-1的方式更加先进。
SCRAM-SHA-1是一个双向鉴定机制,不仅客户端在访问服务端的时候,服务端要鉴定客户端是否有资格进行连接,而且客户端本身也要鉴定所连接的服务端是不是正确的。相对于单单只是建立客户端的鉴权,这样的双向鉴权话就比较安全。如果不使用双向鉴权,客户端连上了一个不该连上的Server进行操作,并且Server记录了客户端的操作,会形成带有风险性的行为。
这种鉴权方式的实现比较简单。与传统的方式一样,Server端需要保留一些内容,但它不像传统的方式把密码给保存下来,而是针对密码做一次加盐哈希,即加一个salt和iteration-count做一个哈希得到password[s],然后通过password[s]得到两个key,一个是key[c],另一个是key[s],它们是password[s]直接通过HMAC和”Client Key”和”Server Key”进行哈希得到的,服务端只保存了用户名、H(key[c])、 key[s]、 salt和iteration-count,没有保存真正的password,甚至它没有保留key[c],这也是出于安全考虑。
SCRAM-SHA-1的具体流程是这样的:
1、客户端先发送用户名和一个client-nonce(即客户端生成的随机数)。
2、然后服务端返回salt和iteration-count,并生成一个server-nonce,与client-nonce做一个异或返回给客户端。
3、客户端根据这些信息进行计算得到Auth。Auth就是根据服务端返回的结果算出来的,这时候关键操作是它需要证明自己是客户端,具体的和服务端的准备工作一样根据password计算得到key[c],然后key[c]跟Auth使用HMAC再做一次hash,最后再跟key[c]做一次异或,得到_proof[c],粗略来说就是根据password和步骤2中服务端返回的信息计算得到proof[c]。
4、客户端返回proof[c]和client-nonce|server-nonce。
5、服务端对_proof[c]是否合法进行验证,粗略来说就是通过一些计算将_proof[c]还原出key[c],然后校检H(key[c])和本地保存的值,如果它们一样,则客户端鉴权成功。
6、由于是双向鉴权,服务端也要进行鉴权,它使用步骤3的方法计算出_proof[s]并返回给客户端。
7、客户端也用和服务端校检类似的方式对_proof[s]进行验证。
可以看到,整个鉴权流程还是比较冗长,而每次建立一个新的连接都要走一次这样的流程。而且在服务端鉴权的时候,需要生成随机数做一些计算,这些计算本身耗费的cpu不是太大,但MongoDB实现里面随机数是通过系统调用去取的(通过访问Linux的dev/urandom来获取随机数),系统调用的开销就不小了;此外为了使得系统调用取到的随机数不重复,这个系统调用还处在一个spinlock的保护下。这样当用户使用大量并发的短链接去连接的时候,所有链接都要做并发的鉴权,而鉴权会去进行系统调用,系统调用还加锁,性能上就会产生问题。所以在短链接的情况下,社区版MongoDB的性能并不好,而瓶颈就在于鉴权环节上。这个问题可以通过改用长连接来解决,实际上MongoDB官方也是推荐大家使用长链接的,因为长链接相对于短链接来说省去了反复链接和鉴权的开销,然而对于某些高级语言(比如PHP)来说,它没有办法只能够使用短链接,或者总有一些场景短连接更适合。而对于云MongoDB来说,短链接是可以放心食用的,因为云MongoDB的内核做了一个优化,它不再通过系统调用去获取Linux的默认的随机数,而是做了一个用户态随机数,这样就把鉴权的瓶颈给取消掉了,所以短链接的性能也非常好。这个问题在云MongoDB也就不存在了。
三、Role-Based Access Control
MongoDB的权限管理,顾名思义,是根据role这个角色来管理的。意思是说,在db下面创建了一个user(创建user需要指定一个db,实际上是通过db.createUser()这个方法创建,这个时候的user即为当前db下创建的),一个user有多个role,一个role又定义了多组权限(权限是指有权对一个resource做某个action操作),因此用户就可以通过组合多个role的方式来得到多种权限的组合,MongoDB还支持role的继承,总的来说role-based access control的是一种非常灵活、强大的方式。
首先讲解一些基本概念,位于最底部的Privileges层涉及到两个概念:Resource和Action。Resource其实就是指一组资源,具体分为三种:collection resource、database resource和cluster resource。如下图中的{
db:”products”, collection:”inventory” }是指数据库products下的名为inventory集合的资源。当然resource也可以不用这么细致,也可以把一个database当成一个resource,即如下图中的{
db:”test”, collection:”” }把collection处置为空,表示对于所有的collection都有权限。还有一种cluster resource,如下图中的{ resource:{ cluster:true } actions:{ “shut down” } }表示当前role拥有向集群发送shut down的action的权限。
Action其实也是一个封装过的概念,对于MongoDB来说,它的命令的基本单位是command,比如在shell上无论我们使用db.runCommand()的方式还是直接调用db.insert()这样的方法,最终在server端转都是赚化为command来执行。Action其实是一类相似的command的组合,比如以insert这个action为例,它其实是代表了insert和create这两个command,所以如果对一个db有insert的权限(这个insert是action),或者说一个insert权限的source是某个db,这就意味着用户拥有对数据库insert和create这两个command的权限。
MongoDB的role是支持用户自定义的,功能非常强大。可以通过createRole()方法指定任意的role,如下图中的例子:
自定义一个role,实际上就是定义一组权限。可以看到上图中有一个字段privileges,它是一个数组,里面包含着若干自定义的权限,每个权限内又具体细分为resource和actions两部分。下方还有一个字段roles,它也是一个数组,里面包含了一个名为read的role,意思是自定义的role继承了名为read的role,这里的继承和面向对象中的继承非常类似,即表示当前名为myClusterwideAdmin的role继承了名为read的role的所有权限。
自定义的方式功能上非常强大的,用户几乎可以根据自己的喜好定义任意一种role,即对任何资源的任意操作的任意组合。但是有的时候,用户不想麻烦地自己进行定义,因此MongoDB也提供了一些内置的role。这样用户就可以通过组合这些内置的role或者直接使用这些内置的role达到自己权限管理的目的。
如下图所示,这些内置的role也分为好几类,比如Database User Roles和Database Administration Roles。大家都知道在MongoDB里面数据库分为两种,一种是Admin数据库,这个数据库是在MongoDB Server启动时就存在的,无需自己创建。还有一种是用户在使用时,自己创建的数据库。特别值得注意的是内置role中有一类Superuser Roles,它实质上就是root,即用户拥有对所有资源的所有权限,在理论上这类用户可以干任何他想干的事情。
在MongoDB中,user是在database下创建的,在鉴权的时候也需要指定当前user属于哪个database。虽然user是在database下创建的,但是它的权限可以跨越它所属的database。如下图创建user的例子:这里使用的是products这个database,但是我们在给这个user指定roles的时候,指定的两个role的权限都是作用在数据库admin下,这种写法在MongoDB里面是允许的,这样写也比较灵活。实际上可以在创建一个用户的时候给它指定任意的role,也就是说可以把对其它数据库的权限通过role赋予给它。
MongoDB还提供了一组管理接口,可以让用户可以很方便地管理user、管理role,之前介绍的最简单、最基础的两个db.createUser()和db.createRole()方法就在这组接口中。除此之外,如果想创建完一个用户之后更改这个用户的role,即通过更改用户的role来更改用户的权限,那么可以调用db.updateUser()的方法来进行重置;如果想进行查看的话,也可以调用db.revokeRolesFromUser()。这里就不进行一一介绍了。对于用户的一些修改操作和对权限的一些操作,它都封装了相应的方法以方便用户。
我们知道在MongoDB的数据库里,数据包含三个基础概念:database、collection和document。在进行权限管理的时候,相关数据的存储其实跟普通数据的存储没有什么差别,MongoDB几乎把它所有的信息数据,包括它内部的信息、用户自定义或自己插入的一些数据,都是以document的模式放在某个db的collection里面,只不过像role、user这些信息,它是放在admin数据库下,即便role和user是创建在非admin数据库区,它们的信息也是统一集中存储在admin这个数据库下admin.system.role或admin.system.user这两个collection里面。从下图的储存实例中可以看到,system.role这个collection下存放的一个document展开项跟使用createRole()这个function时传入的document一模一样,其实在使用createRole()时,系统就是往system.role的collection里面直接插入一个document,所以才会导致它们内容一模一样。
User信息的存储也是类似的,在使用createUser()时,系统也是往system.user的collection里面直接插入了一个document。
虽然从原理上,大家可以发现,直接对system.role和system.user两个collection进行curd操作也可以进行权限管理,但是强烈不推荐这么做,强烈推荐使用上面介绍的像createUser()这样的权限管理的接口。因为mongodb的document事物schema的,是自由的,可以是任意的格式,这是它的特色,那么好了假如system.role插入了一个奇怪的文档怎么办,如果修改某个role的document的时候字段由于失误写错了怎么办。Curd操作是不会对这些做校检的,但是调用接口就会。使用mongodb提供的接口管理权限更方便也更不容易出错。