被脱库咋办?KMS 给你解决方案!

最近经常能听见某某某公司用户信息被泄漏了,某某某公司用户开房记录被脱裤了 … 数据隐私是企业的生命线,其重要性我想无需我多说什么。

上一篇文章我们科普了一下 AWS KMS 服务:
AWS KMS 科普: What Why and How?
简要回顾一下:KMS 全称为 Key Management Service,也就是一个密钥管理系统,它在设计上从根本解决了数据的安全性问题,同时还具备完整的安全审计功能,能让我对谁能够访问我的数据、谁什么时候动了我的数据一目了然。

借助 KMS 的帮助,我们能够做到,即使我被脱裤了,攻击者拿到我的数据也无法解密!这篇文章,我们就来具体看看怎么将 KMS 应用到实践中,从而提升产品安全性。

这里我们将产品分为两种:

  1. 一般类型产品:需要保护自身数据,比如各种自营电商平台等。
  2. 平台类产品:平台服务商需要保护租户数据,比如 Authing、有赞 等。

之所以这么区分,是因为平台服务商属于多租户场景。对于一般类型产品,只需要加密好数据就行了,而对于平台类产品,理想情况下,租户希望做到以下两点:

1. 知道平台服务商什么时候、以什么理由访问了我的数据。
2. 当我不再使用你的服务时,我可以一键取消对平台服务商的数据授权,今后平台服务商再也无法访问到我的数据。

这种情况下,可以使用 KMS 跨账号授权的功能,基本流程为:租户在 AWS KMS 后台创建一个密钥并授权给平台服务商,平台服务商使用这个密钥加密/解密该租户的数据,借助 AWS Cloud Trail ,租户可以很轻松地知道平台服务商什么时候、以什么理由访问了我的数据。当租户决定不再使用平台服务商产品的时候,只需要在 AWS KMS 后台取消对其授权就行了。

基本原理就这么简单,下面从技术角度,梳理一下需要解决的几个技术问题:

  1. 如何使用 KMS 加密/解密的数据?
  2. 如何将 KMS 密钥授权给其他账号?
  3. 如何使用 Cloud Trail 查看第三方什么时候访问了我的数据?
  4. 作为平台服务商,如何解决 KMS 服务商绑定问题?
  5. 最后一点也是比较有难度的一点:如何在不大幅变动业务逻辑的情况下做到对数据自动加密解密?

下面我们由浅入深地介绍如何将 KMS 应用到你的产品中。

1. 开通 KMS 服务,创建一个 KMS CKM

首先你需要有一个 AWS 账号,创建一个具备 KMS 全部权限的 User(或者 Role),该 User 继承的 Policy 如下:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "kms:*",
            "Resource": "*"
        }
    ]
}

创建好 User 之后,你应该能够得到该 User 的 accessKeyId 和 secretAccessKey,接下来在 SDK 中会用到。

接着在 AWS Console KMS 管理页面,创建一个客户管理的密钥 (Custom Managed Key),并将此密钥的「密钥管理员」和「可在加密操作中使用 CMK 的 IAM 用户和角色」设置为前一步创建的 User,创建之后你可以得到该密钥的 KeyId:


总结一下,这一步我们获取了以下这些内容:accessKeyId secretAccessKey 和 KeyId。

2. KMS 加密/解密 Demo

我们一般使用 AWS Encryption SDK 来完成数据的加密解密,它为我们节省了什么工作量,同时自带了很多最佳实践,如使用信封加密、 kerying 等。

AWS Encryption SDK 支持多种语言,如 Java/Python/C/JavaScript 等,这里我们以 JavaScript 为例。
加密 Demo:

import * as AWS from "aws-sdk"
import { KmsKeyringNode, encrypt, decrypt, getClient, MessageHeader } from '@aws-crypto/client-node'

 const keyring = new KmsKeyringNode({
  generatorKeyId: "给你的 KeyId",
  clientProvider: getClient(AWS.KMS, {
      credentials: {
        accessKeyId: config.accessKeyId,
        secretAccessKey: config.secretAccessKey,
      }
    })
 })
 const { result } = await encrypt(keyring, "需要加密的内容", { encryptionContext: {
    key: "额外上下文信息"
} })

说明以下几点:

  1. generatorKeyId 就是 CMK 密钥 ID
  2. 这里初始化了一个 keyring 。
  3. encryptionContext 是可选的加密上下文信息,可以在 Cloud Trail 审计页面看到。

解密 Demo:

import * as AWS from "aws-sdk"
import { KmsKeyringNode, encrypt, decrypt, getClient, MessageHeader } from '@aws-crypto/client-node'

 const keyring = new KmsKeyringNode({
  generatorKeyId: "你的 KeyId",
  clientProvider: getClient(AWS.KMS, {
      credentials: {
        accessKeyId: config.accessKeyId,
        secretAccessKey: config.secretAccessKey,
      }
    })
 })
const { plaintext, messageHeader } = await decrypt(keyring, encryptedData)

3. 如何将密钥授权给其他 AWS 账号

在创建 CMK 「定义密钥使用权限」步骤时,可以指定需要授权的第三方 AWS 账号,填入对方的 AWS 账号 ID 即可。

被授权方,可以按照如下方式使用此 CMK:

  1. 创建一个具备全部 CKM 操作权限的 User
  2. 将用户继承以下 Policy:
    a. Resource 为 CMK 的 arn
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Allow Use Of CMK In External Account",
      "Effect": "Allow",
      "Action": [
        "kms:Encrypt",
        "kms:Decrypt",
        "kms:ReEncrypt*",
        "kms:GenerateDataKey*",
        "kms:DescribeKey"
      ],
      "Resource": "arn:aws:kms:ap-northeast-1:xxxxxxxxx:key/b09ea52c-7262-4e60-b775-xxxxxx"
    }
  ]
}
  1. 借助 SDK 使用 CMK 进行数据加密、解密。
  2. 当授权方不再希望被授权方使用此 CMK 时,在密钥管理页面将对方账号 ID 移除。

4. 利用 Cloud Trail 查看密钥审计

创建一个 Trail:

默认情况下,接下来你的密钥所有使用情况都会出现在 Event History 中了:

如上图所示,你可以看到 Decrypt(解密) 记录,以及请求详情:

5. 作为平台服务商,如何解决 KMS 服务商绑定问题

市面上常见的的 KMS 服务商大致有:

  • AWS KMS
  • 阿里云 KMS
  • 腾讯云 KMS

作为平台服务商,我们最好能够提供给租户选择 KMS 服务商的权利,而各家 KMS 服务商的使用方法存在差异,所以我们就需要做一下代码层面的封装,比如可以设置 kms, aliyun, aws, tecent 四个模块,在 kms 模块暴露 encrypt 和 decrypt 方法,根据租户配置的 KMS 服务商调用对应实际方法。

6. 如何在尽量不改动已有代码前提下,对数据完成加密、解密

如果前期代码设计的不够好,会出现需要大量修改已有查询、插入业务代码情况,这种成本是很大的。所以我们需要思考一下其他的解决方案:

  1. 订阅数据库日志,也即 MySQL 的 binlog、 PostgreSQL的 Write-Ahead Logging (WAL)、mongodb 的 change streams。
  2. 利用数据库 ORM 的 hook,如 typeorm Entity Listeners and Subscribers 和 mongoose middlewares 等,在插入数据前加密、读取数据之后解密。

下面给一个 mongoose 的示例代码:

schema.post(['find', 'findOne'], async function (docs, next) {
  if (!Array.isArray(docs)) {
    docs = [docs];
  }
  for (let doc of docs) {
   // 对数据进行解密
  }
  next()
})

schema.pre('findOneAndUpdate', async function (next) {
 // 对数据进行加密
})

schema.post('findOneAndUpdate', async function (doc, next) {
  // 对数据进行解密
})

总结

脱裤事件层出不穷,一个脱裤事件背后就是一家企业的灾难,我们都需要为用户的数据负责,尤其是平台服务商。在目前看来,KMS 不失为一个很好的选择。