coredot.today
AWS Secrets Manager 완전 정복: 비밀번호를 코드에 넣지 않는 법
블로그로 돌아가기
Secrets ManagerAWS보안비밀번호Parameter Store환경변수

AWS Secrets Manager 완전 정복: 비밀번호를 코드에 넣지 않는 법

GitHub에 DB 비밀번호가 올라갔다. 그 순간 모든 것이 시작된다. 환경변수부터 AWS Parameter Store, Secrets Manager까지 — 비밀 관리의 단계별 진화를 코드와 함께 풀어본다. 자동 로테이션, Lambda 통합, 최소 권한 원칙까지.

코어닷투데이2026-03-0345

들어가며: "커밋 한 줄이 회사를 날릴 뻔한 이야기"

모니터에 비밀번호가 적힌 포스트잇이 붙어있고 뒤에서 해커가 엿보는 위험한 상황을 보여주는 일러스트

금요일 저녁 7시. 신입 개발자 J는 첫 프로젝트의 마감을 앞두고 있었다. Django 앱을 AWS에 배포해야 하는데, 로컬에서는 잘 돌아가던 코드가 서버에서 DB 연결이 안 됐다. 급한 마음에 settings.py에 이렇게 적었다:

hljs language-python
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'HOST': 'prod-db.cluster-abc123.ap-northeast-2.rds.amazonaws.com',
        'NAME': 'myapp_production',
        'USER': 'admin',
        'PASSWORD': 'SuperSecret!2026',  # TODO: 나중에 환경변수로 바꾸기
    }
}

배포가 급했다. "월요일에 고치지 뭐." J는 git add .을 하고 커밋했다. 그리고 푸시했다.

그 "월요일"은 오지 않았다. 토요일 새벽 3시, 알 수 없는 IP에서 RDS 인스턴스에 대량 접속이 시작됐다. 일요일 오전, 프로덕션 데이터베이스의 모든 테이블이 삭제되고, 이런 메시지가 남아 있었다:

"Your database has been backed up. Send 0.5 BTC to restore."

GitHub 레포지토리가 퍼블릭이었다. 봇이 GitHub의 새 커밋을 실시간으로 스캔하여 AWS 키, DB 비밀번호, API 토큰을 탈취하는 데는 30초도 걸리지 않는다.

이 이야기는 픽션이 아니다. 매일 일어나는 현실이다.

  • 2016년, Uber: GitHub에 커밋된 AWS 자격 증명이 탈취되어 5,700만 명의 개인정보가 유출. $148M 벌금.
  • 2019년, Capital One: 잘못 구성된 자격 증명에서 시작된 사고로 1억 600만 명 데이터 유출.
  • 2024년, GitHub 자체 보고: 한 해 동안 퍼블릭 레포에서 3,900만 개 이상의 시크릿이 탐지됨.
!
문제
개발자의 72%가 한 번 이상 코드에 시크릿을 하드코딩한 경험이 있다 (GitGuardian 2025). Git 히스토리에 남으면 커밋을 삭제해도 복구 가능.
*
해결
비밀번호, API 키, 토큰을 **전용 시크릿 관리 서비스**에 저장하고, 앱은 런타임에 API로 가져온다.
O
결과
코드에 비밀번호가 없으니 Git에 올려도 안전. 비밀번호가 유출되면 **자동 로테이션**으로 즉시 교체.

1. 하드코딩된 시크릿: 가장 위험한 안티패턴

왜 사람들은 시크릿을 코드에 넣는가

답은 간단하다 — 편하니까. 로컬에서 config.py에 DB 비밀번호를 넣으면 바로 동작한다. 환경변수를 설정하거나 외부 서비스를 연동하는 것은 "나중에 할 일"이 된다.

사고가 터지는 경로

하드코딩 → 데이터 유출 경로공격 시나리오
1개발자가 비밀번호를 소스 코드에 직접 입력
2git commit && git push — Git 히스토리에 영구 기록
3자동화 봇이 GitHub 이벤트 스트림을 실시간 스캔 (<30초)
4탈취된 자격 증명으로 DB/클라우드 리소스에 무단 접근 → 데이터 유출

핵심 문제 3가지:

  1. Git 히스토리는 영원하다git rm으로 파일을 삭제해도 git log에 기록이 남는다.
  2. 비밀번호 교체가 불가능하다 — 코드를 수정하고 재배포해야 하니, 같은 비밀번호를 몇 년씩 쓰게 된다.
  3. 환경 분리가 안 된다 — 로컬/스테이징/프로덕션이 같은 코드를 쓰는데, 환경별로 다른 값을 넣기 어렵다.

.env 파일의 함정

".env 파일에 넣으면 되지 않나?" — 하드코딩보다는 낫다. 하지만 .gitignore에 추가하지 않으면 역시 Git에 올라가고, 팀원 간에 슬랙이나 Confluence로 공유하는 경우가 많다. .env는 로컬 개발에서만 사용하고, 프로덕션에서는 전용 서비스를 써야 한다.


2. 환경변수: 첫 번째 단계, 하지만 충분하지 않다

2011년, Heroku의 Twelve-Factor App 방법론은 "설정을 환경변수에 저장하라"고 권장했다. 코드에서 시크릿을 분리하는 첫 번째 단계다.

hljs language-python
import os
db_password = os.environ['DB_PASSWORD']

이것만으로도 큰 발전이다. 하지만 환경변수만으로는 부족하다:

시크릿 관리 방법별 보안 성숙도 (10점 만점)
하드코딩
1점
.env 파일
3점
환경변수
5점
Parameter Store
7점
Secrets Manager
9.5점

환경변수의 한계:

  • 암호화 없음 — OS 메모리에 평문 저장. printenv로 확인 가능
  • 감사 로그 없음 — 누가 언제 읽었는지 추적 불가
  • 자동 로테이션 없음 — 모든 서버를 수동으로 변경해야 함
  • 접근 제어 조잡 — 서버 접속 = 모든 환경변수 읽기 가능

3. AWS Parameter Store: 무료로 시작하는 설정 관리

AWS Systems Manager Parameter Store는 설정 데이터와 시크릿을 계층적으로 저장하는 서비스다. 기본 사용은 무료이며, AWS에서 가장 먼저 접하게 되는 시크릿 관리 도구다.

Parameter Store 핵심 특징
계층적 저장 /prod/db/password, /staging/api/key 같은 경로 기반 구조
KMS 암호화 SecureString 타입은 AWS KMS로 자동 암호화
무료 (Standard) Standard 파라미터 10,000개까지 무제한 무료

파라미터 타입

타입암호화용도
String없음설정값, URL, 기능 플래그
StringList없음서버 목록, 허용 IP
SecureStringAWS KMS비밀번호, API 키, 토큰

실습: 저장하고 가져오기

hljs language-bash
# SecureString으로 DB 비밀번호 저장
aws ssm put-parameter \
  --name "/myapp/prod/db/password" \
  --value "SuperSecret!2026" \
  --type SecureString
hljs language-python
import boto3

ssm = boto3.client('ssm', region_name='ap-northeast-2')

response = ssm.get_parameter(
    Name='/myapp/prod/db/password',
    WithDecryption=True
)
db_password = response['Parameter']['Value']

Parameter Store의 한계

  • 자동 로테이션 없음 — 비밀번호 자동 교체 기능이 없다
  • 교차 계정 공유 제한 — Advanced만 교차 계정 접근 가능
  • 처리량 제한 — Standard는 초당 40 요청이 상한

4. AWS Secrets Manager: 시크릿 관리의 완성형

은행 금고처럼 생긴 AWS Secrets Manager 볼트에서 API 키와 비밀번호를 안전하게 보관하는 모습

AWS Secrets Manager는 시크릿의 전체 생명주기를 관리하는 전용 서비스다. Parameter Store가 "설정 관리 도구 + 시크릿 저장"이라면, Secrets Manager는 시크릿만을 위해 설계된 전용 서비스다.

애플리케이션
API 호출
Secrets Manager
복호화
AWS KMS
CloudTrail
감사 로그
Secrets Manager
자동 실행
Lambda (로테이션)

핵심 특징

Secrets Manager의 5가지 핵심 가치
자동 로테이션 Lambda 함수로 비밀번호를 주기적으로 자동 교체. RDS, Redshift, DocumentDB 네이티브 지원
256-bit AES 암호화 AWS KMS 키로 저장 시 자동 암호화
완전한 감사 추적 CloudTrail로 누가, 언제, 어떤 시크릿을 읽었는지 모두 기록
버전 관리 AWSCURRENT, AWSPREVIOUS 레이블로 즉시 롤백 가능
세밀한 접근 제어 IAM 정책 + 리소스 기반 정책으로 시크릿별 권한 관리

시크릿의 구조

Secrets Manager의 시크릿은 단순한 키-값이 아니다. JSON 구조를 저장할 수 있어서, DB 연결에 필요한 모든 정보를 하나로 묶는다:

Secrets Manager에 저장된 RDS 시크릿
시크릿 이름
prod/myapp/rds-credentials
시크릿 값 (JSON)
username: "admin" / password: "aB3$kL9mN2pQ7wX5"
engine: "postgres" / host: "prod-db.abc123.rds.amazonaws.com"
port: 5432 / dbname: "myapp_production"
메타데이터
Rotation: 30일마다 자동 교체 / Last Rotated: 2026-03-15

5. Parameter Store vs Secrets Manager: 비교

Parameter StoreSecrets Manager
주 용도설정 관리 + 간단한 시크릿시크릿 전용 관리
자동 로테이션없음 (수동 구현 필요)내장. Lambda 기반 자동 교체
암호화SecureString만 KMS 암호화모든 시크릿 기본 KMS 암호화
교차 계정 공유Advanced만 가능리소스 기반 정책으로 쉽게 공유
교차 리전 복제없음다중 리전 자동 복제
비용Standard: 무료$0.40/시크릿/월 + API 호출 비용
API 처리량Standard: 40 TPS기본 5,000 TPS
RDS 네이티브 통합없음RDS, Redshift, DocumentDB 로테이션
Parameter Store 환경별 설정값, 기능 플래그, 엔드포인트 URL — 변경 빈도 낮고 자동 교체 불필요한 값
어느 쪽이든 API 키, 서드파티 토큰 — 중요하지만 자동 로테이션이 당장 불필요한 경우
Secrets Manager DB 비밀번호, 마스터 키 — 자동 로테이션, 교차 계정/리전 공유가 필요한 경우

실무 팁: 많은 팀이 두 서비스를 함께 사용한다. 설정값은 Parameter Store에, 민감한 자격 증명은 Secrets Manager에 저장하는 것이 가장 흔한 패턴이다.


6. 실습: 시크릿 저장하고 가져오기

AWS CLI로 시크릿 생성

hljs language-bash
aws secretsmanager create-secret \
  --name "prod/myapp/db-credentials" \
  --secret-string '{
    "username": "admin",
    "password": "aB3$kL9mN2pQ7wX5",
    "engine": "postgres",
    "host": "prod-db.cluster-abc123.rds.amazonaws.com",
    "port": 5432,
    "dbname": "myapp_production"
  }'

Python에서 가져오기

hljs language-python
import json
import boto3
from botocore.exceptions import ClientError

def get_secret(secret_name: str, region: str = 'ap-northeast-2') -> dict:
    """AWS Secrets Manager에서 시크릿을 가져온다."""
    client = boto3.client('secretsmanager', region_name=region)
    try:
        response = client.get_secret_value(SecretId=secret_name)
    except ClientError as e:
        code = e.response['Error']['Code']
        if code == 'ResourceNotFoundException':
            raise ValueError(f"시크릿 '{secret_name}'을 찾을 수 없습니다.")
        elif code == 'AccessDeniedException':
            raise PermissionError("접근 권한이 없습니다.")
        raise
    return json.loads(response['SecretString'])

# 사용 예시
db = get_secret('prod/myapp/db-credentials')

from sqlalchemy import create_engine
engine = create_engine(
    f"postgresql://{db['username']}:{db['password']}"
    f"@{db['host']}:{db['port']}/{db['dbname']}"
)

Node.js에서 가져오기

hljs language-javascript
import {
  SecretsManagerClient,
  GetSecretValueCommand,
} from '@aws-sdk/client-secrets-manager';

const client = new SecretsManagerClient({ region: 'ap-northeast-2' });

async function getSecret(secretName) {
  const response = await client.send(
    new GetSecretValueCommand({ SecretId: secretName })
  );
  return JSON.parse(response.SecretString);
}

// 사용 예시
const db = await getSecret('prod/myapp/db-credentials');

import pg from 'pg';
const pool = new pg.Pool({
  host: db.host,
  port: db.port,
  database: db.dbname,
  user: db.username,
  password: db.password,
});

캐싱으로 성능 최적화

매번 API를 호출하면 지연과 비용이 발생한다. AWS 공식 캐싱 라이브러리를 사용하자:

hljs language-python
from aws_secretsmanager_caching import SecretCache, SecretCacheConfig

cache = SecretCache(config=SecretCacheConfig(
    secret_refresh_interval=300  # 5분 캐싱
))

secret_string = cache.get_secret_string('prod/myapp/db-credentials')
db = json.loads(secret_string)

7. 자동 로테이션: 비밀번호가 스스로 바뀌는 마법

로봇 열쇠공이 정기적으로 서버 문의 자물쇠를 자동 교체하는 시크릿 로테이션 과정 일러스트

왜 자동 로테이션인가

비밀번호가 유출되었더라도, 이미 만료된 상태라면 피해를 막을 수 있다. 하지만 수동으로 매월 모든 DB 비밀번호를 바꾸는 것은 현실적으로 불가능하다. Secrets Manager는 이를 Lambda 함수 하나로 자동화한다.

4단계 로테이션 프로세스

createSecret Lambda가 새 비밀번호를 생성하여 AWSPENDING 레이블로 저장. 현재 비밀번호는 그대로 유효.
setSecret Lambda가 RDS에 접속하여 비밀번호를 새 값으로 변경.
testSecret 새 비밀번호로 DB 연결 테스트. 실패하면 롤백.
finishSecret AWSPENDINGAWSCURRENT 승격. 이전 값은 AWSPREVIOUS로 이동.

핵심은 무중단(zero-downtime)이다. 새 비밀번호가 테스트를 통과한 후에야 공식 값으로 승격된다.

RDS 로테이션 설정 (CLI)

hljs language-bash
aws secretsmanager rotate-secret \
  --secret-id "prod/myapp/db-credentials" \
  --rotation-lambda-arn "arn:aws:lambda:ap-northeast-2:123456789:function:SecretsManagerRDSRotation" \
  --rotation-rules '{"AutomaticallyAfterDays": 30}'

CDK로 설정 (프로덕션 권장)

hljs language-python
from aws_cdk import aws_secretsmanager as sm, Duration

db_secret = sm.Secret(self, "DBSecret",
    secret_name="prod/myapp/db-credentials",
    generate_secret_string=sm.SecretStringGenerator(
        secret_string_template='{"username": "admin"}',
        generate_string_key="password",
        password_length=32,
    ),
)

db_secret.add_rotation_schedule("Rotation",
    automatically_after=Duration.days(30),
    hosted_rotation=sm.HostedRotation.postgre_sql_single_user(),
)

싱글 유저 vs 멀티 유저 로테이션

싱글 유저멀티 유저 (교대)
동작하나의 DB 사용자 비밀번호를 변경두 DB 사용자를 번갈아가며 사용
순단 가능성비밀번호 변경 순간 짧은 단절 가능무중단 — 항상 하나는 유효
적합한 환경개발, 소규모 서비스프로덕션, 고가용성 필수

8. 서비스 통합: Lambda, ECS, EC2에서 시크릿 사용

Lambda

Lambda Extensions을 사용하면 코드 변경 없이 시크릿을 로컬 HTTP 캐시로 가져올 수 있다:

hljs language-bash
# Lambda 함수에 Extension 레이어 추가
aws lambda update-function-configuration \
  --function-name my-function \
  --layers "arn:aws:lambda:ap-northeast-2:044395824272:layer:AWS-Parameters-and-Secrets-Lambda-Extension:12"

Extension 없이 직접 호출도 가능하다:

hljs language-python
import json, boto3

secrets = boto3.client('secretsmanager')

def lambda_handler(event, context):
    response = secrets.get_secret_value(SecretId='prod/myapp/db-credentials')
    db = json.loads(response['SecretString'])
    # DB 작업 수행 ...

ECS (Fargate/EC2)

ECS는 Task Definition에서 시크릿을 직접 참조한다. 컨테이너 시작 시 환경변수로 자동 주입되므로, 시크릿 관리 코드를 아예 작성하지 않아도 된다:

hljs language-json
{
  "containerDefinitions": [{
    "name": "myapp",
    "image": "123456789.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:latest",
    "secrets": [
      {
        "name": "DB_PASSWORD",
        "valueFrom": "arn:aws:secretsmanager:ap-northeast-2:123456789:secret:prod/myapp/db-credentials:password::"
      }
    ]
  }]
}

앱에서는 os.environ['DB_PASSWORD']로 읽으면 된다.

EC2

EC2는 인스턴스 프로파일(IAM Role)을 통해 접근한다. AWS Access Key를 직접 설정하지 않고, SDK가 인스턴스 메타데이터에서 임시 자격 증명을 자동으로 획득한다.

LambdaECSEC2
시크릿 주입코드 호출 or ExtensionTask Def 참조 (환경변수)코드에서 API 호출
캐싱Extension 자동 캐싱시작 시 1회 주입앱에서 직접 구현
코드 변경최소~없음없음필요

9. 베스트 프랙티스 10가지

1. 최소 권한 원칙

hljs language-json
{
  "Effect": "Allow",
  "Action": "secretsmanager:GetSecretValue",
  "Resource": "arn:aws:secretsmanager:ap-northeast-2:123456789:secret:prod/myapp/*"
}

secretsmanager:* 와일드카드 금지. 읽기만 필요한 서비스에 삭제 권한까지 주면 사고 범위가 커진다.

2. CloudTrail 감사 + 알림

모든 API 호출이 CloudTrail에 기록된다. 비정상적인 GetSecretValue 폭증, 예상치 못한 DeleteSecret, RotationFailed 이벤트에 CloudWatch Alarm을 설정한다.

3. 환경 접두사로 이름 짓기

/prod/myapp/db-credentials
/staging/myapp/db-credentials
/dev/myapp/db-credentials

IAM 정책에서 Resource: "arn:...:prod/*"로 환경별 접근 제어가 가능해진다.

4. 평문 노출 절대 금지

!
하지 말 것
시크릿 값을 로그에 출력, 에러 메시지에 포함, printenv 덤프, Slack/이메일로 공유
*
해야 할 것
필요한 코드에서만 변수로 참조. 로깅 시 반드시 마스킹: "password": "****"

5. 자동 로테이션 주기

  • DB 비밀번호: 30일
  • API 키: 60~90일
  • KMS 마스터 키: 365일 (자동)

6. VPC 엔드포인트 사용

기본적으로 Secrets Manager API는 인터넷을 경유한다. VPC 엔드포인트를 생성하면 AWS 내부 네트워크만으로 통신하여 보안이 강화된다.

7. 리소스 기반 정책으로 교차 계정 공유

중앙 보안 계정에서 시크릿을 관리하고, 애플리케이션 계정에서 접근하는 것이 엔터프라이즈 패턴이다.

8. 삭제 보호

시크릿 삭제 시 기본 30일 복구 기간이 있다. 최소 7일 유지를 권장한다.

9. 태깅 전략

태그 키값 예시용도
Environmentprod, staging, dev환경 구분
Applicationmyapp소속 서비스
Ownerplatform-team관리 주체

태그 기반 IAM(ABAC)으로 "Environment=prod인 시크릿은 SRE팀만 접근" 같은 규칙을 만들 수 있다.

10. Git Pre-commit Hook

시크릿이 코드에 들어가는 것 자체를 막자:

hljs language-bash
brew install git-secrets   # macOS
git secrets --register-aws
git secrets --install
# 이후 커밋 시 AWS 키 포함되면 자동 차단

10. 비용과 실전 아키텍처

비용

월간 비용 비교 — 시크릿 50개 기준 (USD)
Parameter Store (Standard)
$0
Parameter Store (Advanced)
$2.50
Secrets Manager
~$25

**25/로데이터유출사고(평균피해액25/월**로 데이터 유출 사고(평균 피해액 4.88M — IBM 2024)를 예방할 수 있다면? 이건 보험이다.

실전 아키텍처

개발자 (IaC)
배포
Secrets Manager
시크릿 주입
ECS Fargate
CloudTrail
모니터링
Secrets Manager
30일 로테이션
RDS PostgreSQL

이 아키텍처에서 코드에는 단 하나의 시크릿도 존재하지 않는다. GitHub에 레포를 퍼블릭으로 공개해도 안전하다.


마치며: "TODO: 나중에 고치기"는 없다

이 글의 첫 부분에서 소개한 신입 개발자 J의 이야기로 돌아가 보자. settings.py에 남긴 # TODO: 나중에 환경변수로 바꾸기 주석 — 그 "나중에"가 오기 전에 사고가 터졌다.

보안에서 "나중에"는 없다. 하지만 다행히도, 지금 시작하는 것은 어렵지 않다.

오늘 git-secrets 설치. 코드에 하드코딩된 시크릿 검사. .env.gitignore에 있는지 확인.
이번 주 프로덕션 DB 비밀번호를 Secrets Manager로 이전. 설정값은 Parameter Store로.
이번 달 RDS 자동 로테이션 설정. CloudTrail 알림 구성. VPC 엔드포인트 생성.
습관 새 프로젝트를 시작할 때 첫 번째로 시크릿 관리를 설정한다. "나중에"가 아니라 "처음부터".

비밀번호를 코드에 넣는 것은 집 열쇠를 우편함 위에 올려놓는 것과 같다. 대부분의 날은 괜찮을 수도 있다. 하지만 한 번의 사고가 모든 것을 바꾼다.

AWS Secrets Manager는 그 열쇠를 금고에 넣어주는 서비스다. 금고는 잠겨 있고, 열쇠는 주기적으로 바뀌며, 누가 금고를 열었는지 기록이 남는다. 월 $25도 안 되는 비용으로.

코드에서 시크릿을 지우자. 오늘.


참고 자료