亚马逊AWS官方博客
AWS KMS 实现跨租户的安全数据加密
1 技术背景
在大量saas 平台客户需求实践中,有非常多的User担心敏感数据如何保存,如何安全隔离敏感数据。本文主要针对saas平台敏感数据加密需求,利用AWS KMS 服务,结合用户场景构建数据安全加密方案。
1.1 问题描述
- 对客户数据里的指定数据加密(如:姓名,电话,地址等);
- 密码与加解密权限要有严格的管理方案(隔离租户与用户间的使用权限);
- 隔离除系统管理员外其他开发和运维用户访问用户数据;
- 记录CMK 访问记录,用户可以审计data key使用情况;
- 用户可以拥有全部的CMK管理权限(可以随时修改CMK使用策略);
- 采用行业认证的公开加密算法和加密流程加密数据(信封加密)。
1.2 系统架构
- AWS cognito 实现saas平台认证机制集成;
- 通过AWS KMS 服务实现主密钥的安全管理;
- AWS encryption SDK 实现信封加密;
- IAM Policy 实现系统层的安全隔离;
- Cloud trail 实现操作审计;
1.3 数据加密解密流程
2 KMS 配置
2.1 创建KMS CMK
2.2 创建别名
2.3 配置CMK 及管理权限
安全考虑CMK管理和数据加密权限是分开管理
防止管理员误删除CMK
2.4 配置用户使用用户(方案中选定角色,与下面Cognito 角色一致)
2.5 编辑KMS 策略(只允许指定的租户可以使用CMK)
{
"Id": "key-consolepolicy",
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Enable IAM User Permissions",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws-cn:iam::accountid:root"
},
"Action": "kms:*",
"Resource": "*"
},
{
"Sid": "Allow use of the key",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws-cn:iam::accountid:role/Cognito_testAuth_Role"
},
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey"
],
"Resource": "*",
"Condition": {
"StringLike": {
"cognito-identity.amazonaws.com:amr":
"[\"authenticated\",\"cognito Developer provider name\",\"cognito Developer provider name:cn-north-1:????????????????????????????????????:租户ID:*\"]"
}
}
}
]
}
3 配置 Cognito identity pool 及IAM 角色
3.1 创建Identity pool 及 IAM role
3.2 配置 Developer provider name
3.3 创建IAM 角色
利用identity pool 生成的 身份证书管理CMK(权限已在 CMK 管理中指定),删除其他的权限
4 实现 Cognito Developer provider
/*
*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except
* in compliance with the License. A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
*/
package com.amazon.saas.idp;
import java.util.Collections;
import java.util.Map;
import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
import com.amazonaws.services.cognitoidentity.AmazonCognitoIdentity;
import com.amazonaws.services.cognitoidentity.AmazonCognitoIdentityClientBuilder;
import com.amazonaws.services.cognitoidentity.model.GetCredentialsForIdentityRequest;
import com.amazonaws.services.cognitoidentity.model.GetCredentialsForIdentityResult;
import com.amazonaws.services.cognitoidentity.model.GetOpenIdTokenForDeveloperIdentityRequest;
import com.amazonaws.services.cognitoidentity.model.GetOpenIdTokenForDeveloperIdentityResult;
public class CognitoDeveloperIdentityProvider {
private String region = "cn-north-1";
//cognito 对于 sts 的 provider id 中国区选择 "cognito-identity---cn-north-1.amazonaws.com.rproxy.goskope.com.cn"
private String cognitoProviderId= "cognito-identity---cn-north-1.amazonaws.com.rproxy.goskope.com.cn"
// 前面配置的 Developer Identity Provider Name
private String developerIdentityProviderName = "developerIdentityProviderName";
// indentity pool id 如 cn-north-1:cc310c44-de78-4661-8e6e-8cc21d974058
private String identityPoolId="cn-north-1:cc310c44-de78-4661-8e6e-8cc21d974058";
private AmazonCognitoIdentity amazonCognitoIdentity;
// 构建 AmazonCognitoIdentityClient
public void init() {
AmazonCognitoIdentityClientBuilder cidBuilder = AmazonCognitoIdentityClientBuilder.standard();
cidBuilder.setRegion(region);
cidBuilder.setCredentials(DefaultAWSCredentialsProviderChain.getInstance());
amazonCognitoIdentity = cidBuilder.build();
}
//获取 cognito OpenIdToken
public GetOpenIdTokenForDeveloperIdentityResult getOpenIdTokenFromCognito(String tenantid, String userid) {
GetOpenIdTokenForDeveloperIdentityRequest request = new GetOpenIdTokenForDeveloperIdentityRequest();
request.setIdentityPoolId(identityPoolId);
Map<String, String> logins = Collections.singletonMap(developerIdentityProviderName, tenantid + ":" + userid);
request.setLogins(logins);
GetOpenIdTokenForDeveloperIdentityResult result = amazonCognitoIdentity
.getOpenIdTokenForDeveloperIdentity(request);
return result;
}
//获取 aws credentials 用于调用 KMS
public GetCredentialsForIdentityResult getCredentialsForIdentity(String identityid, String token, TenantUserInfo userinfo) {
GetCredentialsForIdentityRequest request = new GetCredentialsForIdentityRequest();
request.setIdentityId(identityid);
request.setLogins(Collections.singletonMap(cognitoProviderId, token));
GetCredentialsForIdentityResult result = amazonCognitoIdentity.getCredentialsForIdentity(request);
return result;
}
}
5 实现数据加密解密
5.1 AWS 证书与data key 缓存
/*
*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except
* in compliance with the License. A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
*/
package com.amazon.saas.tes;
import java.util.concurrent.TimeUnit;
import com.amazon.saas.tes.TenantCryptoMaterialsManagerHolder.Cachekey;
import com.amazonaws.auth.BasicSessionCredentials;
import com.amazonaws.encryptionsdk.CryptoMaterialsManager;
import com.amazonaws.encryptionsdk.MasterKeyProvider;
import com.amazonaws.encryptionsdk.caching.CachingCryptoMaterialsManager;
import com.amazonaws.encryptionsdk.caching.CryptoMaterialsCache;
import com.amazonaws.encryptionsdk.caching.LocalCryptoMaterialsCache;
import com.amazonaws.encryptionsdk.kms.KmsMasterKey;
import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
public class TenantCryptoMaterialsManagerHolder extends CacheLoader<Cachekey, CryptoMaterialsManager> {
@Getter
@Setter
@AllArgsConstructor
public static class Cachekey {
private String tenantID;
private String userid;
}
@Data
public static class Config {
private String keyArn;
private int maxCacheSize;
private int maxEntryAge;
private int credentialsDuration;
}
private LoadingCache<Cachekey, CryptoMaterialsManager> cache;
private Config config;
public void init() {
//配置缓存参数
config=new Config();
cache = CacheBuilder.newBuilder().maximumSize(10000)
.expireAfterWrite(config.getCredentialsDuration(), TimeUnit.MINUTES).build(this);
}
public CryptoMaterialsManager createMaterialsManager(Cachekey key) {
Credentials credentialsForIdentity = cognitoDeveloperIdentityProviderClient;
MasterKeyProvider<KmsMasterKey> keyProvider = KmsMasterKeyProvider.builder()
.withKeysForEncryption(config.getKeyArn())
.withCredentials(new BasicSessionCredentials(credentialsForIdentity.getAccessKeyId(),
credentialsForIdentity.getSecretKey(), credentialsForIdentity.getSessionToken()))
.build();
int MAX_CACHE_SIZE = config.getMaxCacheSize();
CryptoMaterialsCache cache = new LocalCryptoMaterialsCache(MAX_CACHE_SIZE);
int MAX_ENTRY_AGE_SECONDS = config.getMaxEntryAge();
int MAX_ENTRY_MSGS = config.getMaxCacheSize();
CryptoMaterialsManager cachingCmm = CachingCryptoMaterialsManager.newBuilder()
.withMasterKeyProvider(keyProvider).withCache(cache).withMaxAge(MAX_ENTRY_AGE_SECONDS, TimeUnit.SECONDS)
.withMessageUseLimit(MAX_ENTRY_MSGS).build();
return cachingCmm;
}
public CryptoMaterialsManager getMaterialsManager(TenantUserInfo userinfo) {
return cache.get(new Cachekey(userinfo.getTenantID(), userinfo.getUserid()));
}
@Override
public CryptoMaterialsManager load(Cachekey key) throws Exception {
return createMaterialsManager(key);
}
}
5.2 数据加密解密
/*
*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except
* in compliance with the License. A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
*/
package com.amazon.saas.tes;
import java.util.Collections;
import com.amazonaws.encryptionsdk.AwsCrypto;
import com.amazonaws.encryptionsdk.CryptoMaterialsManager;
import com.amazonaws.encryptionsdk.CryptoResult;
import org.springframework.beans.factory.annotation.Autowired;
public class EncDecService {
private final AwsCrypto crypto = new AwsCrypto();
@Autowired
TenantCryptoMaterialsManagerHolder tenantCryptoMaterialsManagerHolder;
public TESEncryptedMessage encrypt(String plaintext, String authorizationHeader, TenantUserInfo userinfo) {
CryptoMaterialsManager cmm = tenantCryptoMaterialsManagerHolder.getMaterialsManager(authorizationHeader,
userinfo);
String message = crypto.encryptString(cmm, plaintext,
Collections.singletonMap("tenantid.userid", userinfo.getTenantID() + "." + userinfo.getUserid()))
.getResult();
TESEncryptedMessage re = new TESEncryptedMessage();
re.setEncyptedMsg(message);
return re;
}
public TesPlainText dectypt(String encryptMessage, String authorizationHeader, TenantUserInfo userinfo) {
CryptoMaterialsManager cmm = tenantCryptoMaterialsManagerHolder.getMaterialsManager(authorizationHeader,
userinfo);
CryptoResult<String, ?> decryptResult = crypto.decryptString(cmm, encryptMessage);
TesPlainText re = new TesPlainText();
re.setText(decryptResult.getResult());
return re;
}