Secrets Manager vs 하드코딩: DB 비밀번호를 코드에 넣으면 안 되는 이유

레포지토리가 private이니까 괜찮다고 생각했던 DB 비밀번호가 GitHub 유출 사고의 주인공이 되는 건 순식간이다. Secrets Manager를 왜 써야 하는지, 자동 로테이션이 실제로 어떻게 동작하는지 운영 관점에서 정리한다.

TL;DR: 핵심 비교

항목하드코딩 / 환경변수AWS Secrets Manager
비밀번호 노출 경로코드, 로그, CI/CD 환경변수API 호출 시점에만 메모리 접근
로테이션수동 (배포 필요)자동 (Lambda 기반, 무중단)
감사 추적없음CloudTrail로 모든 접근 기록
접근 제어코드 접근 권한 = 비밀번호 접근IAM 정책으로 세분화
비용0원 (보안 사고 비용 제외)시크릿당 월 과금 — AWS 공식 문서 확인

왜 Secrets Manager가 필요한가 — 하드코딩의 실제 위협 모델

'private 레포니까 괜찮다'는 가정이 깨지는 시나리오는 생각보다 많다. 팀원 계정 탈취, 실수로 public 전환, CI/CD 파이프라인 로그 노출, 컨테이너 이미지 레이어 분석 — 이 중 하나라도 발생하면 비밀번호가 코드에 있는 순간 게임 오버다.

환경변수도 완전한 해결책이 아니다. docker inspect, /proc/<pid>/environ, 애플리케이션 에러 로그에 환경변수가 그대로 출력되는 사례는 실제 운영 환경에서 반복적으로 발생한다. 비밀번호를 '어디에 두느냐'가 아니라 '누가, 언제, 어떤 경로로 접근하느냐'를 제어하는 것이 핵심이다.

하드코딩은 자물쇠 없는 금고다. 금고가 지하실에 있다고 안전한 게 아니다 — 지하실 문이 열리는 순간 모든 게 노출된다.

Secrets Manager 동작 원리

Secrets Manager는 단순한 키-값 저장소가 아니다. 시크릿 값 자체는 AWS KMS로 암호화되어 저장되고, 애플리케이션은 런타임에 GetSecretValue API를 호출해서 복호화된 값을 받는다. 이 흐름을 이해해야 로테이션이 왜 무중단으로 가능한지 납득이 된다.

graph LR App["애플리케이션"] -->|"GetSecretValue API 호출"| SM["Secrets Manager"] SM -->|"복호화 요청"| KMS["AWS KMS"] KMS -->|"복호화된 값 반환"| SM SM -->|"시크릿 값 반환"| App App -->|"DB 연결"| DB["RDS / DB"] SM -->|"모든 API 호출 기록"| CT["CloudTrail"] IAM["IAM Role
(인스턴스/태스크)"] -->|"자격증명 제공"| App
  1. 애플리케이션 → Secrets Manager API: 런타임에 GetSecretValue 호출. 코드에는 시크릿 이름(ARN 또는 이름 문자열)만 존재한다.
  2. IAM 인증: EC2 인스턴스 프로파일, ECS Task Role, Lambda 실행 역할 등 서비스 자격증명으로 인증. 별도 키 관리 불필요.
  3. KMS 복호화: Secrets Manager가 KMS를 호출해 시크릿 값을 복호화한 뒤 애플리케이션에 반환.
  4. CloudTrail 기록: 모든 GetSecretValue 호출이 CloudTrail에 기록된다. 누가 언제 어떤 시크릿에 접근했는지 추적 가능.

자동 로테이션이 실제로 동작하는 방식

로테이션의 핵심은 Lambda 함수다. Secrets Manager가 직접 DB 비밀번호를 바꾸는 게 아니라, 로테이션 로직을 담은 Lambda를 트리거하는 구조다. 이 Lambda가 새 비밀번호 생성 → DB 업데이트 → Secrets Manager 값 갱신 → 검증의 4단계를 수행한다.

sequenceDiagram participant SM as Secrets Manager participant L as 로테이션 Lambda participant DB as RDS DB SM->>L: 로테이션 트리거 (createSecret) L->>SM: 새 비밀번호를 AWSPENDING으로 저장 SM->>L: setSecret 호출 L->>DB: 새 비밀번호로 DB 계정 업데이트 SM->>L: testSecret 호출 L->>DB: AWSPENDING 값으로 연결 테스트 DB-->>L: 연결 성공 SM->>L: finishSecret 호출 L->>SM: AWSPENDING → AWSCURRENT 승격 Note over SM: 기존 값은 AWSPREVIOUS로 보존
  1. createSecret: Lambda가 새 비밀번호 후보를 생성하고 AWSPENDING 스테이징 레이블로 저장.
  2. setSecret: DB에 새 비밀번호를 실제로 적용. 이 시점에 기존 비밀번호(AWSCURRENT)도 아직 유효하다.
  3. testSecret: AWSPENDING 값으로 DB 연결 테스트. 실패 시 롤백.
  4. finishSecret: AWSPENDINGAWSCURRENT로 승격. 기존 값은 AWSPREVIOUS로 보존 (짧은 전환 기간 동안 유효).

애플리케이션이 커넥션 풀을 사용한다면, 로테이션 직후 기존 연결이 AWSPREVIOUS 비밀번호로 유지되다가 재연결 시 AWSCURRENT를 사용하게 된다. 이 전환 구간 때문에 로테이션이 무중단으로 동작한다.

Secrets Manager 설정 — 단계별 구현

1단계: 시크릿 생성

먼저 DB 자격증명을 Secrets Manager에 저장한다. JSON 형식으로 저장하면 애플리케이션에서 파싱이 편하다.

aws secretsmanager create-secret \
  --name prod/myapp/db-password \
  --description "Production RDS credentials for myapp" \
  --secret-string '{"username":"myapp_user","password":"initial-password-here"}' \
  --region us-east-1

시크릿 이름은 경로 형식(prod/myapp/db-password)으로 관리하면 IAM 정책에서 경로 기반 접근 제어가 가능해진다.

2단계: 애플리케이션 IAM 권한 설정

EC2, ECS, Lambda 등 애플리케이션 실행 역할에 최소 권한을 부여한다. GetSecretValue만 허용하고, 특정 시크릿 ARN으로 범위를 제한하는 것이 원칙이다.

🔽 IAM 정책 예시 (클릭하여 펼치기)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "GetAppSecret",
      "Effect": "Allow",
      "Action": "secretsmanager:GetSecretValue",
      "Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/myapp/db-password-*"
    },
    {
      "Sid": "AllowKMSDecrypt",
      "Effect": "Allow",
      "Action": [
        "kms:Decrypt",
        "kms:GenerateDataKey"
      ],
      "Resource": "arn:aws:kms:us-east-1:123456789012:key/"
    }
  ]
}

시크릿 ARN 끝에 -*를 붙이는 이유가 있다. Secrets Manager는 시크릿 생성 시 ARN 끝에 6자리 랜덤 접미사를 자동으로 추가한다. 정확한 ARN을 모르는 상태에서 와일드카드로 처리하거나, 생성 후 확인한 정확한 ARN을 사용하면 된다.

3단계: 애플리케이션 코드 수정 (Python 예시)

코드에서 비밀번호를 직접 참조하는 부분을 API 호출로 교체한다. 커넥션 풀 초기화 시 한 번 호출하고 캐싱하는 패턴이 일반적이다 — 매 쿼리마다 API를 호출하면 레이턴시와 비용이 증가한다.

🔽 Python boto3 예시 (클릭하여 펼치기)
import boto3
import json
import psycopg2

def get_db_credentials(secret_name: str, region_name: str) -> dict:
    client = boto3.client('secretsmanager', region_name=region_name)
    response = client.get_secret_value(SecretId=secret_name)
    return json.loads(response['SecretString'])

# 애플리케이션 시작 시 한 번 호출
creds = get_db_credentials('prod/myapp/db-password', 'us-east-1')

conn = psycopg2.connect(
    host='your-rds-endpoint.us-east-1.rds.amazonaws.com',
    database='myapp_db',
    user=creds['username'],
    password=creds['password']
)

로테이션 이후 커넥션 오류가 발생하면 get_db_credentials를 재호출하는 재시도 로직을 추가하는 것이 좋다. 로테이션 완료 직후 기존 커넥션이 끊기는 짧은 구간이 존재할 수 있다.

4단계: 자동 로테이션 활성화

로테이션을 활성화하려면 실제 로테이션 로직을 수행할 Lambda 함수가 먼저 존재해야 한다. Lambda ARN 없이 로테이션 명령만 실행하면 동작하지 않는다.

로테이션 Lambda를 준비하는 두 가지 방법:

  • 방법 A (권장 — RDS 사용 시): AWS 콘솔에서 해당 시크릿의 '로테이션 구성' 탭으로 이동 → '자동 로테이션 활성화' 선택 → RDS 데이터베이스를 대상으로 지정하면 AWS가 로테이션용 Lambda 함수를 자동으로 생성하고 연결해준다. 이 방법이 가장 간단하며, 생성된 Lambda ARN을 콘솔에서 확인할 수 있다.
  • 방법 B (커스텀 DB 또는 수동 설정): AWS Serverless Application Repository에서 SecretsManagerRDSPostgreSQLRotationSingleUser 등 DB 엔진에 맞는 애플리케이션을 검색해 배포한다. 배포 후 생성된 Lambda 함수의 ARN을 아래 CLI 명령에 사용한다.

Lambda ARN을 확인한 뒤 CLI로 로테이션을 활성화한다:

# 로테이션용 Lambda 함수가 이미 생성되어 있어야 합니다.
# <ROTATION_LAMBDA_ARN> 부분을 실제 Lambda 함수 ARN으로 대체하세요.
# (콘솔 자동 생성 또는 Serverless Application Repository 배포 후 확인)
aws secretsmanager rotate-secret \
  --secret-id prod/myapp/db-password \
  --rotation-lambda-arn <ROTATION_LAMBDA_ARN> \
  --rotation-rules AutomaticallyAfterDays=30 \
  --region us-east-1

이 명령은 로테이션 설정을 저장하고 즉시 첫 번째 로테이션을 트리거한다. 이후 30일마다 자동으로 반복된다. Lambda 함수에는 Secrets Manager가 호출할 수 있도록 리소스 기반 정책도 자동으로 추가된다.

5단계: 로테이션 상태 확인

로테이션이 정상 동작하는지 확인한다. LastRotatedDateRotationEnabled 값을 체크하면 된다.

aws secretsmanager describe-secret \
  --secret-id prod/myapp/db-password \
  --region us-east-1 \
  --query '{RotationEnabled:RotationEnabled,LastRotatedDate:LastRotatedDate,NextRotationDate:NextRotationDate}'

실제 운영에서 마주치는 문제 — 오진과 실제 원인

증상: 로테이션 활성화 후 애플리케이션에서 간헐적으로 DB 인증 오류 발생. CloudWatch 로그에 password authentication failed가 찍힌다.

첫 번째 오진: 로테이션 Lambda가 실패했다고 가정하고 Lambda 로그부터 뒤진다. Lambda 실행 자체는 성공으로 기록되어 있어서 원인을 찾지 못한다.

실제 원인: 애플리케이션이 시작 시 비밀번호를 한 번 읽어서 메모리에 캐싱한 뒤, 커넥션 풀이 끊기지 않는 한 재조회하지 않는 구조였다. 로테이션으로 AWSCURRENT가 갱신됐지만 애플리케이션은 여전히 이전 비밀번호로 연결을 시도했다.

수정: DB 인증 오류 발생 시 Secrets Manager를 재조회하는 재시도 로직 추가. 커넥션 풀 라이브러리의 reconnect-on-auth-failure 옵션이 있다면 활성화한다.

import time

def get_connection_with_retry(secret_name: str, region_name: str, max_retries: int = 2):
    for attempt in range(max_retries):
        try:
            creds = get_db_credentials(secret_name, region_name)
            return psycopg2.connect(
                host='your-rds-endpoint.us-east-1.rds.amazonaws.com',
                database='myapp_db',
                user=creds['username'],
                password=creds['password']
            )
        except psycopg2.OperationalError as e:
            if 'password authentication failed' in str(e) and attempt < max_retries - 1:
                time.sleep(1)
                continue
            raise

로테이션 주기와 커넥션 풀 수명을 함께 고려하지 않으면 이 문제는 반드시 발생한다 — 로테이션 자체가 아니라 애플리케이션 캐싱 전략이 문제다.

Secrets Manager vs Parameter Store — 언제 무엇을 쓸까

기준Secrets ManagerSSM Parameter Store (SecureString)
자동 로테이션지원 (Lambda 기반)미지원 (수동 구현 필요)
주요 용도DB 자격증명, API 키, OAuth 토큰설정값, 비민감 파라미터, 환경별 설정
비용시크릿당 과금Standard tier 무료, Advanced 과금
크로스 계정 접근리소스 기반 정책 지원제한적

비밀번호나 API 키처럼 주기적으로 교체해야 하는 자격증명은 Secrets Manager, 환경 설정값이나 feature flag처럼 교체 주기가 없는 값은 Parameter Store가 적합하다.

Secrets Manager 도입 마무리 및 다음 단계

하드코딩 제거는 보안의 시작이지 끝이 아니다. Secrets Manager를 도입했다면 다음 단계로 넘어가야 한다.

  • CloudTrail에서 GetSecretValue 이벤트를 모니터링하고 비정상 접근 패턴에 알림 설정
  • 시크릿별 리소스 정책으로 크로스 계정 접근 제어
  • KMS 키 로테이션 활성화 (Secrets Manager 로테이션과 별개)
  • VPC 엔드포인트 설정으로 Secrets Manager API 호출이 인터넷을 경유하지 않도록 구성

공식 문서: AWS Secrets Manager 로테이션 가이드 | Secrets Manager 모범 사례

핵심 용어 정리

용어설명
AWSCURRENT현재 활성화된 시크릿 버전 레이블. 애플리케이션이 기본으로 조회하는 값.
AWSPENDING로테이션 진행 중 새 비밀번호가 임시로 저장되는 스테이징 레이블.
AWSPREVIOUS로테이션 완료 직후 이전 비밀번호가 보존되는 레이블. 전환 기간 동안 유효.
리소스 기반 정책시크릿 자체에 부착하는 정책. 크로스 계정 접근 제어에 사용.
KMS CMK시크릿 암호화에 사용되는 고객 관리형 KMS 키. 기본값은 AWS 관리형 키.

Related Posts

댓글

이 블로그의 인기 게시물

EC2 SSH 연결 타임아웃 완전 해결 가이드: Security Group 인바운드 규칙부터 라우팅까지

EC2 SSH 연결 시간 초과: 확인해야 할 보안 그룹(Security Group) 규칙

IAM User vs IAM Role 차이점 완전 정리 — EC2에서 S3 접근 시 무엇을 써야 하는가