서버리스 앱에서 비밀 정보 저장하기

비밀 정보를 다룰 때 일반적인 원칙은 코드베이스 외부에 저장하고 Git에 커밋하지 않는 것입니다. 그리고 런타임에 이 정보를 사용할 수 있도록 하는 것이 중요합니다. 이를 구현하는 방법은 다양하지만, 일부 방법은 다른 방법들보다 보안 측면에서 취약합니다. 이번 장에서는 여러 환경에서 비밀 정보를 저장하고 관리하는 최선의 방법을 설명하겠습니다.

AWS 파라미터 스토어에 비밀 저장하기

AWS Systems Manager 파라미터 스토어 (SSM)는 설정 데이터와 비밀을 키-값 쌍으로 중앙에 저장할 수 있는 AWS 서비스입니다. 값은 일반 텍스트나 암호화된 데이터로 저장할 수 있습니다. 암호화된 데이터로 저장할 경우, 값은 쓰기 시 AWS KMS 키를 사용해 암호화되고, 읽기 시 복호화됩니다.

예시로, SSM을 사용해 Stripe 비밀 키를 저장해 보겠습니다. Stripe는 라이브 키와 테스트 키 두 가지를 제공합니다. 이 키들을 다음과 같이 저장할 것입니다:

  • 라이브 키는 프로덕션 계정의 SSM 콘솔에 저장
  • 테스트 키는 개발 계정의 SSM 콘솔에 저장

먼저 프로덕션 계정으로 이동해 Systems Manager 콘솔로 들어갑니다.

Systems Manager 서비스 선택

왼쪽 메뉴에서 파라미터 스토어를 선택하고 파라미터 생성을 클릭합니다.

파라미터 스토어에서 파라미터 생성 선택

다음 정보를 입력합니다:

  • 이름: /stripeSecretKey/live
  • 설명: Stripe 비밀 키 - 라이브

파라미터 스토어에서 파라미터 세부 정보 설정

SecureString을 선택하고, 라이브 Stripe 키를 에 붙여넣습니다.

SecureString 파라미터 타입 선택

아래로 스크롤하여 파라미터 생성을 클릭합니다.

파라미터 스토어에서 파라미터 생성

키가 추가되었습니다.

파라미터 생성 완료 스크린샷

그런 다음, 개발 계정으로 전환하고, 테스트 Stripe 키를 추가하기 위해 동일한 단계를 반복합니다. 이때 다음 정보를 사용합니다:

  • 이름: /stripeSecretKey/test
  • 설명: Stripe 비밀 키 - 테스트

개발 계정에서 파라미터 생성

Lambda에서 SSM 파라미터 접근하기

이제 SSM 파라미터를 사용하려면 Lambda 함수가 어떤 환경에서 실행 중인지 알 수 있도록 해야 합니다. Lambda 함수에 스테이지 이름을 환경 변수로 전달할 것입니다.

serverless.yml을 업데이트합니다.

...


custom: ${file(../../serverless.common.yml):custom}

provider:
  environment:
    stage: ${self:custom.stage}
    resourcesStage: ${self:custom.resourcesStage}
    notePurchasedTopicArn:
      Ref: NotePurchasedTopic

  iamRoleStatements:
    - ${file(../../serverless.common.yml):lambdaPolicyXRay}
    - Effect: Allow
      Action:
        - ssm:GetParameter
      Resource:
        !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/stripeSecretKey/*'
...

Lambda 함수가 SSM 파라미터를 가져오고 복호화할 수 있는 권한을 부여합니다.

다음으로 config.js에 파라미터 이름을 추가합니다.

const stage = process.env.stage;
const adminPhoneNumber = "+14151234567";

const stageConfigs = {
  dev: {
    stripeKeyName: "/stripeSecretKey/test"
  },
  prod: {
    stripeKeyName: "/stripeSecretKey/live"
  }
};

const config = stageConfigs[stage] || stageConfigs.dev;

export default {
  stage,
  adminPhoneNumber,
  ...config
};

위 코드는 환경 변수 process.env.stage에서 현재 스테이지를 읽고, 해당하는 설정을 선택합니다.

  • 스테이지가 prod이면 stageConfigs.prod를 내보냅니다.
  • 스테이지가 dev이면 stageConfigs.dev를 내보냅니다.
  • 스테이지가 featureX와 같은 경우, dev 설정으로 대체되어 stageConfigs.dev를 내보냅니다.

설정 구조에 대해 다시 확인하려면 환경 관련 설정 관리를 참조하세요.

이제 Lambda 함수에서 SSM 값을 접근할 수 있습니다.

import AWS from '../../libs/aws-sdk';
import config from "../../config";

// SSM에서 비밀 키 로드
const ssm = new AWS.SSM();
const stripeSecretKeyPromise = ssm
  .getParameter({
    Name: config.stripeKeyName,
    WithDecryption: true
  })
  .promise();

export const handler = (event, context) => {
  ...

  // Stripe를 통해 결제
  const stripeSecretKey = await stripeSecretKeyPromise;
  const stripe = stripePackage(stripeSecretKey.Parameter.Value);
  ...
};

WithDecryption: true와 함께 ssm.getParameter를 호출하면, 반환된 값이 복호화되어 바로 사용할 수 있습니다.

비밀을 복호화하는 작업은 Lambda 함수 핸들러 외부에서 수행해야 합니다. 이는 Lambda 함수 호출당 한 번만 수행하면 되기 때문입니다. 첫 번째 호출에서 비밀을 복호화하고, 이후 호출에서는 동일한 값을 사용합니다.

요약하면, 민감한 데이터는 SSM에 저장합니다. SSM 파라미터 이름은 설정 파일에 저장합니다. Lambda 함수가 실행될 때, 실행 중인 환경을 기반으로 사용할 SSM 파라미터를 결정합니다. 마지막으로, Lambda 핸들러 함수 외부에서 비밀을 복호화합니다.

다음으로, 환경 간에 API Gateway 커스텀 도메인을 설정하는 방법을 살펴보겠습니다.