在 DynamoDB 中使用 KMS 存储敏感信息
Source: Dev.to
请提供您希望翻译的文章正文(除代码块和 URL 之外的文字),我将把它翻译成简体中文并保持原有的 Markdown 格式。
介绍
最近,我遇到了 AWS EventBridge Connections 的问题。它是一个托管的 AWS 服务,帮你处理机密——你为 API 配置身份验证(无论是为自己还是为你的客户,例如 webhook),当它与 EventBridge API 目标或 Step Functions HTTP 调用任务关联时,EventBridge Connections 会处理其余工作。
这两项服务乍看之下都很不错,但在超出简单用例后会暴露出局限性。对我而言,缺乏自定义和控制成为了阻碍。这促使我去研究替代方案:我可以在哪里安全地存储客户提供的机密或敏感数据?
显而易见的选择:AWS Secrets Manager
对于大多数人来说,首先想到的解决方案是 AWS Secrets Manager。Secrets Manager 是一项专为存储和轮换机密(如数据库凭证、API 密钥和 OAuth 令牌)而设计的全托管服务。
什么是 Secrets Manager?
AWS Secrets Manager 帮助您在无需前期投入和持续维护成本的情况下,保护对应用程序、服务和 IT 资源的访问。它使您能够在整个生命周期中轮换、管理和检索数据库凭证、API 密钥以及其他机密。
主要特性包括
- 自动机密轮换
- 通过 IAM 实现细粒度访问控制
- 通过 CloudTrail 日志进行审计和合规
- 与 RDS、DocumentDB 以及其他 AWS 服务集成
不足之处
虽然 Secrets Manager 功能强大,但并非总是必需的:
| 问题 | 细节 |
|---|---|
| 成本 | 每个机密每月 $0.40,外加每 10,000 次 API 调用 $0.05。对于管理大量客户机密的应用程序,这笔费用会迅速累积。 |
| 对简单用例来说功能过剩 | 如果您不需要自动轮换或高级功能,就在为不使用的功能付费。 |
| 复杂性 | 对于直接的加密需求,该服务会增加不必要的开销。 |
这时 AWS Key Management Service (KMS) 就成为一个有吸引力的替代方案。
更合适的选择:AWS KMS
什么是 KMS?
AWS Key Management Service(KMS)是一项托管服务,可轻松创建和控制用于加密数据的密码密钥。与 Secrets Manager 不同,KMS 不存储你的机密——它只存储用于自行加密和解密数据的加密密钥。
类比
- Secrets Manager:存放机密的安全金库。
- KMS:持有你用于锁定/解锁自己金库的钥匙的保管人。
为什么在 DynamoDB 中使用 KMS?
DynamoDB 加密 vs. 应用层加密
- DynamoDB 已经默认使用 AWS 托管的 KMS 密钥对所有静态数据进行加密。这可以防止对物理磁盘的访问以及 AWS 基础设施层面的威胁。
- 但是,仅靠服务器端加密(SSE)在处理 客户提供的机密 时往往不足。
应用层加密(在将数据写入 DynamoDB 之前先进行加密)提供了额外的保障:
- 防止 IAM 策略过于宽松导致的风险
- 在意外数据访问时限制暴露范围
- 保证导出、备份和日志中的数据仍保持加密状态
- 在应用边界实现细粒度的访问控制
在 DynamoDB 中存储敏感数据时,你主要有两种做法:
- 在 DynamoDB 中存储引用 – 将机密加密后存入 AWS Secrets Manager(或 SSM Parameter Store),然后在 DynamoDB 中仅保存该引用。
- 直接在 DynamoDB 中存储加密数据 – 使用 KMS 对敏感数据进行加密,并将加密后的值直接写入 DynamoDB 表中。
第二种方法对许多使用场景来说更简单且成本更低。下面我们来看看如何实现它。
使用 AWS CDK 设置 KMS
下面是一个最小化的 CDK 堆栈示例,它会创建一个 KMS 密钥和一个 DynamoDB 表,然后授予 Lambda 函数加密/解密数据的权限。
import * as kms from 'aws-cdk-lib/aws-kms';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Stack, StackProps, RemovalPolicy } from 'aws-cdk-lib';
import { Construct } from 'constructs';
export class SecureStorageStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// Create a KMS key for encrypting sensitive data
const encryptionKey = new kms.Key(this, 'SensitiveDataKey', {
description: 'Key for encrypting customer secrets in DynamoDB',
enableKeyRotation: true, // Automatically rotate key every year
removalPolicy: RemovalPolicy.RETAIN, // Keep key even if stack is deleted
});
// Create DynamoDB table
const secretsTable = new dynamodb.Table(this, 'SecretsTable', {
partitionKey: { name: 'customerId', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'secretId', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
});
// Grant your Lambda function access to the key
// (Assuming you have a Lambda function defined)
encryptionKey.grantEncryptDecrypt(yourLambdaFunction);
secretsTable.grantReadWriteData(yourLambdaFunction);
// Add key ARN to Lambda environment variables
yourLambdaFunction.addEnvironment('KMS_KEY_ID', encryptionKey.keyId);
yourLambdaFunction.addEnvironment('SECRETS_TABLE_NAME', secretsTable.tableName);
}
}
在 TypeScript 中加密和解密
重要限制:KMS 加密大小限制
AWS KMS Encrypt 的明文最大尺寸为 4 KB。这对于以下小型机密非常适用:
- API 密钥
- Webhook 密钥
- 短期 OAuth 令牌
对以下较大的负载将 无法工作:
- PEM 证书
- 大型 JSON 凭证
- 多字段配置块
大负载的信封加密
(原文在此处截断;后续应继续说明信封加密的原理,包括生成数据密钥、使用数据密钥加密负载,以及将加密后的数据密钥与密文一起存储。)
Source: …
机密
对于大于 4 KB 的机密,您应该使用 信封加密:
- 使用 KMS 生成数据加密密钥(DEK)
- 使用对称算法(例如 AES‑256‑GCM)在本地加密机密
- 将加密后的机密 以及 加密的数据密钥一起存储在 DynamoDB 中
- 仅在需要时使用 KMS 解密数据密钥
为什么使用信封加密?
- 能够扩展到任意大小的机密
- 最小化 KMS API 调用次数
- 是 AWS 推荐的最佳实践
本文聚焦于直接 Encrypt / Decrypt 的方式,以简化处理小型机密。对于处理更大负载的生产系统,应改用信封加密。
使用 AWS SDK for JavaScript v3 加密和解密敏感数据
import {
KMSClient,
EncryptCommand,
DecryptCommand,
} from '@aws-sdk/client-kms';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import {
DynamoDBDocumentClient,
PutCommand,
GetCommand,
} from '@aws-sdk/lib-dynamodb';
const kmsClient = new KMSClient({});
const dynamoClient = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const KMS_KEY_ID = process.env.KMS_KEY_ID!;
const TABLE_NAME = process.env.SECRETS_TABLE_NAME!;
interface CustomerSecret {
customerId: string;
secretId: string;
encryptedValue: string;
createdAt: string;
}
/**
* Encrypt a sensitive value using KMS
*/
async function encryptSecret(plaintext: string): Promise<string> {
const command = new EncryptCommand({
KeyId: KMS_KEY_ID,
Plaintext: Buffer.from(plaintext, 'utf-8'),
});
const response = await kmsClient.send(command);
if (!response.CiphertextBlob) {
throw new Error('Encryption failed: no ciphertext returned');
}
// Store as base64
return Buffer.from(response.CiphertextBlob).toString('base64');
}
/**
* Decrypt a KMS‑encrypted value
*/
async function decryptSecret(encryptedValue: string): Promise<string> {
const command = new DecryptCommand({
CiphertextBlob: Buffer.from(encryptedValue, 'base64'),
// KeyId is optional for decrypt – KMS knows which key was used
});
const response = await kmsClient.send(command);
if (!response.Plaintext) {
throw new Error('Decryption failed: no plaintext returned');
}
return Buffer.from(response.Plaintext).toString('utf-8');
}
/**
* Store an encrypted secret in DynamoDB
*/
async function storeSecret(
customerId: string,
secretId: string,
plainSecret: string,
): Promise<void> {
const encryptedValue = await encryptSecret(plainSecret);
const item: CustomerSecret = {
customerId,
secretId,
encryptedValue,
createdAt: new Date().toISOString(),
};
await dynamoClient.send(
new PutCommand({
TableName: TABLE_NAME,
Item: item,
}),
);
}
/**
* Retrieve and decrypt a secret from DynamoDB
*/
async function getSecret(
customerId: string,
secretId: string,
): Promise<string | null> {
const response = await dynamoClient.send(
new GetCommand({
TableName: TABLE_NAME,
Key: { customerId, secretId },
}),
);
if (!response.Item) {
return null;
}
const secret = response.Item as CustomerSecret;
return await decryptSecret(secret.encryptedValue);
}
// Example usage
async function example() {
// Store a customer's API key
await storeSecret('customer-123', 'api-key', 'super-secret-api-key-xyz');
// Retrieve and decrypt it later
const apiKey = await getSecret('customer-123', 'api-key');
console.log('Decrypted API key:', apiKey);
}
SSM Parameter Store vs KMS:权衡
您可能会想:是使用带 KMS 加密的 SSM Parameter Store,还是直接用 KMS 加密后将数据存入 DynamoDB?
SSM Parameter Store 方案
优点
- 集中式密钥管理
- 内置版本控制
- 免费层:最多 10 000 个参数
- 可与众多 AWS 服务集成
缺点
- 额外的 API 调用(SSM + DynamoDB)
- 延迟增加
- 需要管理两个服务
- 10 000 参数的限制在大规模时可能受限
示例
// 存入 SSM,在 DynamoDB 中引用
const paramName = `/customers/${customerId}/secrets/${secretId}`;
await ssm.putParameter({
Name: paramName,
Value: plainSecret,
Type: 'SecureString', // 使用 KMS 加密
KeyId: KMS_KEY_ID,
});
// 在 DynamoDB 中存储引用
await dynamodb.putItem({
TableName: 'Customers',
Item: {
customerId: { S: customerId },
secretRef: { S: paramName }, // 仅存储引用
},
});
在 DynamoDB 中直接使用 KMS 加密
优点
- 单一服务(DynamoDB)
- 延迟更低(一次 API 调用代替两次)
- 没有参数数量限制
- 架构更简洁
缺点
- 没有内置版本控制(需要自行实现)
- 在 AWS 控制台中可见性较低
- 需要手动处理密钥轮换
何时使用哪种
- 需要版本控制 – 如有需要,您必须实现自己的版本控制逻辑。
- 由客户提供且由客户拥有/轮换的机密 – 直接在 DynamoDB 中使用 KMS 加密通常足够,因为轮换由外部处理。
关键要点: 为任务选择合适的工具。
- Secrets Manager 在需要轮换的应用程序机密方面表现出色。
- KMS 在高吞吐量、面向特定客户的数据加密方面表现卓越。
您是否在大规模管理机密时遇到类似挑战?欢迎在下方评论中分享您的做法。