阿里云OSS+STS实现更安全的kintone扩容

cybozu发表于:2023年03月03日 09:46:52更新于:2023年08月07日 15:39:48

Index


概要

这篇文章的初衷主要来自客户的一个问题:
作为贸易公司,每天有大量的出货入货单据存储在kintone上。但是渐渐的容量不够用了,单单购买kintone容量也无法满足需求。
现在的想法是有什么其他的办法能起到扩容的效果?前提条件是:这些单据算得上是公司的商业机密,一定要保证安全性。

为此,我们做了以下解决方案的demo插件。

这次使用的是阿里云对象存储(Object Storage Service,简称 OSS),是阿里云对外提供的海量、安全、低成本、高可靠的云存储服务。
在本文中,我们会将kintone的附件存储到OSS中,并通过STS授权来访问。这样在增加存储容量之外,还能避免泄露机密信息,安全又可靠。


创建应用

代码基于 使用“Tencent Cos for Kintone” 插件实现腾讯云+kintone文件管理 修改而来。

请参考或重用该插件的表单设置,创建一个kintone应用。

0016405ab9c31959e2b4108e1173fcd


阿里云设置

OSS

开通OSS后我们可以通过“控制台”进入对象存储OSS设置画面。

点击"创建Bucket"后填写Bucket名称、并选择离你所在地近的地域。(请记住Bucket名称、地域)

001640188aa6af7496007e27a170c8e


由于代码中会使用OSS的SDK去访问阿里云API,因此我们需要开启Bucket的跨域设置。

在“历史访问路径”中找到我们创建的Bucket,在“数据安全”中找到“跨域设置”,创建一条跨域规则。

为求简单,我们的来源和headers都设置为*。

在Method中,由于我们涉及到列表、上传、更新、删除等操作,因此勾选对应的GET、POST、PUT、DELETE即可。

00164018ab7cc621f7f9329f66b4c60


STS

此时,如果使用我们主账户的AccessKeyId和AccessKeySecret,OSS SDK就可以直接通过API来操作该Bucket。

然而,这种操作会产生主账户信息泄露的风险。

一旦主账户信息泄露,对方就可以利用API用来使用其他阿里云的服务,而因此产生的费用则由主账户的所有者来承担。

为了解决这个问题,阿里云官方提供了 STS临时访问凭证方案

我们可以利用 “访问控制RAM”(Resource Access Management)来创建一个角色。

通过STS(Security Token Service)来为该角色临时授于OSS的操作权。

参考官方方案,我们需要创建一个仅仅只有“OpenAPI 调用访问”权限的用户。

00164018e7006b58acc871e957d56ef

0016401921ad35df41225ded353be72

 

点击复制,将该用户的AccessKey ID及AccessKey Secret保存到本地任意文件中。(将被使用在插件设置中)

请务必保存以上信息, Secret值只显示一次。如您遗失这个 AccessKey,可以创建新的来替代。

00164019246ba34dc1f8809ff96c39a


为刚刚创建的RAM用户授予AliyunSTSAssumeRoleAccess的系统策略。

001640194b435b1aa7eadc9300d7889

0016406e4946e488094e8920c300818

0016406e1ad6fbad96c39c649ed56b6


创建一个OSS专用的角色,用于获取临时访问凭证。

0016401959d88a219ab3e0a048586b3

0016406e2e42cb8802304423455ca1b


建立一个自定义的权限策略,并赋予其操作我们之前创建的Bucket的API操作权限。

001640196252e81cdb1881e804754c9


我们需要在Action下添加Object的Get、Delete、Put、List权限。

在Resource下填写自己的AccountID和Bucket名(例如本次创建的“kintone-oss-demo”)。

00164019684969e7bbf49f18dd56ed9


为了方便大家,这里提供文档模版,请修改对应的AccountID和Bucket名。

{
    "Version": "1",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "oss:GetObject",
                "oss:DeleteObject",
                "oss:PutObject",
                "oss:ListObjects"
            ],
            "Resource": [
                "acs:oss:*:your account id:your bucket",
                "acs:oss:*:your account id:your bucket/*"
            ]
        }
    ]
}


填写策略名称后保存。

001640197d4550e75228a5cf1e752b3


回到角色画面,找到我们之前的创建的角色。

0016401982aecf70074b9fe9eb508aa


为其添加我们之前定义的策略。

0016406e4ffe39bda3e641f9202e31b

0016406e64d2f58c125703f606ccb80


点击角色名,进入详细画面。

001640198ccc9c272ac57ae887e013d


复制ARN,并保存到本地文件。(将被使用于插件配置中)

001640199c7265120b041574767a530


插件准备

到这里,OSS和STS的设置完成了。下面就需要到kintone环境里设置插件。

Demo插件源码

可以通过下面的地址获取源码。

请按Readme的说明进行编译和安装。

源码地址

插件配置

插件配置OSS信息
OSS 身份识别IDAccessKey ID
OSS 身份密钥AccessKey Secret
OSS 存储桶名Bucket名
OSS 地域信息bucket中设置的地域的Region ID。可以参照 这里 获取对应信息
RoleARN


验证

配置完插件设置后,可以添加一条记录并上传文件。
00164085d2e0064ca50a4f2be1c1a80


在阿里云的“对象存储OSS”新建立的路径文件中就已上传了刚刚那个文件。
00164085da57bbe59cfd967a93de070


注意事项

由于暴露的AccessKey ID及AccessKey Secret是新建用户的,与主账户无关。

而且该用户只能通过STS扮演、拥有部分API权限的角色,才能正常访问某个Bucket。

因此由信息泄露造成的风险被降到了最低。


关键代码

最后聊一下核心代码 sts.js 及调用方案 provide.js

    this.oss = new OSS({
      region: config.region,
      bucket: config.bucket,
      accessKeyId: res.Credentials.AccessKeyId,
      accessKeySecret: res.Credentials.AccessKeySecret,
      stsToken: res.Credentials.SecurityToken,
      refreshSTSToken: async () => {
        const {Credentials} = await this.sts.assumeRole(config.role);
        return {
          accessKeyId: Credentials.AccessKeyId,
          accessKeySecret: Credentials.AccessKeySecret,
          stsToken: Credentials.SecurityToken,
        };
      },
      refreshSTSTokenInterval: 3600,
    });


从调用处provide.js可以看到,OSS 会通过refreshSTSToken去实现Token的刷新。

而刷新的时间间隔是refreshSTSTokenInterval中定义的3600秒,即1小时。

  async assumeRole(role, policy, expiration) {
    const method = 'POST';
    const opts = this.options;
    const params = {
      Action: 'AssumeRole',
      RoleArn: role,
      RoleSessionName: 'app',
      DurationSeconds: expiration || 3600,
      Format: opts.format,
      Version: opts.apiVersion,
      AccessKeyId: opts.accessKeyId,
      SignatureMethod: opts.sigMethod,
      SignatureVersion: opts.sigVersion,
      SignatureNonce: Math.random(),
      Timestamp: new Date().toISOString(),
    };
    if (policy) {
      params.Policy = typeof policy === 'string' ? JSON.stringify(JSON.parse(policy)) : JSON.stringify(policy);
    }
    params.Signature = STS.getSignature(method, params, opts.accessKeySecret);
    const result = await kintone.proxy(
      opts.endpoint,
      method,
      {'Content-Type': 'application/x-www-form-urlencoded'},
      new URLSearchParams(params).toString(),
    );
    const body = JSON.parse(result[0]);
    const status = result[1];
    if (status !== 200) {
      const err = new Error();
      err.status = status;
      const {Code, Message, RequestId} = body || {};
      err.message = `${Code}: ${Message}`;
      err.requestId = RequestId;
      throw err;
    }
    return body;
  }


在sts.js中的token获取方案,是向sts.aliyuncs.com 发送对应的API请求。

由于普通的js存在跨域问题,因此发送授权请求,必须在服务端运行。

而在kintone中,我们则可以使用kintone.proxy来代理发送授权请求,从而避免跨域问题。


那么,如果要改用安全性更高的kintone.plugin.app.proxy应该怎么写?

由于传递的参数类型是application/x-www-form-urlencoded,而且body的数据并不是Object,在调用kintone.plugin.app.proxy时会被 限制

此时可以尝试将params放到url中,并将method改为GET来发送请求。

实测成功,然而并不采用,原因有二:

原因一:阿里云的官方并没有明确声明支持AssumeRole的Get请求,因此不推荐将其应用于实际的项目中。

原因二:kintone.plugin.app.proxy对比kintone.proxy的好处,仅仅是隐藏了accessKeyId。

而另一个关键参数accessKeySecret由于被指纹加密过,无法还原。

因此在安全性上,二者几无差别。

考虑到原因一的影响,在这种情况下还是推荐使用kintone.proxy。


最后

本文是在  使用“Tencent Cos for Kintone” 插件实现腾讯云+kintone文件管理  原有基础上的升级。既保证了安全性,又起到了扩容的效果。感兴趣的小伙伴都可以来试用一下!