콘텐츠로 이동

Secrets Management

분류: Layer 3 - AWS 인프라 & 보안

시크릿(API 키, DB 비밀번호, 토큰 등)을 코드와 분리하여 안전하게 저장·주입·교체하는 체계.

하드코딩된 시크릿이 git에 올라가면 벌어지는 일

2019년 Toyota 자회사는 GitHub 공개 저장소에 AWS 키를 실수로 커밋했다가 5년치 고객 데이터(약 3.1만 명)를 탈취당했다. git에 시크릿이 올라간 순간, 자동화된 봇이 수 분 내에 해당 키를 스캔한다. git rm으로 파일을 삭제해도 git 히스토리에 영구적으로 남는다.

2024년 대규모 AWS 자격증명 탈취 사건 (규모로 보는 현실)

2024년 8월, Nemesis·ShinyHunters 해킹 그룹은 2.68억 개 IP 주소를 자동 스캔해 노출된 .env 파일과 설정 파일에서 AWS Access Key를 대량 수집했다. 피해 데이터는 2TB 이상이었으며, 탈취된 자격증명에는 AWS Access Key, GitHub 토큰, Twilio API Key, DB 비밀번호가 포함됐다. Palo Alto Networks는 별도 조사에서 9만 개 이상의 고유 환경변수가 노출된 .env 파일에서 유출되었음을 확인했다.

2025년 1월에는 Codefinger 랜섬웨어 그룹이 유출된 AWS 자격증명을 이용해 S3 버킷을 SSE-C(고객 제공 키)로 암호화한 뒤 복호화 키를 요구하는 새로운 유형의 공격이 등장했다. AWS가 직접 복호화를 도울 수 없어 피해 복구가 불가능한 사례가 발생했다.

핵심 수치: 자격증명이 GitHub에 올라가면 평균 5초 이내에 자동화된 봇이 탐지해 악용한다 (GitGuardian 측정). git rm 이후 히스토리 rewrite + 자격증명 즉시 교체가 필수다.

환경변수만으로 부족한 이유

  • ps aux 명령으로 프로세스 인자가 노출될 수 있다
  • 컨테이너 실행 로그, CloudWatch Logs에 환경변수 값이 찍힐 수 있다
  • .env 파일을 실수로 git에 올리는 사고가 빈번하다

플랫폼 엔지니어가 “다른 개발자의 시크릿 관리”를 설계해야 하는 이유

플랫폼 엔지니어는 개인 프로젝트 하나가 아닌 팀 전체의 인프라를 설계한다. 팀원 10명이 각자 .env 파일을 Slack으로 공유하는 방식이 굳어지기 전에, “Secrets Manager에서 주입받는” 올바른 패턴을 인프라 수준에서 강제해야 한다.

정적 시크릿 vs 동적 시크릿

구분예시특징
정적 시크릿API Key, DB 비밀번호만료 없이 영구 유효 → 유출 시 즉시 교체해야
동적 시크릿STS 임시 토큰, Vault가 생성하는 DB 자격증명TTL이 있어 자동 만료 → 유출 피해 최소화

유출 시 영향 범위 (Blast Radius)

  • AWS Access Key: 계정 전체 탈취 가능 (최악)
  • DB 비밀번호: 해당 DB의 모든 데이터 노출
  • OAuth Token: 연동된 서비스의 사용자 데이터 접근 가능
  • TLS 인증서 Private Key: 중간자 공격(MITM) 가능
  • SSH Key: 해당 서버 전체 접근 가능

비유: 은행 금고 + 도어맨

Secrets Manager는 시크릿을 “은행 금고(KMS)“에 보관하고, 꺼낼 때마다 “도어맨(IAM)“에게 허가를 받는 구조다. 키(KMS)와 시크릿(Secrets Manager)을 분리한 것이 핵심이다.

저장 구조: Envelope Encryption(봉투 암호화)

애플리케이션이 시크릿 요청
Secrets Manager API
IAM 권한 검증 (GetSecretValue 허용 여부)
KMS에 데이터 키(256-bit AES) 요청
데이터 키로 시크릿 값 복호화
평문 시크릿 반환

실제로 시크릿은 “암호화된 데이터 키 + 암호화된 시크릿 값”으로 저장된다. KMS 자체가 시크릿 값을 알지는 못하고, 오직 데이터 키만 암호화/복호화한다. 이것이 Envelope Encryption이다.

📖 더 보기: AWS KMS Envelope Encryption — KMS가 데이터를 직접 암호화하지 않는 이유 설명

자동 교체(Rotation): Lambda 기반 4단계 흐름

1. createSecret → 새 비밀번호 생성 + AWSPENDING 버전으로 저장
2. setSecret → DB에 접속해서 비밀번호 실제 변경
3. testSecret → 새 비밀번호로 DB 접속 테스트
4. finishSecret → AWSPENDING → AWSCURRENT 승격, 기존은 AWSPREVIOUS

왜 Lambda를 쓰는가? 시크릿 교체 로직은 서비스마다 다르다(RDS, Redshift, Elasticache 등). Lambda를 사용하면 교체 로직을 서비스별로 커스터마이징할 수 있다. AWS가 RDS, Redshift 등의 managed rotation을 제공하지만, 커스텀 서비스는 직접 Lambda를 작성해야 한다.

버저닝: AWSCURRENT / AWSPREVIOUS 스테이지

버전 ID 스테이지
abc123 → AWSCURRENT (현재 사용 중)
def456 → AWSPREVIOUS (직전 버전, 롤백용)
ghi789 → AWSPENDING (교체 진행 중)

교체 중 서비스 무중단을 위해 AWSCURRENT와 AWSPREVIOUS 두 버전이 동시에 유효하다. 교체 완료 전에는 기존 DB 연결이 AWSPREVIOUS로도 가능하다.

비용 구조

항목비용
시크릿 저장$0.40/개/월
API 호출$0.05/10,000건
교체용 LambdaLambda 요금 별도

실습: 시크릿 생성 및 조회

Terminal window
# 시크릿 생성
aws secretsmanager create-secret \
--name prod/myapp/db-password \
--description "Production DB password" \
--secret-string '{"username":"admin","password":"MyS3cur3Pass!"}'
# 예상 출력
{
"ARN": "arn:aws:secretsmanager:ap-northeast-2:123456789:secret:prod/myapp/db-password-AbCdEf",
"Name": "prod/myapp/db-password",
"VersionId": "a1b2c3d4-1234-5678-abcd-ef0123456789"
}
# 시크릿 조회
aws secretsmanager get-secret-value \
--secret-id prod/myapp/db-password \
--query SecretString \
--output text
# 예상 출력
{"username":"admin","password":"MyS3cur3Pass!"}

📖 더 보기: AWS Secrets Manager 공식 문서 - Rotation — Lambda 기반 교체 4단계 상세 설명

AWS Systems Manager Parameter Store — 차이점

섹션 제목: “AWS Systems Manager Parameter Store — 차이점”

비유: 금고 vs 서랍장

Secrets Manager는 강화 금고(자동 교체, 버저닝, 비용 있음), Parameter Store는 열쇠 달린 서랍장(단순 저장, 무료 티어 있음)이다.

파라미터 유형

유형암호화용도
StringX공개 설정값 (환경 이름, 리전 등)
StringListX쉼표 구분 목록
SecureStringO (KMS)시크릿 값 (Secrets Manager 대안)

Standard vs Advanced

StandardAdvanced
비용무료$0.05/파라미터/월
최대 크기4KB8KB
파라미터 정책XO (TTL, 알림)
스루풋40 TPS100 TPS

Secrets Manager vs Parameter Store 선택 기준

자동 교체가 필요한가? → YES → Secrets Manager
비용이 민감한가? → YES → Parameter Store SecureString
교차 계정 접근이 필요? → YES → Secrets Manager (리소스 정책 지원)
단순 설정값인가? → YES → Parameter Store String

계층형 경로 설계 예시

/prod/api/db-password
/prod/api/jwt-secret
/prod/cache/redis-url
/staging/api/db-password
/dev/api/db-password

IAM 정책에서 /prod/* 또는 /staging/*로 환경별 접근 제어 가능.

ECS Task Definition에서 시크릿 참조

{
"containerDefinitions": [
{
"name": "api",
"image": "my-api:latest",
"secrets": [
{
"name": "DB_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:ap-northeast-2:123456789:secret:prod/myapp/db-password-AbCdEf"
},
{
"name": "JWT_SECRET",
"valueFrom": "arn:aws:ssm:ap-northeast-2:123456789:parameter/prod/api/jwt-secret"
}
]
}
]
}

ECS 에이전트가 태스크 시작 시 Secrets Manager/Parameter Store에서 값을 가져와 컨테이너 환경변수로 주입한다. 애플리케이션 코드는 process.env.DB_PASSWORD로 접근하면 된다.

📖 더 보기: ECS에서 민감한 데이터 전달 — Task Definition secrets 필드 공식 설명

NestJS 애플리케이션에서 런타임 시크릿 주입 패턴

config/secrets.service.ts
import {
SecretsManagerClient,
GetSecretValueCommand,
} from "@aws-sdk/client-secrets-manager";
@Injectable()
export class SecretsService {
private client = new SecretsManagerClient({ region: "ap-northeast-2" });
async getSecret(secretId: string): Promise<Record<string, string>> {
const command = new GetSecretValueCommand({ SecretId: secretId });
const response = await this.client.send(command);
return JSON.parse(response.SecretString || "{}");
}
}
// app.module.ts — AppModule 초기화 시 시크릿 로드
async function bootstrap() {
const secretsService = new SecretsService();
const dbCreds = await secretsService.getSecret("prod/myapp/db-password");
// 환경변수로 설정 (ConfigService가 읽도록)
process.env.DB_PASSWORD = dbCreds.password;
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}

빌드타임 주입 vs 런타임 주입 차이

빌드타임 주입 (React REACT_APP_*)
- 번들 파일에 시크릿이 포함됨
- 브라우저에서 소스 보기로 노출 가능
- ❌ API Key 등 민감 정보에 절대 사용 금지
런타임 주입 (ECS secrets, 서버사이드)
- 컨테이너 메모리에만 존재
- 소스 코드/빌드 산출물에 포함 안 됨
- ✅ 모든 민감 정보는 런타임 주입

Vault는 AWS 서비스와 달리 온프레미스, 멀티클라우드 환경에서 동작한다. 가장 큰 차별점은 동적 시크릿이다: DB에 접근할 때마다 고유한 임시 계정을 생성하고, 사용 후 자동 삭제한다. TTL이 1시간짜리 DB 계정이 생성되므로 유출되어도 피해가 최소화된다.

AWS만 사용하는 환경이라면 Secrets Manager로 충분하다. Vault가 필요한 경우:

  • 멀티클라우드(AWS + GCP + Azure)에서 통합 시크릿 관리
  • 온프레미스 서버와 클라우드를 동시에 운영
  • 동적 시크릿이 필수인 고보안 환경

멀티클라우드 전이 매핑 — 핵심 원리는 플랫폼을 바꿔도 동일하다

섹션 제목: “멀티클라우드 전이 매핑 — 핵심 원리는 플랫폼을 바꿔도 동일하다”

시크릿 관리의 세 가지 핵심 원리(Envelope Encryption, Dynamic Secret, Blast Radius)는 AWS에만 존재하지 않는다. 어떤 클라우드/플랫폼을 쓰든 같은 문제를 같은 방식으로 푼다.

Envelope Encryption (봉투 암호화) 구현체 비교

원리AWSGCPAzureHashiCorp Vault
마스터 키 보관KMS CMKCloud KMS Key RingKey Vault KeyTransit Secret Engine
데이터 키 암호화KMS GenerateDataKeyCloud KMS CryptoKeyKey Vault WrapVault Transit Encrypt
시크릿 저장소Secrets ManagerSecret ManagerKey Vault SecretKV Secret Engine
BYOK 지원CMK(고객 관리 키)CMEKCustomer-Managed Key셀프호스팅 자체가 BYOK

핵심 패턴은 동일하다: 마스터 키는 HSM/KMS가 보관하고, 실제 데이터는 마스터 키로 암호화된 데이터 키로 암호화한다. KMS가 데이터를 직접 암호화하지 않는 이유는 모든 플랫폼에서 같다 — 대용량 데이터를 KMS로 보내는 건 느리고 비싸기 때문이다.

Dynamic Secret (동적 시크릿) 구현체 비교

원리구현체TTL 동작
IAM 임시 자격증명AWS STS AssumeRole15분~12시간, 만료 시 자동 폐기
Workload IdentityGCP Workload Identity FederationOIDC 토큰 기반, 단기 유효
Managed IdentityAzure Managed Identity플랫폼이 토큰 갱신 자동 처리
K8s Service AccountK8s Projected Volume TokenexpirationSeconds 설정, kubelet이 자동 교체
DB 동적 자격증명Vault Database Secrets EngineTTL 만료 시 DB 계정 자동 삭제

공통 원리: 자격증명에 TTL을 부여하면 유출 피해 창이 TTL로 제한된다. 정적 API Key는 만료가 없어 유출 즉시 조치가 없으면 영구 피해다.

Blast Radius (피해 반경) 축소 패턴

패턴AWS 구현동일 원리의 타 플랫폼 구현
최소 권한 스코프IAM Policy (Action/Resource 세분화)GCP IAM Conditions, Azure RBAC 조건부 역할
시크릿 범위 격리Secrets Manager 경로별 분리 (/prod/*)Vault Namespace, GCP Secret 프로젝트 격리
인증서 기반 인증ACM Private CA + mTLSVault PKI Engine, GCP Certificate Authority Service
환경별 자격증명 분리IAM Role per env (prod-role, dev-role)Vault Policy per env, GCP Service Account per env

📖 더 보기: Vault Transit Secrets Engine — Encryption-as-a-Service 원리 / GCP Secret Manager — GCP 공식 시크릿 저장소 / Vault K8s Secrets Engine — K8s 동적 Service Account Token

전체 시크릿 주입 아키텍처 (텍스트 다이어그램)

┌─────────────────────────────────────────────────────────┐
│ 시크릿 저장 계층 │
│ AWS Secrets Manager SSM Parameter Store │
│ (DB 비밀번호, API Key) (환경 설정, 기능 플래그) │
│ ↓ ↓ │
│ └─────────── KMS 암호화 ────┘ │
└─────────────────────────────────────────────────────────┘
↓ IAM Role 기반 접근
┌─────────────────────────────────────────────────────────┐
│ 시크릿 주입 계층 │
│ │
│ [ECS Task] ──secrets 필드──→ 컨테이너 환경변수 │
│ [Lambda] ──환경변수 참조──→ 함수 실행 시 주입 │
│ [GitHub Actions] ──OIDC AssumeRole──→ 런타임 조회 │
│ [NestJS App] ──SDK 직접 호출──→ 부트스트랩 시 로드 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 감사/모니터링 계층 │
│ CloudTrail: GetSecretValue 호출 로그 기록 │
│ CloudWatch: 비정상 호출 패턴 알람 │
│ AWS Config: 미암호화 파라미터 감사 │
└─────────────────────────────────────────────────────────┘

핵심 원칙: 시크릿은 절대 코드/이미지에 포함되지 않고, 런타임에 IAM Role 권한으로만 조회된다.

CI/CD 파이프라인에서 시크릿 주입

GitHub Actions에서 AWS 자격증명을 직접 저장하는 대신 OIDC로 AssumeRole하고, Secrets Manager에서 배포용 시크릿을 동적으로 가져온다.

.github/workflows/deploy.yml
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/GithubActionsDeployRole
aws-region: ap-northeast-2
- name: Get deployment secrets
run: |
DB_URL=$(aws secretsmanager get-secret-value \
--secret-id prod/myapp/db-url \
--query SecretString \
--output text)
echo "DB_URL=$DB_URL" >> $GITHUB_ENV

환경별 시크릿 분리

prod/myapp/db-password → 프로덕션
staging/myapp/db-password → 스테이징
dev/myapp/db-password → 개발

IAM 정책으로 개발자는 dev/*만, CI/CD는 prod/*만 접근하도록 최소 권한 적용.

마이크로서비스 간 인증 토큰 관리

서비스 A → 서비스 B 호출 시 공유하는 Internal API Key를 Secrets Manager에 저장하고, 두 서비스 모두 IAM Role로 접근하도록 설정. 수동으로 키를 교환하지 않아도 된다.

보안 TF: 기존 하드코딩된 시크릿 마이그레이션 — Step-by-step 시나리오

실제 업무에서 하드코딩된 시크릿을 Secrets Manager로 이관할 때 아래 순서를 따른다.

Phase 1: 탐색 (Discovery)

Terminal window
# 1-1. git 히스토리에서 민감 정보 패턴 스캔
git log -S "password" --oneline | head -20
git log -S "secret_key" --oneline | head -20
# 1-2. 현재 파일 전체에서 하드코딩 패턴 검색
grep -rn "password\s*=" --include="*.ts" --include="*.js" src/
grep -rn "AKIA[A-Z0-9]{16}" . # AWS Access Key 패턴
# 1-3. Amazon CodeGuru로 자동 시크릿 탐지 (AWS 콘솔)
# Security → CodeGuru → Detector Library → Secrets detection

Phase 2: 시크릿 등록 (Create)

Terminal window
# 2-1. 발견된 시크릿을 Secrets Manager에 등록
aws secretsmanager create-secret \
--name prod/myapp/db-password \
--secret-string '{"username":"admin","password":"기존비밀번호"}' \
--region ap-northeast-2
# 2-2. 기존 코드를 환경변수 참조로 교체 (중간 단계)
# 코드: const password = process.env.DB_PASSWORD
# ECS Task Definition에 secrets 필드 추가

Phase 3: 전환(Cutover) — 유지보수 윈도우에서 실행

Terminal window
# 3-1. 전환 전 현재 버전 태깅 (롤백 기준점)
aws secretsmanager tag-resource \
--secret-id prod/myapp/db-password \
--tags Key=migration-status,Value=cutover-$(date +%Y%m%d)
# 3-2. ECS 서비스 재배포 (새 Task Definition 적용)
aws ecs update-service \
--cluster prod-cluster \
--service myapp-service \
--force-new-deployment
# 3-3. CloudTrail로 실제로 Secrets Manager를 통해 시크릿이 조회되는지 확인
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=GetSecretValue \
--start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ) \
--query 'Events[].{Time: EventTime, User: Username, Source: EventSource}'

Phase 4: 기존 시크릿 폐기 (Revoke)

Terminal window
# 4-1. 전환 확인 후 기존 DB 비밀번호 교체 (Rotation 트리거)
aws secretsmanager rotate-secret \
--secret-id prod/myapp/db-password \
--rotation-rules AutomaticallyAfterDays=30
# 4-2. git-secrets hook 설치하여 재발 방지
brew install git-secrets && git secrets --install && git secrets --register-aws
# 4-3. 노출된 AWS Access Key는 즉시 비활성화
aws iam update-access-key \
--access-key-id AKIA... \
--status Inactive \
--user-name deploy-user

전환(Phase 3)은 반드시 저트래픽 시간대(새벽)에 실행하고, 롤백 플랜(이전 Task Definition 버전으로 복원)을 사전에 확인한다.

Terraform에서 Secrets Manager 리소스 관리

secrets.tf
resource "aws_secretsmanager_secret" "db_password" {
name = "prod/myapp/db-password"
recovery_window_in_days = 7
tags = {
Environment = "prod"
Service = "myapp"
}
}
resource "aws_secretsmanager_secret_version" "db_password" {
secret_id = aws_secretsmanager_secret.db_password.id
secret_string = jsonencode({
username = "admin"
password = var.db_password # terraform.tfvars에서 주입, 절대 코드에 하드코딩 금지
})
}
비교 항목Secrets ManagerParameter Store (SecureString)HashiCorp Vault
비용$0.40/시크릿/월무료(Standard)셀프호스팅 비용
자동 교체✅ 내장❌ 직접 구현✅ 내장
동적 시크릿
멀티클라우드❌ AWS 전용❌ AWS 전용
리소스 기반 정책
적합한 케이스DB 비밀번호, 교체 필요환경 설정, 단순 시크릿멀티클라우드, 온프레미스

KMS CMK vs AWS Managed Key

AWS Managed KeyCustomer Managed Key (CMK)
비용무료$1/월
교체 주기자동 (3년)설정 가능 (1년 권장)
접근 감사CloudTrailCloudTrail
사용 케이스기본값, 소규모컴플라이언스 요구, 세밀한 제어 필요

시크릿 vs 설정값: 어디까지가 시크릿인가

시크릿 (Secrets Manager / SecureString):
- DB 비밀번호, API Key, OAuth Client Secret
- Private Key, 인증서
- 내부 서비스 토큰
설정값 (Parameter Store String / 환경변수):
- 서비스 URL, 도메인
- 기능 플래그 (feature flags)
- 리전, 환경 이름 (prod/staging)

도구 선택 판단 트리 (실무 기준)

Q1. 자동 교체(Rotation)가 필요한가?
→ YES: Secrets Manager 확정
→ NO : Q2로 이동
Q2. 교차 계정(Cross-Account) 접근이 필요한가?
→ YES: Secrets Manager (리소스 기반 정책 지원)
→ NO : Q3로 이동
Q3. 시크릿인가, 설정값인가?
→ 시크릿 (DB 비밀번호, API Key): Parameter Store SecureString 또는 Secrets Manager
→ 설정값 (URL, 환경명): Parameter Store String (무료)
Q4. SecureString vs Secrets Manager 중 선택 시:
→ 시크릿 개수 < 10개: Secrets Manager ($4/월 이하)
→ 시크릿 개수 > 50개이고 교체 불필요: Parameter Store SecureString (비용 절감)
→ 규정 준수(Audit Trail, 버저닝) 필요: Secrets Manager
Q5. AWS 외 환경(온프레미스, 멀티클라우드)이 포함되는가?
→ YES: HashiCorp Vault 고려
→ NO : AWS 서비스로 충분

Rotation 전략: Single User vs Alternating User

전략동작 방식장점단점
Single User기존 DB 계정 비밀번호를 변경단순교체 중 기존 연결 끊김 위험
Alternating Useruser_a ↔ user_b 두 계정을 번갈아 사용무중단 교체 보장DB에 계정 2개 필요

Alternating User 흐름 (무중단 권장 패턴):

현재: user_a (AWSCURRENT) 활성 중
1. user_b 비밀번호 변경 + AWSPENDING에 저장
2. user_b 연결 테스트
3. user_b → AWSCURRENT 승격
4. user_a → AWSPREVIOUS (이전 연결들이 자연스럽게 교체됨)
다음 교체: user_a 비밀번호 변경 → user_a가 다시 AWSCURRENT로

기존 DB 연결 풀이 AWSPREVIOUS(user_a)로도 정상 동작하므로, 연결 재시도 없이 무중단 교체가 가능하다.

❌ AccessDeniedException: User is not authorized to perform secretsmanager:GetSecretValue

섹션 제목: “❌ AccessDeniedException: User is not authorized to perform secretsmanager:GetSecretValue”

증상

An error occurred (AccessDeniedException) when calling the GetSecretValue operation:
User: arn:aws:iam::123456789:user/deploy is not authorized to perform:
secretsmanager:GetSecretValue on resource: prod/myapp/db-password

원인

ECS Task Role, Lambda 실행 역할, 또는 IAM 사용자에게 secretsmanager:GetSecretValue 권한이 없거나, 리소스 ARN이 잘못 지정된 경우.

해결

// IAM Policy — 특정 시크릿에만 GetSecretValue 허용
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Resource": "arn:aws:secretsmanager:ap-northeast-2:123456789:secret:prod/myapp/*"
},
{
"Effect": "Allow",
"Action": "kms:Decrypt",
"Resource": "arn:aws:kms:ap-northeast-2:123456789:key/key-id"
}
]
}

CMK(고객 관리 키)를 사용하는 경우 kms:Decrypt 권한도 함께 필요하다는 점을 반드시 확인하자.


❌ ECS 태스크 시작 실패: unable to pull secrets

섹션 제목: “❌ ECS 태스크 시작 실패: unable to pull secrets”

증상

ECS 태스크가 시작되지 않고 다음 에러 발생:

ResourceInitializationError: unable to pull secrets or registry auth:
execution resource retrieval failed: unable to retrieve secret from ssm:
service call has been retried 5 time(s): RequestError: send request failed
caused by: Post "https://secretsmanager.ap-northeast-2.amazonaws.com/":
dial tcp: no such host

원인

Private 서브넷에서 실행 중인 ECS 태스크가 Secrets Manager 엔드포인트에 도달하지 못하는 경우. Private 서브넷은 기본적으로 인터넷 접근이 없으므로, NAT Gateway 또는 VPC Endpoint가 필요하다.

해결

Terminal window
# 방법 1: Secrets Manager용 VPC Endpoint 생성 (권장 - 비용 절감)
aws ec2 create-vpc-endpoint \
--vpc-id vpc-xxxxxxxx \
--service-name com.amazonaws.ap-northeast-2.secretsmanager \
--vpc-endpoint-type Interface \
--subnet-ids subnet-private-1 subnet-private-2 \
--security-group-ids sg-xxxxxxxx
# 방법 2: NAT Gateway를 통한 인터넷 접근 (이미 있다면 라우팅 확인)

❌ AWSPENDING 상태 고착 — Rotation이 완료되지 않고 멈춤 (Silent Failure)

섹션 제목: “❌ AWSPENDING 상태 고착 — Rotation이 완료되지 않고 멈춤 (Silent Failure)”

증상

자동 교체가 스케줄대로 트리거되었지만 AWSCURRENT가 갱신되지 않는다. 새 버전이 AWSPENDING에 묶인 채 다음 교체 시도 시 아래 에러가 반환된다.

An error occurred (ConflictException): An attempt was made to start a new
rotation while another rotation is already in progress for secret ...

또는 CloudTrail에서 RotationFailed 이벤트와 함께 다음 메시지가 나타난다:

Pending secret version VERSION_ID for Secret SECRET_ARN wasn't created
by Lambda LAMBDA_ARN. Remove the AWSPENDING staging label and restart rotation.

원인

  • Rotation Lambda가 setSecret / testSecret 단계에서 타임아웃 또는 DB 접속 실패로 중단됨
  • 2024년 12월 변경으로, Lambda가 PutSecretValue 호출 시 rotation token을 전달하지 않으면 Secrets Manager가 호출 주체 검증 실패 처리
  • 여러 Lambda 함수에 rotation 로직이 분산된 경우

확인 방법

Terminal window
# 현재 버전별 스테이지 확인 — AWSPENDING이 AWSCURRENT와 분리되어 있으면 고착 상태
aws secretsmanager describe-secret \
--secret-id prod/myapp/db-password \
--query 'VersionIdsToStages'
# 정상: {"abc123": ["AWSCURRENT"], "def456": ["AWSPREVIOUS"]}
# 고착: {"abc123": ["AWSCURRENT"], "def456": ["AWSPREVIOUS"], "ghi789": ["AWSPENDING"]}

해결

Terminal window
# 1. AWSPENDING 스테이지 레이블 제거
aws secretsmanager update-secret-version-stage \
--secret-id prod/myapp/db-password \
--version-stage AWSPENDING \
--remove-from-version-id ghi789
# 2. Rotation 재시도
aws secretsmanager rotate-secret --secret-id prod/myapp/db-password
# 3. Lambda 실행 로그에서 실패 원인 확인
aws logs filter-log-events \
--log-group-name /aws/lambda/rotation-function \
--start-time $(date -d '1 hour ago' +%s)000 \
--filter-pattern "ERROR"

📖 더 보기: AWS Secrets Manager Rotation 트러블슈팅 — AWSPENDING 고착 포함 공식 해결 가이드


❌ 런타임 캐시된 시크릿 만료 — Rotation 성공 후 앱이 조용히 실패 (Silent Failure)

섹션 제목: “❌ 런타임 캐시된 시크릿 만료 — Rotation 성공 후 앱이 조용히 실패 (Silent Failure)”

증상

Rotation이 성공적으로 완료되어 AWSCURRENT가 새 값으로 갱신됐는데도, 서비스에서 DB 연결 오류가 수 분~수 시간 뒤에 산발적으로 발생한다.

원인

애플리케이션이 부트스트랩 시 시크릿을 메모리에 캐싱하는 패턴을 사용할 때 발생한다. NestJS bootstrap() 에서 getSecret()으로 한 번 로드한 process.env.DB_PASSWORD는 프로세스 재시작 전까지 갱신되지 않는다. AWSPREVIOUS 유예 기간(기본 보장)이 끝나면 기존 비밀번호로의 DB 인증이 실패한다.

해결 패턴

// ❌ 문제 패턴: 부트스트랩 시 1회 로드 후 캐싱
process.env.DB_PASSWORD = await secretsService.getSecret(
"prod/myapp/db-password",
);
// ✅ 권장 패턴: TTL 기반 캐시 갱신 (rotation 주기보다 짧게 설정)
// rotation이 30일 주기이면 캐시 TTL을 1시간으로 설정
const CACHE_TTL_MS = 60 * 60 * 1000;
let cachedSecret: string | null = null;
let cacheExpiry = 0;
async function getSecretWithCache(secretId: string): Promise<string> {
if (Date.now() < cacheExpiry && cachedSecret) return cachedSecret;
cachedSecret = await fetchFromSecretsManager(secretId);
cacheExpiry = Date.now() + CACHE_TTL_MS;
return cachedSecret;
}

또는 Alternating User 패턴을 사용하면 AWSPREVIOUS가 AWSCURRENT 갱신 후에도 유효해 연결 끊김 없이 자연 갱신된다(위 섹션 6 참조).


❌ Rotation 실패: Lambda invocation failed

섹션 제목: “❌ Rotation 실패: Lambda invocation failed”

증상

CloudWatch Logs에서 다음 패턴 발견:

Found credentials in environment variables
(30초 후 아무 로그 없이 타임아웃)

원인

Rotation Lambda가 Private 서브넷에서 실행 중이고, Secrets Manager VPC Endpoint 또는 NAT Gateway가 없어서 API 호출이 실패한다. 또는 Lambda 실행 역할에 secretsmanager:* 권한이 없는 경우.

해결

  1. Lambda 함수 VPC 설정 확인: Secrets Manager Endpoint에 도달 가능한 서브넷/보안 그룹인지 확인
  2. Lambda 타임아웃 확인 (기본 3초 → 30초 이상으로 설정)
  3. Lambda 실행 역할에 다음 권한 추가:
{
"Action": [
"secretsmanager:DescribeSecret",
"secretsmanager:GetSecretValue",
"secretsmanager:PutSecretValue",
"secretsmanager:UpdateSecretVersionStage"
],
"Resource": "arn:aws:secretsmanager:*:*:secret:prod/*"
}

❌ 시크릿 참조가 빈 값으로 주입됨

섹션 제목: “❌ 시크릿 참조가 빈 값으로 주입됨”

증상

ECS 컨테이너에서 process.env.DB_PASSWORDundefined 또는 빈 문자열.

원인

Task Definition의 secrets 배열에서 ARN 끝에 버전 접미사가 포함되었거나, 잘못된 버전 스테이지를 참조한 경우. 또는 JSON 형태로 저장된 시크릿에서 특정 키만 추출하려 할 때 경로 지정 누락.

해결

// ❌ 잘못된 예 — 버전 ID 오타 또는 존재하지 않는 버전
"valueFrom": "arn:aws:secretsmanager:...:secret:prod/myapp/db-password-AbCdEf:1"
// ✅ 올바른 예 — ARN만 지정 (AWSCURRENT 자동 사용)
"valueFrom": "arn:aws:secretsmanager:...:secret:prod/myapp/db-password-AbCdEf"
// ✅ JSON에서 특정 키 추출
"valueFrom": "arn:aws:secretsmanager:...:secret:prod/myapp/db-password-AbCdEf:password::"
// ↑ JSON 키 ↑ 버전(빈=AWSCURRENT) ↑ 스테이지(빈=AWSCURRENT)

6.6 시크릿 유출 Incident Response 타임라인

섹션 제목: “6.6 시크릿 유출 Incident Response 타임라인”

시크릿이 유출됐을 때 가장 큰 실수는 영향 범위 파악보다 조용히 교체만 하는 것이다. 교체 전에 attacker가 이미 사용한 기록이 있는지 확인하지 않으면 사후 분석이 불가능해진다.

T+0 유출 감지 (GitGuardian 알림, CloudTrail 이상 탐지, 직접 발견)
└─ CloudTrail 조회로 해당 자격증명의 마지막 사용 시각과 IP 기록 먼저 확인
T+1m 즉시 차단 — 서비스 영향 최소화 순서로 revoke
├─ AWS Access Key: aws iam update-access-key --status Inactive
├─ DB 비밀번호: aws secretsmanager rotate-secret (즉시 교체)
└─ OAuth Token: 해당 서비스 콘솔에서 revoke
T+5m 영향 범위 파악 (Blast Radius 조사)
├─ CloudTrail: 유출된 자격증명으로 실행된 API 호출 목록 추출
│ aws cloudtrail lookup-events --lookup-attributes AttributeKey=AccessKeyId,AttributeValue=AKIA...
├─ 접근된 S3 버킷, RDS 인스턴스, 기타 리소스 목록 확인
└─ 외부 노출 여부: GitHub, Pastebin 등에서 키 문자열 검색
T+30m 사후 분석 및 재발 방지
├─ git-secrets / trufflehog로 전체 히스토리 재스캔
├─ 유출 경로 확인 (CI/CD 로그 노출, .env 커밋, 코드 하드코딩 등)
└─ 내부 팀 공유 — 같은 자격증명을 다른 서비스가 사용 중인지 확인
T+1h 이해관계자 통보 (NIST SP 800-61 권장)
└─ 영향받은 고객 데이터가 있다면 법적 통보 의무 검토

핵심 원칙: 차단(revoke) 전에 CloudTrail 스냅샷을 먼저 저장한다. 자격증명을 비활성화하면 attacker 활동이 멈추지만, 그 전에 로그를 확인하지 않으면 무엇이 접근됐는지 파악하기 어렵다. 단, 확인에 너무 오래 걸리면 피해가 커지므로 1분 내 차단이 원칙이다.

📖 더 보기: HashiCorp Well-Architected — Remediate Leaked Secrets — 유출 후 단계별 remediation 가이드 / GCP 자격증명 침해 대응 — GCP 공식 playbook

  • 코드/git에 하드코딩된 시크릿이 없는가?

    Terminal window
    # git 히스토리 전체 스캔
    git log -S "password" --oneline | head -20
    git log -S "secret" --oneline | head -20
    # 현재 파일 스캔
    grep -rn "password\s*=" --include="*.ts" src/
  • git-secrets pre-commit hook이 설치되어 있는가?

    Terminal window
    git secrets --list # 등록된 패턴 확인
    # 출력 없으면 미설치 → brew install git-secrets && git secrets --install && git secrets --register-aws
  • 시크릿에 접근하는 IAM 정책이 최소 권한인가? (secretsmanager:GetSecretValue만)

    Terminal window
    # ECS Task Role 정책 확인
    aws iam list-role-policies --role-name ecsTaskRole
    aws iam get-role-policy --role-name ecsTaskRole --policy-name SecretsPolicy
    # Action에 secretsmanager:* 같은 와일드카드가 없는지 확인
  • 자동 교체가 설정되어 있는가? (최소 DB 비밀번호)

    Terminal window
    aws secretsmanager describe-secret \
    --secret-id prod/myapp/db-password \
    --query '{RotationEnabled: RotationEnabled, NextRotationDate: NextRotationDate}'
    # RotationEnabled: false 이면 미설정
  • 시크릿 접근 로그가 CloudTrail에 기록되는가?

    Terminal window
    aws cloudtrail lookup-events \
    --lookup-attributes AttributeKey=EventName,AttributeValue=GetSecretValue \
    --start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ) \
    --max-results 5
    # 결과가 비어있으면 CloudTrail Trail이 비활성화된 것
  • 환경별(dev/staging/prod) 시크릿이 분리되어 있는가?

    Terminal window
    aws secretsmanager list-secrets \
    --query 'SecretList[].Name' --output table
    # prod/, staging/, dev/ 경로가 각각 존재하는지 확인
  • Private 서브넷의 ECS 태스크가 VPC Endpoint 또는 NAT Gateway를 통해 Secrets Manager에 접근 가능한가?

    Terminal window
    # VPC Endpoint 존재 여부 확인
    aws ec2 describe-vpc-endpoints \
    --filters Name=service-name,Values=com.amazonaws.ap-northeast-2.secretsmanager \
    --query 'VpcEndpoints[].{State: State, VpcId: VpcId}'
    # 결과 없으면 NAT Gateway 라우팅 또는 VPC Endpoint 생성 필요
  • git-secrets: pre-commit hook으로 시크릿 커밋 방지. AWS Labs 공식 도구.
  • AWS KMS Envelope Encryption: KMS가 데이터를 직접 암호화하지 않고 데이터 키를 암호화하는 패턴
  • SOPS (Mozilla): 파일 전체가 아닌 값만 암호화하는 설정 파일 암호화 도구. git에 암호화된 상태로 커밋 가능.
  • Sealed Secrets (Kubernetes): K8s Secret을 암호화하여 git에 커밋 가능하게 하는 컨트롤러
  • AWS IAM Roles Anywhere: 온프레미스 서버에서도 IAM Role 기반 임시 자격증명 사용 가능
  • 현재 프로젝트에서 시크릿이 어떻게 관리되고 있는지 점검
Terminal window
# git 히스토리에서 민감 정보 검색
git log --all --full-history -- "**/.env*"
git log -S "password" --oneline | head -20
  • Secrets Manager에 시크릿 하나 생성 → ECS 태스크에서 참조
Terminal window
# 1. 시크릿 생성
aws secretsmanager create-secret \
--name test/myapp/db-password \
--secret-string '{"username":"testuser","password":"TestPass123!"}'
# 예상 출력
{
"ARN": "arn:aws:secretsmanager:ap-northeast-2:ACCOUNT_ID:secret:test/myapp/db-password-XXXXXX",
"Name": "test/myapp/db-password",
"VersionId": "uuid-here"
}
# 2. 시크릿 확인
aws secretsmanager get-secret-value \
--secret-id test/myapp/db-password \
--query SecretString --output text
# 예상 출력
{"username":"testuser","password":"TestPass123!"}
  • Lambda 기반 자동 교체 설정해보기
Terminal window
# RDS MySQL 자동 교체 활성화 (30일마다)
aws secretsmanager rotate-secret \
--secret-id prod/myapp/db-password \
--rotation-rules AutomaticallyAfterDays=30
# 교체 상태 확인
aws secretsmanager describe-secret \
--secret-id prod/myapp/db-password \
--query '{RotationEnabled: RotationEnabled, NextRotationDate: NextRotationDate}'
# 예상 출력
{
"RotationEnabled": true,
"NextRotationDate": "2026-05-08T00:00:00+00:00"
}
  • git-secrets hook 설치하여 커밋 전 검사 적용
Terminal window
# git-secrets 설치 (macOS)
brew install git-secrets
# 현재 repo에 훅 설치
git secrets --install
# AWS 자격증명 패턴 등록
git secrets --register-aws
# 테스트
echo "AKIA1234567890ABCDEF" > test-key.txt
git add test-key.txt
git commit -m "test"
# 예상 출력: error: Matched one or more prohibited patterns
  1. 시크릿은 코드와 반드시 분리 — git에 올라가면 히스토리에 영구적으로 남고, 봇이 수 분 내에 탐지한다
  2. AWS에서는 Secrets Manager(자동 교체 필요, $0.40/월) vs Parameter Store(단순 저장, 무료) 선택
  3. 시크릿 주입은 ECS Task Definition secrets 필드로, 접근 권한은 IAM Role로 통제 — 절대 환경변수에 직접 값 입력 금지
  4. 자동 교체(Rotation)는 Lambda 4단계(createSecret → setSecret → testSecret → finishSecret) 흐름으로 무중단 교체
  5. 시크릿 접근은 CloudTrail로 추적하고, Private 서브넷에서 접근 시 VPC Endpoint 설정 필수

프론트 개발자 시절 경험과 시크릿 관리의 연결

React 개발 시절에도 비슷한 문제를 마주쳤을 것이다: REACT_APP_API_KEY=my-secret-key.env에 넣고 빌드하면, 그 값이 번들된 JavaScript 파일에 그대로 포함된다. 브라우저 개발자 도구에서 Sources 탭을 열면 누구나 볼 수 있다.

[프론트엔드 환경변수 — 위험]
.env → React 빌드 → bundle.js 안에 평문으로 포함
브라우저 → 개발자 도구 → Sources → bundle.js → API_KEY 검색 → 노출!
[서버사이드 시크릿 — 안전]
Secrets Manager → ECS Task 시작 시 메모리에 주입
프로세스 메모리 → process.env.DB_PASSWORD (외부 접근 불가)

핵심 원칙: 민감한 정보는 절대 클라이언트(브라우저)에 내려가면 안 된다. React 앱이 API를 직접 호출할 때 API Key가 필요하다면, 그 API Key를 프론트에 넣는 게 아니라 NestJS 백엔드가 프록시 역할을 하도록 구조를 바꿔야 한다.

❌ 잘못된 구조:
React → 외부 API (API Key를 React 코드에 포함)
✅ 올바른 구조:
React → NestJS API (인증만)
NestJS → 외부 API (API Key는 서버 메모리에만 존재, Secrets Manager에서 주입)

환경 변수 파일 (.env)의 한계

로컬 개발에서 .env 파일은 편리하지만, 여러 사람이 공유하는 프로젝트에서는 한계가 있다:

문제.env 파일Secrets Manager
git 커밋 실수자주 발생구조적으로 불가
새 팀원 온보딩Slack으로 전달 (위험)IAM 권한 부여
시크릿 교체모든 서버 수동 변경자동 교체 + 즉시 반영
감사(Audit)불가능CloudTrail 자동 기록

플랫폼 엔지니어로서의 역할: 팀원이 “안전한 방법이 더 편리한” 환경을 만드는 것. .env 파일을 공유하는 것보다 Secrets Manager IAM 권한 받는 게 더 쉽도록 프로세스를 설계한다.

실무 아키텍처 패턴 — 시크릿 라이프사이클

섹션 제목: “실무 아키텍처 패턴 — 시크릿 라이프사이클”
[시크릿 생성 단계]
플랫폼 엔지니어 → Secrets Manager에 시크릿 등록
→ IAM Policy로 접근 가능한 Role 지정
→ 자동 교체 일정 설정 (30~90일)
[런타임 주입 단계]
ECS Task 시작
→ ECS 에이전트가 Execution Role로 Secrets Manager 호출
→ IAM + KMS 권한 확인
→ 시크릿 복호화 (Envelope Encryption)
→ 컨테이너 환경변수로 주입 (메모리)
NestJS 코드: process.env.DB_PASSWORD (Secrets Manager 모름)
[교체 단계 (30일마다 자동)]
Lambda Rotation 함수:
createSecret → 새 비밀번호 생성
setSecret → RDS에 새 비밀번호 적용
testSecret → 새 비밀번호로 연결 테스트
finishSecret → 새 버전 AWSCURRENT 승격
ECS Task 재시작 시 자동으로 새 시크릿 로드
(애플리케이션 코드 변경 없음)
[감사 단계]
모든 GetSecretValue 호출 → CloudTrail 자동 기록
비정상 접근 패턴 → CloudWatch Alarm → SNS 알림

2025년 업데이트 — Secrets Manager 배치 조회 지원: 여러 시크릿을 한 번의 API 호출로 조회할 수 있는 BatchGetSecretValue API가 안정화됐다. 마이크로서비스에서 10개 시크릿을 개별 조회하면 10번의 API 호출이 발생하지만, 배치 조회를 사용하면 1번으로 줄여 지연 시간과 비용을 모두 절감할 수 있다.