今天这一讲我想和你聊一聊云上的访问控制。

很多刚开始开发 Serverless 应用的同学都会遇到权限问题,比如没有权限发布函数、函数没有权限访问其他云服务。我也看到身边有很多小伙伴直接使用具有 AdministratorAccess 权限的访问凭证(即 AK,包括 AccessKey ID 和 AccessKey Secret)去部署应用甚至管理云资源。这是非常不安全的,一旦 AK 泄漏后果非常严重,因为 AdministratorAccess 权限的 AK 可对你的账户进行无限制地访问,比如我就见过某企业开发者不小心将 AK 上传到 Github 导致企业内部数据泄漏。

此外,当企业规模逐渐变大,企业中有不同角色的成员(开发、运维、财务……),为了云上资源的安全性,你就需要为不同角色配置不同权限,限制不同成员能够访问的云资源,可很多同学不清楚应该如何进行操作。

遇到这些问题的根本原因,主要是很多同学不了解云上访问控制,所以我准备了今天的内容,希望通过这一讲,你能弄清楚访问控制的工作原理,这样不管你是开发 Serverless 应用还是别的应用,都不用担心云上的权限问题。

对于访问控制,各个云厂商都有相应的产品,比如 AWS 的 IAM(AWS Identity and Access Management)、阿里云的 RAM(Resource Access Management)……不同云厂商的实现细节可能有所差异,但工作原理基本一致,因为这个工作原理是众多云厂商针对企业上云过程中总结出来的实践经验。

为了方便你理解,我以阿里云的访问控制功能进行演示,不过不用担心,各个云厂商的访问控制实现很类似,所以当你懂得怎么使用阿里云的访问控制后,也能很轻易地学会怎么使用其他云厂商中的访问控制。

访问控制的工作原理

在学习访问控制的工作原理之前,咱们先假设一个场景:如果你是一个云产品的架构师,那你要怎么设计一个访问控制系统,实现这样几个很常见的需求呢?

分权

如何使不同成员拥有不同的权限?比如运维同学才能购买云产品、Serverless 开发同学只能使用 Serverless 产品而不能购买、财务同学只能使用费用中心查看账单等(这在大型团队或企业中是很常见的需求)。

云服务授权

如何使云服务能够访问某个云资源?比如只允许函数计算读对象存储中的文件,而不能删除或创建。如果没有这个能力,只要某个用户能够编写一个函数,就能通过函数中的代码删除对象存储中的任意文件,这是非常危险的。

跨账号授权

如何使其他账号能够访问你的云资源?比如某个大型企业有两个云账号,其中一个云账号 A 是用来开发生产的,另一个 B 用于审计,存储所有日志。那么 A 如何使用 B 里面的日志?

以上的分权、授权等其实是企业上云过程中的非常关注的问题,即访问控制,而各个云厂商已经总结出了很完善的实践方案:通过子账号、角色和权限策略来实现云上的访问控制。

访问控制实现原理

当你创建一个云账号时(比如阿里云账号、AWS 账号),你的账号就是主账号,主账号具有所有权限,而你可以使用主账号来创建子账号和角色。

而子账号一开始创建时是没有任何权限的,你可以通过给子账号添加“权限策略”来为子账号授权,权限策略就是一组访问权限的集合。下面是一些权限策略示例。

AdministratorAccess: 管理所有云资源的权限。

AliyunOSSFullAccess: 管理对象存储 OSS 的权限,包括对 OSS 存储桶及文件的增删改查等所有操作。

AliyunOSSReadOnlyAccess: 只读访问对象存储 OSS 的权限,只能读取 OSS 的存储桶及文件,不能创建或修改。

基于子账号和权限策略,你就可以为团队中不同成员分别创建一个子账号,然后授予不同的权限,**这样就达到了分权的目的。**子账号创建完成后,有两种使用方式:

控制台访问,就是通过子账号登录控制台管理云资源;

编程访问,就是在代码中使用子账号的 AK 来调用云产品的 API,进而管理云资源。

当我们资源数量越来越多时,通常会通过编程的方式来使用和管理云资源。最直观的例子,当 Serverless 应用变得复杂,一个应用包含大量函数时,我们通常都会使用开发框架去创建、更新、发布函数,而开发框架本质上就是通过编程的方式来管理函数。

我在开篇提到,很多同学会使用具有 AdministratorAccess 权限的 AK 来部署函数,为了图方便就直接给子账号授予了 AdministratorAccess 权限,这样子账号的 AK 就能够对任意云资源进行任意操作。甚至有的同学都没有使用子账号,直接使用了主账号 AK。使用具有 AdministratorAccess 权限的 AK 是非常不安全的。所以当你使用 fun 或 Serverless Framework 等工具去部署函数时,一定记得要使用子账号的 AK,且需要为子账号设置最小化的权限。

除了子账号之外,访问控制中还有一个重要的功能是角色。

角色和子账号区别是,角色是一个虚拟用户,必须被某个具体用户(子账号、云服务等)扮演使用。角色创建后默认也是没有权限的,你可以通过添加权限策略为角色授权。同时创建角色时,需要指定角色能够被谁扮演,即角色的可信实体。角色的可信实体包括云账号、云服务以及其他身份提供商等。

如图所示,要使账号 A 能够访问账号 B 的 OSS,你就可以先为账号 B 创建一个角色 RoleReadOSSAccess,然后将角色的可信实体设置为账号 A,这样 A 就可以通过自己的 AK 来扮演账号 B 的 RoleReadOSSAccess 角色,进而读取账号 B 的 OSS。

角色扮演

基于角色扮演的方式,你就可以实现云服务授权和跨账号授权了。

刚刚我提到通过权限策略给用户或角色授权,什么怎么进行呢?从形式上来看,权限策略就是一个有特定语法的 JSON 字符串,你可以通过配置 JSON 字符串来实现授权。权限策略分为两种,系统策略和自定义策略。系统策略是云厂商内置的、预先定义的 JSON 配置,通常包含 AdministratorAccess 以及各个云服务的完全访问(FullAccess) 和只读( ReadOnlyAccess) 权限。但有时候系统权限可能无法满足你的需求,或者你想要在一个策略里面包含访问多个云服务的权限,你就可以使用自定义权限策略。

不同云厂商的权限策略语法几乎是一样的,比如阿里云 AdministratorAccess 的权限策略如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
    "Statement": [
        {
            "Action": "*",
            "Effect": "Allow",
            "Resource": "*"
        }
    ],
    "Version": "1"
}

其中 Action 表示可以进行的操作,直白点说就是能访问哪些 API,* 表示可以进行所有操作,也就是可以访问所有 API。Effect 有两个值,Allow 表示允许,Deny 表示拒绝。Resource 表示能够操作的资源,格式为 acs: :::  ,* 表示可以操作所有资源。

再举个例子,只读 OSS 的系统权限策略 AliyunOSSReadOnlyAccess 配置如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
    "Statement": [
        {
            "Action": [
                "oss:Get*",
                "oss:List*"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ],
    "Version": "1"
}

其中  oss:Get*表示只允许访问 OSS 中以 Get 开头的 API。

以上就是主账号、子账号、角色、权限策略等访问控制的相关基本知识点了,当然,讲了这么多理论知识,你可能还对访问控制还比较模糊,接下来我就以几个实际场景为例,带你将理论转化为实践。

访问控制的场景案例

使用最小权限的子账号部署函数

“使用最小权限的子账号部署函数”这个例子最简单,相信你在之前的课程中,已经使用了 AK 来部署函数,但你的 AK 可能不是最小权限的。为什么要限制权限呢? 如果你的团队内有多个同学都在开发 Serverless 应用,如果给开发同学的 AK 权限过大,开发者不小心的操作就很可能会对其他正在运行的云服务造成验证影响,比如不小心删除了数据库。如果限制了开发同学不能删库,就能避免这个问题了。

所以我建议你专门为 Serverless 应用开发创建一个子账号,该账号就只有函数计算的 FullAccess,并且该账号只运行编程访问。创建后你就可以得到该账号的 AccessKey ID 和 AccessKey Secret。

接下来我们需要对该账号进行授权。由于需要发布和更新函数,最简单的方式需要添加 AliyunFCFullAccess(管理函数计算服务的权限),这是一个系统策略。当然,更安全一点,我们可能只想让这个 AK 管理某个已存在的服务及服务下的函数,这时就可以使用自定义策略了。假设服务是 serverless-app,则自定义权限策略配置如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "fc:GetService",
                "fc:UpdateService"
            ],
            "Resource": [
                "acs:fc:cn-beijing:*:services/serverless-app"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "fc:*"
            ],
            "Resource": [
                "acs:fc:cn-beijing:*:services/serverless-app/functions/*"
            ]
        }
    ],
    "Version": "1"
}

在这个权限策略中,我们定义了两个策略:

允许对 serverless-app 这个服务进行查询和更新;

允许对 serverless-app下的函数进行所有操作。

然后使用该策略为 Serverless 开发账号授权,这样使用该账号 AK 的开发同学,就只能对 serverless-app 这个服务进行操作了。

当然实际情况可能会复杂一些,主要是有些开发框架如 Fun、Serverless Framework 等,它们允许你通过 YAML 去配置其他云服务,比如日志服务、表格存储,并且在你部署时会自动帮你创建或更新这些服务,这时就需要 Serverless 开发账号也具有这些云服务的权限。所以接下来看一下第二个场景,如何使用日志服务存储函数的日志。

使用日志服务存储函数的日志

这个问题本质上是需要让函数能够访问日志服务,也就是前面提到的 “云服务授权”。

使用日志服务存储函数日志

首先我们需要创建一个角色 RoleForServerlessApp,角色可信实体为阿里云服务,受信服务选择函数计算,这样函数计算就可以扮演 RoleForServerlessApp 这个角色了。

创建角色

接下来还需要给 RoleForServerlessApp 这个角色添加 AliyunLogFullAccess 权限,这样扮演 RoleForServerlessApp 这个角色的用户就能够访问你的日志服务了。

下面是 RoleForServerlessApp 角色的截图,其中 ARN(Aliyun Resource Name) 是角色的唯一标志,角色扮演的时候就会使用到 ARN。

角色创建完成后,我们就可以为函数计算的 serverless-app 服务设置 RoleForServerlessApp 这个角色,这样服务下的所有函数都可以通过扮演 serverless-app 角色来写日志了。那角色到底是怎么扮演的呢?别急,让我们来看第三个场景。

在 A 账号的函数中访问 B 账号的 OSS 文件

通过前面的学习,你已经知道了可以通过角色扮演来实现第三个场景(这种场景常见于有多个账号的大型企业中,了解该场景的实现,可以让你跟深入理解角色扮演。)那具体怎么实现呢?这个过程可能要比你想象的要复杂,但了解其原理后可以帮你更深入理解访问控制。

首先你要为账号 A 中函数计算的角色授予角色扮演的权限,也就是需要为前面的 RoleForServerlessApp 再增加一个 AliyunSTSAssumeRoleAccess 的权限,这样函数实例才能进行角色扮演。

然后你需要在账号 B 中创建一个 RoleForAccountA 的角色,角色可信实体是账号 A,其策略是 OSS 的读权限,这样账号 A 就可以通过扮演 RoleForAccountA 这个角色来读取账号 B 的 OSS。

账号 A 的角色 RoleForServerlessApp

在账号 B 中创建角色

角色和权限都配置完成后,让我们来看看实际的工作流程:

授权账号 A 访问账号 B 的实际流程大致可以分为下面几个步骤:

函数计算扮演 RoleForServerlessApp;

扮演角色后得到临时访问凭证 token1(这里的临时访问凭证本质上是一个临时 AK,包含 AccessKeyId、AccessKeyIdSecret、SecurityToken,并且有过期时间);

将 token1 注入函数上下文 context 中,这样在函数里面,你可以通过 context.Credentials 属性拿到临时访问凭证,并且函数计算会使用 token1 来执行函数,也就是说,真正执行函数的是角色是 RoleForServerlessApp;

在函数实例中,扮演账号 B 的角色;

通过角色扮演得到账号 B 的 RoleForAccountA 角色的临时访问凭证 token2

使用 token2 访问账号 B 的 OSS。

具体代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
const Core = require('@alicloud/pop-core');
const oss = require('ali-oss');

/**
 * 角色扮演
 * @param {string} accessKeyId 
 * @param {string} accessKeySecret 
 * @param {string} securityToken 
 */
async function assumeRole(accessKeyId, accessKeySecret, securityToken) {
    // 构建一个阿里云客户端, 用于发起请求
    const client = new Core({
        accessKeyId,
        accessKeySecret,
        securityToken,
        endpoint: 'https://sts.aliyuncs.com',
        apiVersion: '2015-04-01'
    });
    //设置参数
    const params = {
        "RegionId": "cn-hangzhou",
        // 需要扮演的账号 B 的角色
        "RoleArn": "acs:ram::1676314257720940:role/roleforaccounta",
        "RoleSessionName": "TestAssumeRole"
    }
    const requestOption = {
        method: 'POST'
    };
    // 发起角色扮演请求
    const result = await client.request('AssumeRole', params, requestOption);
    // 返回角色扮演后的临时访问凭证
    return {
        accessKeyId: result.Credentials.AccessKeyId,
        accessKeySecret: result.Credentials.AccessKeySecret,
        securityToken: result.Credentials.SecurityToken,
    };
}

/**
 * 获取账号 B 中的 OSS 文件
 * @param {string} accessKeyId 
 * @param {string} accessKeySecret 
 * @param {string} securityToken 
 */
async function getObject(accessKeyId, accessKeySecret, securityToken) {
    // 构建 OSS 客户端
    const store = oss({
        accessKeyId,
        accessKeySecret,
        stsToken: securityToken,
        bucket: 'role-test',
        region: 'oss-cn-beijing'
    });
    // 获取文件
    const result = await store.get('hello.txt');
    return result.content.toString();
}

module.exports.handler = function (event, context, callback) {
    // 获取函数计算的临时访问凭证
    const accessKeyId = context.credentials.accessKeyId;
    const accessKeySecret = context.credentials.accessKeySecret;
    const securityToken = context.credentials.securityToken;
    assumeRole(accessKeyId, accessKeySecret, securityToken)
        .then(res => getObject(res.accessKeyId, res.accessKeySecret, res.securityToken))
        .then(data => {
            console.log('data: ', data);
            callback(null, data);
        })
        .catch(error => console.log(error))
        ;
};

虽然过程比较复杂,但代码中其实就两个地方需要注意:

要从上下文 context 中获取临时访问凭证 token1;

需要根据 token1 再去扮演账号 B 的角色,得到账号 B 的临时访问凭证 token2,最后才能用 token2 去访问账号 B 的服务。

总结

这一讲我主要为你介绍了云上访问控制的基本工作原理,以及在实际开发中的场景案例。当你一个人开发时,或团队很小时,对云资源的安全管理要求可能不高。

但当团队逐渐发展壮大,组织结构越来越复杂,对云上资源的身份管理、安全管控要求就会越来越高,需要一套完整的访问控制体系。所以希望通过这一讲的学习,你对云上的访问控制有更多的了解,并在今后的开发工作中灵活地运用。关于这一讲我想强调这样几个重点:

云厂商主要通过主账号、角色、权限策略等方式来实现云上资源的访问控制;

通过访问控制,我们能实现分权、云服务授权、跨账号授权等云上资源管控需求;

实际工作中,对于用户访问权限要遵循最小授权原则。

最后,关于访问控制其实还有很多知识点本节课没有涉及,比如用户组、单点登录、OAuth 等,这些主要是对云上访问控制的补充,就留给课下你再去研究研究吧。我们下一讲见。

-– ### 精选评论