
지난 화요일 새벽이었다. 4시 12분에 휴대폰이 부르르 울렸다. PagerDuty.
vault-prod cluster: all nodes sealed
침대에서 일어나면서 머릿속이 멍했다. 보통 vault가 sealed 상태로 돌아가는 일은 없다. unseal key는 사람이 들고 있지도 않다 — auto-unseal로 AWS KMS에 위임해놨으니까. 그런데 sealed라니. 노트북을 열면서 솔직히 멘탈이 좀 나갔다.
첫 30분: 뭐가 죽었는지조차 몰랐다
처음엔 vault 자체 이슈인 줄 알았다. 컨테이너가 OOM이라도 났나 싶어서 노드부터 확인했다. CPU도 메모리도 멀쩡했다. Pod도 다 running이었고. 그런데 vault status를 때려보면 매번 같은 응답이 돌아왔다.
Sealed: true
Total Shares: 0
Threshold: 0
Threshold 0은 auto-unseal을 쓴다는 의미다. 그러니까 vault는 KMS한테 unseal key를 풀어달라고 요청 중인데 그게 안 풀린다는 얘기다. 로그를 보니 확실해졌다.
error="failed to decrypt with seal: ...
RequestError: send request failed...
context deadline exceeded"
AWS KMS가 응답을 안 주고 있었다. ap-northeast-2 리전. AWS Health Dashboard를 열어보니 그제서야 회색 점이 보였다. "Increased error rates for AWS Key Management Service in the Asia Pacific (Seoul) Region." 시작 시각이 03:54. 우리가 sealed로 돌아간 시각과 거의 일치했다.
KMS 리전 단위 장애. 자주 있는 일은 아닌데 없는 일도 아니다. 문제는 우리 vault가 이 단일 KMS 키 하나에만 매달려 있었다는 점이다.
1시간 차 — 우회로가 없다
kms outage 시 vault unseal 같은 키워드로 검색을 돌렸다. 답은 단순했다. KMS가 살아나야 unseal이 된다. 끝.
그 사이 백엔드 팀에서 메시지가 쏟아졌다. 시크릿을 못 가져오니 새로 배포되는 pod들이 전부 init container에서 멈춰 있었다. 기존에 떠 있던 워크로드는 살아 있었지만, 카프카 컨슈머 한 대가 재시작되면서 시크릿을 못 받고 CrashLoopBackOff로 빠졌다. 그게 지표 수집 파이프라인이라 P1 알람이 뜨기 시작했다.
새벽 5시 즈음 AWS가 부분 복구를 시작했고 5시 30분쯤 KMS API가 다시 응답하기 시작했다. vault가 자기 알아서 unseal로 돌아왔다. 약 1시간 30분 다운. 다행히 우리 쪽 매출에 직접 묶이는 트래픽은 아니라 큰 사고는 아니었지만, 회의에서 "이거 또 일어나면?" 질문에 답할 게 없었다.
회고: 왜 우리는 단일 KMS에 매달려 있었나
이건 게으름이었다. Vault 클러스터를 처음 세팅할 때만 해도 auto-unseal 자체가 신박해서 그걸로 끝냈다. 옛날에 unseal key 5개를 사람들이 보안 USB에 나눠 들고 다니던 시절을 생각하면 KMS로 위임하는 게 얼마나 편한가. 그런데 그 위임의 대상이 단일 리전, 단일 키였다.
문제는 두 가지였다.
하나, KMS 키 자체가 SPOF다. KMS는 region별 서비스다. 멀티 리전 키(multi-Region key)를 쓰면 다른 리전으로 fallback이 되지만 우리는 안 썼다.
둘, seal 메커니즘을 다중화할 수 있다는 걸 까먹고 있었다. Vault 1.16부터 Seal HA가 들어왔는데, 우리 클러스터가 1.14에 머물러 있어서 그 기능 자체를 못 쓰고 있었다.
그래서 뭘 바꿨나
먼저 Vault를 1.21로 올렸다. 이게 일주일 걸렸다. 1.14 → 1.21 사이에 storage migration이 한 번 있어서 (Raft 변경 사항들) 신중하게 단계적으로 올렸다.
그리고 seal stanza를 두 개로 늘렸다. 핵심은 이런 구성이다.
seal "awskms" {
name = "kms-primary-seoul"
priority = "1"
region = "ap-northeast-2"
kms_key_id = "alias/vault-unseal-seoul"
}
seal "awskms" {
name = "kms-secondary-tokyo"
priority = "2"
region = "ap-northeast-1"
kms_key_id = "alias/vault-unseal-tokyo"
}
priority 필드가 핵심이다. 우선순위가 낮은 값을 먼저 시도한다. 둘 중 하나만 살아 있어도 vault는 시작/unseal이 된다. 다만 한쪽이 죽은 동안에는 vault가 degraded 상태로 동작한다 (seal wrap된 값들을 새로 쓸 때 살아있는 seal로만 wrapping). 그러다가 죽었던 seal이 돌아오면 vault가 자동으로 모든 값을 다시 두 seal 모두로 rewrap한다.
여기서 한 가지 더 챙겨야 했다. seal HA를 enable하기 전에 반드시 마이그레이션 절차를 거쳐야 한다 (정확히는 priority가 1인 seal로 먼저 unseal한 다음 두 번째 seal을 붙이는 식). 이걸 모르고 그냥 stanza를 추가하면 vault가 시작 자체를 거부한다. 우리 팀에서 한번 stage에서 이걸 밟아서 30분 날렸다.
리전 다중화 대신 KMS + Transit seal 조합으로 가는 곳도 있다고 한다. 별도의 작은 vault를 transit seal용으로 두고 그게 KMS auto-unseal을 쓰는 식. 우리는 운영 부담을 늘리기 싫어서 KMS 두 리전 조합으로 끝냈다.
한 가지 더, 디펜던시 그래프 다시 그리기
이번 사건의 진짜 교훈은 사실 vault가 아니라 디펜던시 그래프였다. 우리는 platform 팀이 운영하는 vault가 죽으면 무슨 일이 일어나는지 정확히 모르고 있었다. application 팀들이 vault sidecar로 시크릿을 받는다는 건 알았지만, 어떤 워크로드가 vault에 어떻게 의존하는지 매핑된 문서가 없었다.
장애 다음 날 platform 팀에서 작업한 게 그거였다. vault sidecar 또는 CSI driver를 통해 시크릿을 받는 워크로드를 다 뽑아서, 그것들이 vault outage 시 어떻게 fallback하는지 표를 만들었다. 의외로 절반 이상이 "fallback 없음 — pod restart 시 사망"이었다. 일부는 secret이 환경변수로 한 번 박혀서 살아있는 pod는 영향 없는 정도. 이 매핑이 있으면 다음에 비슷한 일이 터졌을 때 어디부터 손을 댈지가 보인다.
마무리
KMS 리전 outage는 자주 안 일어난다. 그런데 일어났을 때 vault가 죽으면 시크릿 의존하는 모든 게 같이 흔들린다. seal HA는 Enterprise 기능이라 비용 부담이 있긴 한데, 시크릿 인프라가 회사 매출에 직접 묶이는 환경이라면 충분히 정당화된다. OSS 환경이라면 KMS multi-Region key를 쓰는 것만으로도 1/3 정도는 막아진다.
혹시 비슷한 사고 경험 있으신 분 계시면 어떻게 풀어가셨는지 댓글로 알려주세요. 우리는 Seal HA 도입 후 stage에서 강제로 KMS endpoint를 막아보는 카오스 테스트를 한 달에 한 번 돌리고 있다. 다음에는 그 카오스 테스트 세팅을 정리해서 글로 써볼 생각이다.
'IT > DevSecOps' 카테고리의 다른 글
| Falco vs Tetragon, 우리 팀은 결국 이렇게 정리했다 (0) | 2026.06.10 |
|---|---|
| External Secrets Operator, ClusterSecretStore로 시크릿 관리 정리하기 (0) | 2026.06.09 |
| Kyverno ClusterPolicy를 ValidatingPolicy(CEL)로 옮기는 법 (0) | 2026.06.05 |
| Vault Agent Injector annotation 충돌로 새벽에 일어난 이야기 (0) | 2026.05.31 |
| GitHub Actions OIDC, 아직도 sub에 wildcard 쓰고 계세요? (0) | 2026.05.30 |