DynamoDB와 KMS를 이용한 민감한 정보 저장

발행: (2025년 12월 24일 오후 10:10 GMT+9)
14 분 소요
원문: Dev.to

I’m happy to translate the article for you, but I’ll need the full text of the article (the paragraphs, headings, etc.) in order to do so. Could you please paste the content you’d like translated? I’ll keep the source link at the top exactly as you’ve shown and preserve all formatting, code blocks, URLs, and technical terms.

소개

최근 AWS EventBridge Connections와 관련된 문제를 겪었습니다. 이 서비스는 비밀 정보를 관리해 주는 AWS 관리형 서비스로, API에 대한 인증을 설정하면(본인용이든 고객용이든, 예: 웹훅) EventBridge Connections가 EventBridge API Destination이나 Step Functions HTTP invoke 작업에 연결될 때 나머지를 처리해 줍니다.

두 서비스 모두 처음에는 매우 훌륭해 보이지만, 단순한 사용 사례를 넘어가면 제한 사항이 드러납니다. 제 경우에는 커스터마이징과 제어가 부족한 점이 큰 장애물이 되었습니다. 그래서 대안을 찾아보게 되었습니다: 고객이 제공한 비밀 정보나 민감 데이터를 안전하게 저장할 수 있는 방법은 어디인가?

명백한 선택: 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, API 호출 10,000회당 $0.05. 많은 고객 비밀을 관리하는 애플리케이션에서는 비용이 빠르게 증가합니다.
단순 사용 사례에 과도함자동 회전이나 고급 기능이 필요하지 않다면 사용하지 않을 기능에 대해 비용을 지불하게 됩니다.
복잡성간단한 암호화 요구 사항에 대해서는 서비스가 불필요한 오버헤드를 추가합니다.

이때 **AWS Key Management Service (KMS)**가 매력적인 대안이 됩니다.

Source:

더 나은 선택: 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에 민감한 데이터를 저장할 때는 두 가지 주요 접근 방식이 있습니다:

  1. DynamoDB에 참조만 저장 – 비밀을 암호화하고 AWS Secrets Manager(또는 SSM Parameter Store)에 저장한 뒤, 그 참조를 DynamoDB에 저장합니다.
  2. 암호화된 데이터를 직접 DynamoDB에 저장 – KMS를 사용해 민감한 데이터를 암호화하고, 암호화된 값을 그대로 DynamoDB 테이블에 저장합니다.

두 번째 접근 방식은 많은 사용 사례에서 더 간단하고 비용 효율적입니다. 이제 이를 구현하는 방법을 살펴보겠습니다.

AWS CDK로 KMS 설정

Below is a minimal CDK stack that creates a KMS key and a DynamoDB table, then grants a Lambda function permission to encrypt/decrypt data.

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 키
  • 웹훅 비밀
  • 짧은 OAuth 토큰

더 큰 페이로드에는 작동하지 않습니다:

  • PEM 인증서
  • 대형 JSON 자격 증명
  • 다중 필드 구성 블롭

대형 페이로드를 위한 봉투 암호화

(원본 내용은 여기서 끊깁니다; 이어서 봉투 암호화, 데이터 키 생성, 데이터 키로 페이로드 암호화, 암호화된 데이터 키를 암호문과 함께 저장하는 방법에 대해 설명합니다.)

비밀

4 KB보다 큰 비밀은 Envelope Encryption을 사용해야 합니다:

  • KMS를 사용하여 데이터‑암호화 키(DEK) 생성
  • 대칭 알고리즘(예: AES‑256‑GCM)으로 비밀을 로컬에서 암호화
  • 암호화된 비밀 암호화된 데이터 키를 DynamoDB에 함께 저장
  • 필요할 때만 KMS로 데이터 키를 복호화

왜 Envelope Encryption인가?

  • 임의 크기의 비밀을 처리할 수 있음
  • KMS API 호출을 최소화
  • AWS가 권장하는 모범 사례

이 문서에서는 단순함과 작은 비밀을 위해 직접 Encrypt / Decrypt 접근 방식에 초점을 맞춥니다. 큰 페이로드를 다루는 프로덕션 시스템에서는 대신 Envelope Encryption을 사용해야 합니다.

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: The Trade‑offs

당신은 다음과 같은 고민을 할 수 있습니다: SSM Parameter Store를 KMS 암호화와 함께 사용해야 할까, 아니면 데이터를 직접 KMS로 암호화해서 DynamoDB에 저장해야 할까?

SSM Parameter Store 접근 방식

장점

  • 중앙 집중식 비밀 관리
  • 내장 버전 관리
  • 무료 티어: 최대 10 000 개 파라미터
  • 많은 AWS 서비스와 통합

단점

  • 추가 API 호출 (SSM + DynamoDB)
  • 지연 시간 증가
  • 관리해야 할 서비스가 두 개
  • 10 000 개 파라미터 제한이 규모가 커질수록 제약이 될 수 있음

예시

// Store in SSM, reference in DynamoDB
const paramName = `/customers/${customerId}/secrets/${secretId}`;
await ssm.putParameter({
  Name: paramName,
  Value: plainSecret,
  Type: 'SecureString', // Uses KMS encryption
  KeyId: KMS_KEY_ID,
});

// Store reference in DynamoDB
await dynamodb.putItem({
  TableName: 'Customers',
  Item: {
    customerId: { S: customerId },
    secretRef: { S: paramName }, // Just the reference
  },
});

DynamoDB에서 직접 KMS 암호화

장점

  • 단일 서비스 (DynamoDB)
  • 낮은 지연 시간 (두 번이 아닌 한 번의 API 호출)
  • 파라미터 수 제한 없음
  • 더 간단한 아키텍처

단점

  • 내장 버전 관리가 없음 (직접 구현해야 함)
  • AWS 콘솔에서 가시성이 낮음
  • 수동 회전 관리 필요

언제 어떤 것을 사용할까

  • 버전 관리 필요 – 필요하다면 자체 버전 관리 로직을 구현해야 합니다.
  • 고객이 제공하고 고객이 소유/회전하는 비밀 – 회전이 외부에서 처리되므로 DynamoDB에서 직접 KMS 암호화만으로도 충분한 경우가 많습니다.

핵심 요점: 작업에 맞는 도구를 선택하세요.

  • Secrets Manager는 회전이 필요한 애플리케이션 비밀에 적합합니다.
  • KMS는 대량의 고객별 데이터 암호화에 뛰어납니다.

규모에 맞게 비밀을 관리하는 데 비슷한 어려움을 겪어본 적이 있나요? 아래 댓글로 여러분의 접근 방식을 공유해 주세요.

Back to Blog

관련 글

더 보기 »