새 서비스를 급히 띄우며 DB 비밀번호를 config.js에 적고, AWS 키를 환경변수로 박아 도커 이미지에 구웠습니다. 몇 달 뒤 그 이미지가 외부 레지스트리에 잘못 공개됐고, 안에 박힌 키로 누군가 자원을 휘저었습니다. "코드에는 비밀 없음"이라 믿었지만, 비밀은 코드·이미지·깃·로그 어디에나 새어 있었습니다. 비밀을 '코드 밖'에 두는 것은 선택이 아니라 기본기입니다.
- 1비밀을 코드·깃·이미지에 두면 안 되는 이유를 설명할 수 있다
- 2시크릿 매니저가 보관·접근통제·회전·감사를 어떻게 해주는지 안다
- 3KMS의 봉투 암호화 개념과 '마스터 키는 밖으로 안 나간다'를 이해한다
- 4비밀 회전이 유출 피해를 제한하는 원리를 안다
- 5앱이 실행 시점에 권한으로 비밀을 받아오는 흐름을 그릴 수 있다
비밀은 코드 밖에 둔다
왜 하드코딩이 사고의 지름길인가
비밀번호·API 키·토큰을 코드나 깃에 박으면, 저장소 접근 = 비밀 유출입니다. 특히 깃은 이력을 보존하므로 나중에 그 줄을 지워도 과거 커밋에 그대로 남습니다 — 회수하려면 히스토리 재작성 + 키 폐기·회전이 필요합니다(Git 기본 흐름).
환경변수도 안전지대가 아닙니다. 이미지에 구워지거나, 프로세스 목록·크래시 로그·에러 트래킹에 노출될 수 있습니다. 비밀은 전용 보관소에 두고, 실행 시점에 권한으로만 받아오는 것이 원칙입니다.
확대
위 그림처럼 하드코딩된 비밀은 코드·Git 히스토리·이미지에 남아 영구 노출됩니다. 시크릿 매니저를 사용하면 코드에는 ARN만 남고, 앱이 런타임에 안전하게 주입받습니다.
확대
위 그림처럼 마스터 키는 KMS 내 HSM을 떠나지 않고, 데이터 키를 암호화하는 데만 씁니다. 대용량 데이터는 빠른 데이터 키로 암호화해 효율성과 보안을 동시에 얻습니다.
확대
위 그림처럼 자동 회전은 스케줄에 따라 새 비밀을 생성·갱신·저장하고, 앱은 항상 최신 버전을 참조합니다. 유출돼도 유효 기간이 제한되므로 피해 범위를 줄일 수 있습니다.
시크릿 매니저 — 보관·통제·회전·감사
KMS와 봉투 암호화 — 마스터 키는 나가지 않는다
KMS(Key Management Service)는 암호화 키를 관리합니다. 핵심 기법이 봉투 암호화입니다:
- 데이터는 빠른 데이터 키로 암호화
- 그 데이터 키 자체를 KMS의 마스터 키로 암호화해 데이터 옆에 저장
- 복호화 때 KMS가 데이터 키를 풀어주고, 그 키로 데이터를 푼다
마스터 키는 KMS를 절대 떠나지 않아 안전하고, 대용량을 KMS로 다 보내지 않아 효율적입니다. S3·EBS·RDS의 "저장 시 암호화"가 모두 이 방식입니다(오브젝트·블록·파일 스토리지·관리형 데이터베이스(RDS)).
확대
앱이 비밀번호를 코드 없이 받아오는 법 — 요청부터 회전까지 5단계
코드 어디에도 DB 비밀번호가 없는데 앱은 어떻게 DB에 붙을까요? 비밀은 앱이 실행되는 순간 들어옵니다. 앱이 기동하면 자기 역할(Role) 권한으로 시크릿 매니저에 값을 요청 → IAM이 인증·인가 → KMS로 복호화 → 앱 메모리로 전달 → 캐시하고, 비밀이 회전되면 다음 조회에서 자동으로 새 버전을 받습니다. 이 흐름을 알면 "왜 권한은 있는데 못 읽지", "왜 회전했는데 옛 값을 쓰지"를 단계로 좁혀 진단할 수 있습니다. 하드코딩이 위험한 이유도 이 흐름이 통째로 사라지고 비밀이 코드에 박히기 때문입니다.
[앱 프로세스] 기동 / 캐시 만료
│
① GetSecretValue 요청 (코드엔 비밀 대신 시크릿 이름·ARN만)
│
② IAM 인증·인가 (앱 역할에 이 비밀을 읽을 권한이 있나)
│
③ KMS 복호화 (암호화된 시크릿 → 마스터 키로 데이터 키 복호화)
│
④ 앱 메모리로 전달 (평문 비밀은 디스크·로그 아닌 메모리에만)
│
⑤ 캐시 + TTL (수 분~1시간 재사용 → 매 요청 조회를 피함)
▼
[회전 시] 새 버전 발급 → 다음 ①에서 최신값 수신 (코드 재배포 없음)
각 단계에서 무슨 일이 일어나고, 막히면 어떤 증상인가:
| 단계 | 하는 일 | 여기서 막히면 |
|---|---|---|
| ① 요청 | 앱이 실행 시점에 시크릿 이름으로 값을 요청. 코드·이미지·깃엔 이름만 남고 비밀 자체는 없다 | 코드에 값을 하드코딩해 두면 → 애초에 이 흐름을 안 타고 저장소 유출 = 비밀 유출 |
| ② 인증·인가 | 시크릿 매니저가 앱 역할의 IAM 정책을 확인 — 이 비밀을 읽을 권한이 있는지 | 권한 없음 → AccessDeniedException으로 기동 실패 / 와일드카드(*) 허용이면 최소권한 위반 |
| ③ 복호화 | 저장된 시크릿은 KMS로 암호화된 상태. KMS가 마스터 키로 데이터 키를 풀어 평문을 복원. 이 사용이 CloudTrail에 감사 기록됨 | 역할에 kms:Decrypt가 빠지면 → 읽기 권한이 있어도 복호화 단계에서 실패 |
| ④ 전달 | 평문 비밀을 앱 메모리로만 전달. 디스크·환경변수·로그로 새면 그게 유출 경로 | console.log(secret)·에러 스택에 섞이면 → 로그 접근 권한만으로 비밀 유출 |
| ⑤ 캐시·회전 | 값을 메모리에 캐시(TTL)해 매 요청 조회·과금·스로틀을 피함. 회전되면 다음 조회에서 새 버전 수신 | 캐시만 하고 인증 실패 시 재조회가 없으면 → 회전 직후 옛 값을 쥔 채 실패만 반복 |
즉 이 흐름의 핵심은 비밀이 '저장된 곳'과 '쓰는 곳'이 분리돼, 코드·이미지·깃 어디에도 평문이 남지 않는다는 것입니다. 그래서 "비밀을 어떻게 관리하나"의 답은 단계별로 검증됩니다 — ②에서 누가 읽을 수 있는지(IAM 최소권한), ③에서 무엇이 복호화했는지(KMS·CloudTrail 감사), ④에서 평문이 어디로 흐르는지(로그 마스킹), ⑤에서 회전이 반영되는지(캐시 무효화). 회전이 코드 재배포 없이 반영되는 건 앱이 '값'이 아니라 '이름'을 들고 있어 다음 ①에서 최신 버전을 다시 받아오기 때문입니다.
비밀을 평문으로 출력·로깅하지 말 것
안전한 실행 조건: 디버깅 중에도 비밀은 절대 출력하지 않는다. 존재 여부만 확인하려면 길이·마스킹(앞 2자리+****)만 출력하고, 로깅 라이브러리에 비밀 필드 마스킹 규칙을 건다.
실행 전 반드시 확인
- 비밀을 echo/print/console.log로 찍는 코드가 없는가
- 에러 메시지·스택트레이스에 비밀이 섞여 나가지 않는가
- 로그 집계 파이프라인에 민감필드 마스킹이 적용돼 있는가
- CI 로그에 환경변수 비밀이 평문으로 찍히지 않는가
echo $DB_PASSWORD # 또는 logger.info(secret)위 항목을 모두 확인한 후 복사할 수 있습니다
앱이 시작 시 자신의 역할 권한으로 비밀을 받아오는 흐름입니다. 코드엔 비밀 대신 '비밀의 이름'만 둡니다.
aws secretsmanager get-secret-value \
--secret-id prod/app/db \
--query '{Name:Name,VersionId:VersionId,CreatedDate:CreatedDate}' --output json
SecretString은 셸·CI 로그에 출력하지 말고 애플리케이션 메모리에만 받아 사용합니다. 운영자는 값 대신 조회 성공 여부와 버전 메타데이터만 확인합니다.
{"keys":["password","username"]} # 값은 마스킹되어 표시되지 않음
aws secretsmanager get-secret-value회전이 켜져 있는지, 마지막으로 언제 바뀌었는지 확인합니다.
aws secretsmanager describe-secret --secret-id prod/app/db \
--query "{Rotation:RotationEnabled,LastChanged:LastChangedDate,LastRotated:LastRotatedDate}"
{ "Rotation": true, "LastChanged": "2026-06-01T...", "LastRotated": "2026-06-01T..." }
aws secretsmanager describe-secret- describe-secret의 RotationEnabled가 false인 운영 비밀 — 회전 미설정. 유출 시 무기한 유효하므로 자동 회전 켜기 검토
- get-secret-value를 호출하는 신원(역할)이 최소 권한인지 — 해당 비밀만 읽도록 좁혔는지, 와일드카드(*)로 모든 비밀을 열어두진 않았는지(계정과 IAM)
- 코드/이미지/CI 설정에 평문 비밀이나 액세스 키가 남아 있는지 — git grep·이미지 스캔으로 점검
- LastRotatedDate가 너무 오래됐는지 — 회전 주기 정책(예: 90일) 대비 지연되면 회전 함수 실패 가능성 점검
상황: 앱(인스턴스/함수)이 시크릿 매니저에서 비밀을 가져오지 못해 기동 실패.
원인: 앱에 붙은 IAM 역할에 해당 비밀에 대한 secretsmanager:GetSecretValue 권한이 없거나, 비밀이 KMS 키로 암호화돼 있는데 그 KMS 키 복호화 권한(kms:Decrypt) 이 빠짐. 책임 공유 모델에서 권한 설정은 고객 책임(왜 클라우드인가).
진단: aws sts get-caller-identity로 현재 역할 확인 → 그 역할 정책에 GetSecretValue + (필요시) kms:Decrypt 가 있는지 확인 → 비밀의 리소스 정책도 점검.
해결: 역할에 해당 비밀 ARN으로 범위를 좁힌 GetSecretValue 권한 부여(전체 * 금지), 커스텀 KMS 키 사용 시 kms:Decrypt도 그 키로 한정해 부여. 최소 권한으로 '필요한 비밀만' 열어줍니다.
심화 — 규모가 커지면 만나는 두 벽, 호출량과 키 수명
심화: GetSecretValue를 매 요청마다 부르지 않는다 — 캐싱과 회전의 긴장
시크릿 매니저의 원칙은 '실행 시점 조회'지만, 매 요청마다 조회와는 다릅니다. 규모가 커지면 두 가지가 발목을 잡습니다.
- 비용과 한도: 시크릿 매니저는 비밀 건당 월 과금에 더해 API 호출량 과금이 있고, 계정 단위 초당 호출 한도가 있습니다. 인스턴스 수백 대가 요청마다
GetSecretValue를 부르면 비용이 불필요하게 쌓이고, 트래픽 피크에 호출이 스로틀되면 '비밀을 못 읽어서' 장애가 나는 역설이 생깁니다. - 표준 패턴 — 메모리 캐시 + 실패 시 재조회: 기동 시 받아 메모리에 캐시(TTL 수 분~1시간)하고, 공식 캐싱 클라이언트나 Lambda 확장을 씁니다. 핵심은 인증 실패를 만나면 캐시를 버리고 강제 재조회하는 로직입니다 — 이게 없으면 회전 직후 옛 값을 쥔 채 실패만 반복합니다.
- 회전과 캐시의 긴장: 캐시 TTL이 길수록 싸고 안정적이지만 회전 반영은 늦어집니다. DB 자격증명은 교대 사용자(alternating users) 회전 전략 — DB 계정 두 개를 번갈아 회전해 옛 자격증명이 한 주기 동안은 계속 유효하게 남깁니다. 회전 순간에 아무 커넥션도 끊기지 않게 하는 실무 장치입니다.
상황: 지난달 자원 정리 때 '몇 달간 사용 이력이 없어 보이는' KMS 키 여러 개를 삭제 예약(schedule-key-deletion)했습니다. 대기 기간이 지나 키가 삭제된 뒤, 그 키로 암호화돼 있던 오래된 백업·스냅샷이 전부 열리지 않습니다.
원인: 봉투 암호화 구조상 데이터 키는 마스터 키로만 풀 수 있습니다. 마스터 키 삭제 = 그 키로 감싼 모든 데이터 키가 영영 안 풀림 — 객체·스냅샷 자체는 남아 있어도 읽을 수 없는 데이터가 됩니다. '미사용'으로 보인 것은 최근 호출이 없었을 뿐, 재해 때만 꺼내는 백업이 그 키에 묶여 있었습니다.
진단: aws kms describe-key로 KeyState 확인 — PendingDeletion이면 아직 살릴 수 있습니다. 삭제를 실행하기 전이라면 CloudTrail에서 그 키의 Decrypt·GenerateDataKey 호출 이력과 호출 주체를 확인해 '진짜 미사용'인지 검증합니다.
해결: 대기 기간(7~30일) 안이라면 cancel-key-deletion으로 즉시 복구합니다. 이미 삭제됐다면 그 키로 암호화된 데이터는 복구 불가 — 다른 리전 복제본이나 비암호화 원본에서 재구축하는 길뿐입니다. 예방이 전부입니다: 키는 삭제 대신 비활성화(disable)로 두고 수십 일 관찰 후 지우고, 삭제 전 CloudTrail 사용 이력 확인을 절차화하며, 백업·스냅샷이 어떤 키에 묶여 있는지 목록으로 관리합니다.
시크릿 관리는 보안 감사의 단골 점검 항목입니다. "비밀을 어떻게 관리하나요?"에 좋은 답은 "시크릿 매니저에 두고 IAM으로 접근 제한, 자동 회전, 저장 데이터는 KMS 봉투 암호화, 코드·깃·로그엔 비밀 0" 입니다. 반대로 "환경변수에 다 넣어요"는 감점입니다.
PM 관점에서도 연동 요구사항에 "비밀은 시크릿 매니저 경유", "키 회전 주기", "로그 마스킹"을 인수 기준에 넣으면 사고를 예방합니다. 깃에 키가 새는 사고는 거의 모든 팀이 한 번은 겪으므로, 사전 차단(pre-commit 시크릿 스캐너)과 사후 대응(즉시 폐기·회전) 절차를 함께 둡니다(CI/CD 파이프라인). 이 주제는 계정과 IAM의 권한 관리와 한 쌍입니다.
다음 단계로는 이 트랙에서 배운 IAM·네트워크·스토리지·비밀 관리를 코드로 묶는 IaC와 Terraform, 그리고 전체를 관측·통제하는 관측과 거버넌스로 이어집니다.