亚马逊AWS官方博客

当 DR 灾备遇到 KMS

中国区的KMS服务已经发布,为加密的需求提供了便利的解决方案。但是注意到为了满足安全的要求,KMS的CMK是不允许出区域的,在这种情况下,已经加密的资料,如何能够在另外的区域使用?采用了KMS加密的方案,如何实施灾备呢?

一般来说,各种应用场景可以归类为服务器端加密和客户端加密两种实现方式,针对着两种不同的实现方式,本文都提供了相应的解决方案建议。

【应用场景和对应方案】

1. 场景一、服务器端加密的DR方案

服务器端加密主要是指KMS与托管服务集成的应用场景。这样的场景下的DR方案,代码无感,无需研发人员修改代码。

1.1信封加密的基本原理

为了实现对DR的支持,需要借助信封加密的方法。先回顾信封加密的原理。

 

在信封加密中,首先需要创建在KMS中创建一个CMK(Customer Master Key);其次,在创建数据库或者EBS卷的时候,选择使用刚才创建的CMK加密。实际上,系统服务在后台创建了一个data encryption key(DEK)真正用于加密的数据。这个DEK则需要使用之前指定的CMK进行加密保护。加密后的DEK则会跟相应的业务(RDS/EBS)保存在一起。

 

信封加密之加密过程

服务器根据客户选择的CMK,生成一个DEK,利用DEK加密数据,然后用CMK加密DEK,将加密的DEK和数据密文放在一个信封里。

信封加密之解密过程

从信封中取出加密的DEK,通过CMK解密后得到明文的DEK,使用DEK对数据密文进行解密,得到明文数据。

信封加密在DR方案中应用示意

 

而在DR场景中,数据密文和DEK明文在两个不同的区域间是保存不变的,这样就保证了复制到两个区域的数据是被同一个DEK加密的,就能使用同一个DEK进行解密。关键在于在主区域中的DEK,是用主区域的CMK进行加密保护;而在数据跨区域复制过程中,DEK会用主区域的CMK解密后,将明文的DEK使用备份区域的CMK进行加密保护。这样,借助信封加密的方法,避免了对海量的源数据进行解密和重新加密的过程,高效实现加密数据的跨区域备份。

 

1.2 KMS加密的EBS卷,跨区域复制方法

在数据解密的过程中,以EBS解密为例。当EC2需要读取加密的EBS卷,保存在EBS上的加密的DEK,会通过接口,发送到KMS服务,通过相应的CMK,对DEK进行解密,在这个过程中,传输的是DEK,CMK始终保存在KMS服务中,降低泄漏风险。通过接口返回明文的DEK(当然,传输通道是加密的),EC2就可以使用该明文的DEK,对EBS进行解密,读取相应的数据。该过程对EC2上的应用程序是透明无感的,应用的代码不需要做任何修改。

关于如何将已有的RDS数据库转换为加密的数据库,请参见

https://amazonaws-china.com/cn/blogs/aws/amazon-rds-update-share-encrypted-snapshots-encrypt-existing-instances/

 

那么在DR景下,如何完成加密的EBS卷的夸区域复制呢?

在信封加密中,用户数据实际上是被DEK加密的,而加密的DEK会跟EBS卷保存在一起。因此,当我们需要将加密的EBS卷做垮区域的复制,以便支持DR的场景,需要解决的问题就是如何在新的区域能够将DEK解密,得到明文DEK,就可以对数据解密了。

 

命令行的方式如下

以下代码示例将数据库快照从北京区域复制到宁夏区域并重新加密。在宁夏区域中运行命令。注意kms-key-id这个是目标区域宁夏的

aws rds copy-db-snapshot \
--source-db-snapshot-identifier arn:aws-cn:rds:cn-north-1:3264xxxxxx:snapshot:rds:test-2019-06-29-12-19 \
--target-db-snapshot-identifier ningxia-encry-dbsnapshot \
--source-region cn-north-1 \
--kms-key-id xxxxx

1.3 KMS加密的RDS数据库,创建跨区域只读副本

对于使用了KMS加密的RDS数据库,可以按照以下步骤创建垮区域的只读副本

首先源区域创建使用KMS加密的数据库。

然后在控制台选定该数据库实例,针对该数据库创建只读副本操作;

对于目标区域 Destination region,选择要复制的目的区域,比如ZHY;

切换到目标区域的KMS页面,记录目标区域使用的自建的CMK或者AWS managed keys的ARN

 

在Encryption 选项下面,篮框部分粘贴目标区域的CMK的ARN

 

这两个步骤是最关键的,通过这样的方式,创建出来的目标区域的数据库只读副本,对DEK加密的CMK就会被替换为目标区域的CMK。在这个过程中数据库的数据没有收到任何改变,加密的数据不变,使用的DEK也没有发生变化。

2. 场景二、客户端加密:使用KMS的encryption SDK的应用场景,涉及代码修改

有一些场景下,客户需要使用客户端方式对数据进行加密,这样的方式下,如何实现DR方案呢?

在AWS中,提供了ENCRYPTION SDK,可以支持多个CMK,每个Provider对应了一个或多个CMK。如下图所示,假设我们需要在BJS北京区域和ZHY宁夏区域都能够使用同一个DEK对数据加解密。

我们可以通过SDK的 KmsMasterKeyProvider 方法,通过传入的CMK的ARN,可以构造一个 KmsMasterKeyProvider。我们可以分别通过BJS区域的CMK-bjs以及ZHY区域的CMK-zhy各自构造出一个KmsMasterKeyProvider。将这两个KmsMasterKeyProvider作为参数调用MultipleProviderFactory.buildMultiProvider 方法,就可以构建出一个包含多个CMK的Provider。

接下来,我们就可以使用这个Provider对同一个DEK进行加密,就可以得到两个加密的DEK,分别是Encryted Data Keybjs 和 Encrypted Date Keyzhy. 这两个被加密的datakey,在分别使用北京区域和宁夏区域对应的CMK解密之后,得到的明文的datakey是一样的。这就是加密场景在跨区域DR的应用的基础了。在这个过程中,DEK是不可见的,实际上,调用encryptData(provider, plaintext, context),在方法内部会使用DEK对plaintext做加密。

 

注:context:所有 AWS KMS 加密操作都接受加密上下文,它是一包含有关数据的外上下文信息的 可选键值对。以加密方式定到密文,需要 相同的加密上下文 来解密(或解密和重新加密)数据。

 

在北京区域解密过程。被解密的数据,通过调用 AwsCrypto().decryptData 的方法,只需要传入provider和密文就可以了。在decyptData方法内部,如下图所示,自动判断应该使用BJS的CMK,对DEK解密后,使用DEK的明文,就可以用于对密文进行解密了。

 

在宁夏区域解密过程也是类似的。SDK自动判断应该使用ZHY的CMK,对DEK解密后,使用DEK的明文,用于对密文进行解密。

参考代码如下。此外,AWS提供了一个完整的LAB,建议通过这个LAB来熟悉SDK不同的使用场景。

http://busy-engineers-guide.reinvent-workshop.com/index.html

以java为例,需要指定依赖的encryption sdk的版本:

<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-encryption-sdk-java</artifactId>
    <version>1.6.0</version>
</dependency>

需要构造包含多个CMK的provider,例如

kms = AWSKMSClient.builder().build();
this.masterKeyEast = new KmsMasterKeyProvider(keyIdEast)
     .getMasterKey(keyIdEast);
this.masterKeyWest = new KmsMasterKeyProvider(keyIdWest)
     .getMasterKey(keyIdWest);
this.provider = getKeyProvider(masterKeyEast, masterKeyWest)

 

其中的getKeyProvider的方法如下:   

private static MasterKeyProvider<?> getKeyProvider(KmsMasterKey masterKeyEast, KmsMasterKey masterKeyWest) {
         return MultipleProviderFactory.buildMultiProvider(masterKeyWest, masterKeyEast);
    }
}

 

执行数据加密的时候,就可以调用这个provider,这个provider会使用之前初始化的多个CMK对数据的DEK进行加密。

public String encrypt(JsonNode data) throws IOException {
     FormData formValues = MAPPER.treeToValue(data, FormData.class);

     LOGGER.info("Got form submission for order " + formValues.orderid);

     byte[] plaintext = MAPPER.writeValueAsBytes(formValues);

     HashMap<String, String> context = new HashMap<>();
     context.put(K_MESSAGE_TYPE, TYPE_ORDER_INQUIRY);

     byte[] ciphertext = new AwsCrypto().encryptData(provider, plaintext, context).getResult();

     return Base64.getEncoder().encodeToString(ciphertext);
}

 

执行数据解密的时候,调用这个Provider即可,注:解密的时候,可以用多个CMK,也可以用当前区域的单个CMK初始化Provider。后者代码执行效率更高。

public JsonNode decrypt(String ciphertext) throws IOException {
     byte[] ciphertextBytes = Base64.getDecoder().decode(ciphertext);

     CryptoResult<byte[], ?> result = new AwsCrypto().decryptData(provider, ciphertextBytes);

     // Check that we have the correct type
     if (!Objects.equals(result.getEncryptionContext().get(K_MESSAGE_TYPE), TYPE_ORDER_INQUIRY)) {
         throw new IllegalArgumentException("Bad message type in decrypted message");
     }

     return MAPPER.readTree(result.getResult());
}

3. 场景三,客户端加密场景扩展,需要固定DEK支持密文对比

某些场景下,加密数据不会解密使用,而是用密文做对比,比如用户的密码,数据库存放的就是密文。用户在客户端输入的密码,在加密之后传输,通过密文与密文的对比来判断密码是否一致。这样的情况下,都不会涉及解密过程,但是要求不管任何时间对同样的明文,加密的密文保持不变。这样就要求DEK必须固定不变。在场景二介绍的Encryption SDK中,为了提高安全等级,DEK是调用的时候临时生成的,每次调用,生成的DEK是不一样的。因此场景二的解决方案不能直接用于场景三。调整如下

为了保证DEK不变,需要将DEK从封装的方法中剥离出来,Client端自己管理DEK。具体如下

 

DEK的生成和管理

首先,调用Encryption SDK 中的generateDataKey 方法,生成一个DEK       

// generate data key, with CMK, with AES_128
        GenerateDataKeyRequest dataKeyRequest = new GenerateDataKeyRequest();
        //dataKeyRequest.setKeyId(KEYID);
        dataKeyRequest.setKeyId(this.CMKKeyID);
        dataKeyRequest.setKeySpec("AES_128");
        GenerateDataKeyResult dataKeyResult = kms.generateDataKey(dataKeyRequest);                

 

其次,对该DEK进行加密保护,在加密的时候,为了保证该DEK能够在DR场景下使用,也需要使用场景二中介绍的包含多个区域的CMK的Provider,对DEK进行加密保护,这样,我们就可以在不同的区域都可以将加密的DEK进行解密了。      

// plain text data key
        ByteBuffer plainTextKey = dataKeyResult.getPlaintext();
        // 使用multiple provider 加密管理 datakey,以便跨区域使用
        CryptoResult<byte[], ?> result = new AwsCrypto().encryptData(this.provider, plainTextKey.array());

经过加密的DEK,就可以保存在数据库中备用了。当然,还可以放一份在S3上作为冷备保存,借助S3高达11个9的持久性,更为放心。因为DEK已经加密了,所以也不担心安全的风险。

 

数据加解密环节

从数据库中取出加密的DEK,调用场景二中的解密方法decryptData(),得到明文的DEK,这时候就可以使用该DEK对数据进行加解密了。由于encryption SDK 中包含的方法都有自己的DEK,因此建议使用成熟的库支持AES加密的SDK,对数据做加密和解密。比如java中的Cipher库 javax.crypto.Cipher;

4. 总结

无论是使用客户端加密还是服务器端加密的方式,AWS都提供了对应的DR(多区域)的方案。但是从具体的操作方式来看,服务器端的加密,更加简单方便,无需修改代码;而客户端的方式,是需要在代码层面进行调整的。因此我们建议优先使用服务器端加密的方式,在服务器端加密不能满足的场景下,再考虑客户端加密的方式。

 

本篇作者

陈昇波

亚马逊AWS解决方案架构师,负责基于AWS的云计算方案架构的咨询和设计,同时致力于AWS云服务在国内的应用和推广。现致力于网络安全和大数据分析相关领域的研究。在加入AWS之前,在爱立信东北亚区担任产品经理,负责产品规划和方案架构设计和实施,在用户体验管理以及大数据变现等服务方面有丰富经验。