平台软件应该像数学一样严谨 --- 和阿里云RAM团队商榷
长话短说
阿里云程序员在构建 RAM(资源访问控制)这种基础服务的时候,使用的集合操作符过于原始,使得其代码缺乏通用性,限制了客户的自动化。这是一个比较隐蔽的 bug,但实际上可以对客户造成很严重的开发和运维困难,所以我分享出来,也想借机和大家切磋:短平快的To C软件开发模式,究竟适不适用于云计算平台的开发?
问题背景
客户使用阿里云SMC服务迁移服务器到容器镜像的时候,需要提供一个RAM角色,这个角色必须拥有规定的几个权限。SMC服务的控制台会列举出满足条件的所有角色供用户选择。下面这个截图,就显示有且只有 second-smc-forwarder-push-image 这个角色满足要求。
如果你没有一个角色满足要求,则下拉菜单为空,界面如下:
具体的权限要求,SMC给出了很明确的代码,如下:
{
"Statement": [
{
"Effect": "Allow",
"Action": [
"cr:GetAuthorizationToken",
"cr:PushRepository",
"cr:PullRepository",
"cr:CreateRepository"
],
"Resource": "*"
}
],
"Version": "1"
}
// From https://www.alibabacloud.com/help/zh/server-migration-center/latest/migrate-source-servers-to-container-registry
请注意这段代码里的 Action(操作)部分,它们是SMC的中转服务器推送镜像所必需的几个权限,缺少任意一个都会导致推送失败。但是从 RAM 语义来说,它们不互相依赖,也就是说,这个列表是未排序的。
这四个操作不论怎么排序,效果都是等价的。打个比方,你求婚的时候和老婆承诺“结了婚我承包做饭,洗衣和刷碗”,我求婚的时候承诺“结了婚我承包刷碗,做饭和洗衣”,两个承诺在理论上是等价的。
下面我来一步步证明,阿里云和你我一样,并不尊重理论上的承诺。
我们会用SMC控制台这个功能作为测试用例,来测试 RAM 怎么诠释各种不同的 RAM 配置。
准备测试
首先在 RAM 服务控台创建一个角色 second-smc-forwarder-push-image ,然后创建一个 RAM 策略 container-push-permissions,并且关联起两者。
然后添加策略内容,把上面的代码原版拷贝进去,保存。
再回到 SMC 服务的控制台创建一个服务器到容器的迁移任务,可以看到界面的下拉菜单确实显示了second-smc-forwarder-push-image,这说明该角色拥有文档所要求的权限。一切符合预期,这里没有什么惊喜。
不能重新排序操作(Action)
上面代码里的操作(Action)没有明显的排序,这样不利于代码维护,因此我决定将其按照字母表排序,得到如下代码,重新保存到策略里。
{
"Statement": [
{
"Effect": "Allow",
"Action": [
"cr:CreateRepository",
"cr:GetAuthorizationToken",
"cr:PullRepository",
"cr:PushRepository"
],
"Resource": "*"
}
],
"Version": "1"
}
这时候再回到SMC服务的控制台,砰!没有符合条件的角色了!这意味着,阿里云认为排序后的操作列表和排序前的操作列表不等价。
也不可以分拆为两个授权语句(Statement)
我的所有资源访问控制策略, 都是作为代码管理的。有时候,我会用两个不同的模块分别组装出两个授权语句,然后再把两个授权语句组装出一个完整的策略,得到代码如下。从RAM的语义来说,这个策略代码和阿里云文档给我的策略代码,是等价的。
{
"Statement": [
{
"Effect": "Allow",
"Action": [
"cr:GetAuthorizationToken",
"cr:PushRepository"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"cr:PullRepository",
"cr:CreateRepository"
],
"Resource": "*"
}
],
"Version": "1"
}
但是,结果令人遗憾,绑定这个策略的角色不会显示在SMC的控制台下拉菜单中,说明阿里云RAM并不认为这两个策略等价。
不能添加权限(Action)
由于角色管理太繁琐,我有时候会在多处复用同一个角色,该角色绑定多处权限的并集,得到代码如下:
{
"Statement": [
{
"Effect": "Allow",
"Action": [
"cr:GetAuthorizationToken",
"cr:PushRepository",
"cr:PullRepository",
"cr:CreateRepository",
"cr:ListRepository"
],
"Resource": "*"
}
],
"Version": "1"
}
// 或者
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"cr:GetAuthorizationToken",
"cr:PushRepository",
"cr:PullRepository",
"cr:CreateRepository"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "cr:ListRepository",
"Resource": "*"
}
]
}
// 这个新的cr:ListRepository是另外一个任务需要的权限,被我合并在这个策略了。
于是这个新策略的权限列表,是之前策略的权限列表的超集(Super Set)。从 RAM 的语义上来说,这个角色应该是符合资格的。但是,砰!不行!这个角色也没有在 SMC 控制台显示!
加一个策略(Policy),却又没问题
但是我实在是不想专门创建一个只有四个操作权限的角色,这个角色适用范围太窄了,会没有必要的增加我的管理成本。
于是,我再创建了一个专门的策略 ListRepository,把它绑定到 second-smv-forwarder-push-image 这个角色去。RAM 语义上,它和上面的策略超集也是等价的。那么既然上面的超集不符合资格,那么这个超集也不应该符合资格吧?
然而实际上,这个超集通过了检测,它显示在 SMC 控制台的下拉菜单中了。
实际上,我还可以进一步,再给这个角色添加一个容器镜像服务的管理权限,使得这个超集更大。它仍然会通过 RAM 的检测。
通过检测的角色都会像这样展示在SMC控制台的下拉菜单
诡异的逻辑
上面几个实验的结果,让人非常困惑。如果我老婆采用阿里云RAM一样的逻辑,画风大概是这样的:
老婆:你要承诺做饭,刷碗和洗衣,我才嫁给你!
我:好呢,我承诺我洗衣,刷碗和做饭!
老婆:不行,顺序不对!
我:好呢,我承诺我做饭,刷碗,洗衣和拖地!
老婆:不行,你多了一个承诺!
我:我做饭,刷碗和洗衣!
老婆:你通过了,不过你还可以把拖地加进去。
我:可是你刚才说不能承诺超集呀?
老婆:你拆成两次承诺就可以。
我:我承诺做饭,刷碗和洗衣;我承诺拖地!
老婆:这样就可以,你通过了!
我(问自己):这里的逻辑是什么?以后要怎么沟通?
猜测
对于RAM有这么诡异逻辑的原因,我有一个猜想:
RAM 没有提供一个类似AWS policy simulator的策略验证工具。假设 SMC 服务要求客户提供的角色至少要满足策略 PaP_aPa,而客户角色 X 现在关联了策略 PbP_bPb ,没有一个工具能告诉 SMC 的开发者,PbP_bPb 是否满足 PaP_aPa 的要求。
SMC 开发者于是自己实现了一个策略验证逻辑。
这个逻辑很简朴:遍历角色 X 关联的所有策略 P0−Pn−1P_0 - P_{n-1}P0−Pn−1,看是否有至少一个策略等于 PaP_aPa。
这个 PaP_aPa 和 PiP_iPi 的比较也很简朴,很可能就是用 lodash.isEqual() 函数直接比较。
由于 RFC 7159 规定 JSON 的数组是有序的(这个和 RAM 的语义不一致),所以如果客户更改Action 的顺序,会导致两个数组不相等,导致 SMC 控制台得出结论:PbP_bPb不满足PaP_aPa。
同样,如果客户在PbP_bPb新增一个 Action,或者新增一个 Statement,都会导致两个对象PbP_bPb和PaP_aPa不相等。
由于 RFC 7159 规定对象的属性是无序的,所以如果你把上面代码的 Effect, Resource 和 Action重新排序的话,倒是不会有影响。
为什么把新增的 Action写成一个单独的策略关联到角色,又可以工作呢?因为每次轮询只比较一个策略。
如果你把 SMC 要求的四个 Action 分拆到两个策略,根据这个猜想的实现,也不会通过测试。有兴趣的朋友可以试一下。
资源访问控制是云的核心基础服务
资源访问控制是云计算最重要的基石之一。它被每一个云服务所依赖,也被每一个云用户所使用。如果设计不当,后续要修改的代价极为高昂,一个参考案例是,AWS S3 这些年一直给用户推荐 IAM,却永远摆脱不了它们最初给客户提供的 ACL legacy。
同时,访问控制本身也很复杂。RAM 的模型源于 AWS 的 IAM,主体侧包含用户/用户组/云服务/ Idp / ECS 上的进程/角色。权限策略包含效果,操作,资源,条件。控制台隐藏实现,但是在 Infra as Code 中需要用户显式申明的有主体和策略之间的 attachment。再考虑到跨账号访问,以及基于资源的策略,这个模型就非常容易让人困惑了。很可惜阿里云RAM既没有一个模型图,也没有一个Glossary 可以供用户做权威的参考。
实际上,阿里云RAM团队自己对这个模型也很迷糊。比如他们的《权限策略基本元素》文档里说:
RAM中使用权限策略描述授权的具体内容,权限策略由效果(Effect)、操作(Action)、资源(Resource)、条件(Condition)和授权主体(Principal)等基本元素组成。
这是错误的。实际上,权限策略由版本号和授权语句数组组成,授权语句才由上述的基本元素组成。如果客户使用 IaC 工具比如 terraform 组装策略,授权语句数组会是一个很关键的概念,不可以省略。
由于云计算资源都在互联网上,如果用户不能正确的配置资源访问控制,那么后果可能非常严重,超出云计算提供商的可挽救范围。最近的一个安全漏洞《微软38 TB内部数据惨遭泄露!私人密钥、3w+工作对话流出》,就是由于微软员工对Azure存储资源的访问控制配置不当导致的。
对于这么一个重要,复杂和敏感的云服务,我并不期望阿里云 RAM 很好用。实际上,AWS,Azure 和 GCP 的资源访问控制模型都不简单。但是我期望 RAM 一定要逻辑一致,不能因为代码的不完善实现,而对客户呈现自相矛盾的行为,使得已经很难掌握的 RAM 变得更不可预计。
逻辑不一致会导致不可预测的行为偏差
举例来说,如果我的角色关联的策略中的操作是重新排序的,那么 SMC 控制台就不会显示该角色作为备选角色,因为lodash.isEqual(原始操作数组,排序后的操作数组)返回的是False,这样我就无法继续创建 SMC 的迁移任务了。
但是如果我直接使用CLI或者直接调用 SMC 的创建迁移任务API,那么就会跳过这个函数调用,可以顺利的创建 SMC 的迁移任务。
IT团队最怕的是什么?就是这种无法预测,不可解释的意外行为。研发部的张三在深圳办公室白天操作就成功,测试部的李四在西安家里半夜操作就死活失败,整个团队研究了一个礼拜,排除了一个又一个的变量(部门利益,操作的网络,操作的时间,操作人的人品),最后发现用阿里云的控制台就失败,而用CLI就成功,阿里云的朋友们,你可以想象下他们肚子里的怒火会有多大!
自动化会放大这种不一致
比较深入的云计算用户,基本都不会用控制台直接操作生产环境,而是使用 git 管理 IaC 模版文件。这些 IaC 模版会在 CICD 流水线里被动态的填充变量,生成配置文件,最终交给terraform/cloudformation/cdk 来创建云资源。
在这个 pipeline 中,RAM的配置文件其实是一个中间产物。那么其中的操作数组可能在V3版本是原始顺序,而在V4+版本是字母表顺序。由于流水线不走控制台,因此两个版本都会顺利的通过所有集成测试。但是V4+之后的所有版本,在控制台都不可用了。然后,这时候,来了一个紧急故障,CFO等着CTO回复消息。客户工程师没法等流水线慢慢跑,想要上控制台直接修改 SMC 的角色配置,他发现下拉菜单没有了那个一直工作的角色了。。。
建议
建议RAM提供一个policy simulator工具,以库的形式提供给其他云服务开发者,以在线服务的形式提供给用户。
禁止其他服务自行实现策略验证/比较库,避免“一种策略,两种阐释”。
提供详细并且权威的 RAM Concepts 文档,避免团队内外对复杂概念产生理解误差。
总结
To C互联网服务的代码,实话实说,大部分质量不高。从业者的口头禅“业务驱动技术”翻译成普通话就是“代码能跑就行,赶紧上线,别讲究那么多”。但是换到了Infrastructure领域,逻辑可能不一样了。云计算产品并不服务于最终消费者,而是被用户(这些人也是开发者)集成到自己的代码中,再次构建出最终软件。因此代码的可复用性,一致性,安全性等质量要求,可能比特性更为重要了。当然,这只是我的一家之言,欢迎各位同仁来信讨论。