시크릿 / 인증서 흐름
GenD의 시크릿은 세 계층입니다 — 애플리케이션 자격증명은 Vault에, Vault 를 여는 부트스트랩 비밀(root 토큰·recovery key)은 Azure Key Vault 에, 그 Azure 계정을 되찾는 수단은 Azure 밖 사람 금고에 둡니다.
애플리케이션 계층은 다시 3가지 주입 경로를 씁니다 — External Secrets Operator
(Vault → ESO → K8s Secret, 운영 표준), SealedSecret (레거시 — ESO 이관 후 청산 예정,
Epic #1107), cert-manager (TLS 자동 발급). 모든 시크릿은 단일 원본 원칙을 따라
valueFrom 으로 직접 참조되며, ConfigMap 에 평문 복제하지 않습니다.
:::note Vault Agent Injector 는 미설치입니다
Vault 의 비밀은 Agent 파일 마운트(/vault/secrets/*)가 아니라 ESO 가 동기화한
K8s Secret 을 통해 파드에 도달합니다. 실측: prod 에 agent-injector 파드 0개,
리포 전체에 /vault/secrets 마운트 0건.
:::
세 계층 — 무엇을 어디에 보관하는가
이 문서의 나머지는 애플리케이션이 쓰는 자격증명을 다룹니다. 그런데 그것들을 보관하는 Vault 자체를 여는 열쇠는 어디에 둘까요? Vault 안에 넣을 수 없습니다 — 순환 참조입니다.
그래서 시크릿은 세 계층으로 나뉩니다.
| 계층 | 무엇 | 보관처 | 접근 주체 |
|---|---|---|---|
| 애플리케이션 자격증명 | DB 비밀번호, S3 키, OIDC client secret, API 토큰 | Vault (KV v2) | 워크로드 (ESO 경유) |
| 부트스트랩 비밀 | Vault root 토큰, recovery key 5개, 제공자 키 암호화 키 | Azure Key Vault kv-gend-prod-recovery | 사람 (KV Secrets User 이상) |
| 최종 복구 수단 | Azure 관리자 계정 비밀번호, MFA 백업 코드 | 사람용 금고(1Password 등) + 인쇄 보관 | 사람 (Owner 소수) |
부트스트랩 비밀은 클러스터 안에 두지 않습니다. 클러스터를 장악한 공격자가 Vault 까지 함께 얻게 되기 때문입니다. Azure Key Vault 는 클러스터 밖이고 RBAC 으로 사람에게만 열려 있어 이 조건을 만족합니다.
:::tip 세 번째 줄이 왜 필요한가 — 순환 의존의 끝
prod Vault ──열쇠──▶ Azure Key Vault ──열쇠──▶ Azure 관리자 계정 ──?──▶
Azure Key Vault 가 Vault 를 열고, Azure 계정이 Key Vault 를 엽니다. 그 마지막 고리가 끊기면(계정 분실·MFA 기기 분실) 위의 모든 보관이 무의미해집니다. 그래서 Azure 계정 복구 수단만은 Azure 밖에 두어야 합니다.
사람용 금고를 쓴다면 그 금고의 복구 수단(1Password Emergency Kit 등)도 인쇄해 물리적으로 보관하십시오 — 개인 계정에는 조직 복구 경로가 없습니다. :::
현재 보관 상태 (2026-08-08 실측)
az keyvault secret list --vault-name kv-gend-prod-recovery --query "[].name" -o tsv
# gend-vault-root-token-1172
# gend-vault-recovery-key-1-of-5 … 5-of-5
# gend-provider-key-enc
접근 명부 — 값을 직접 읽을 수 있는 사람 3명(Key Vault Administrator 1,
Key Vault Secrets User 2), 스스로 권한을 부여할 수 있는 사람 6명(Owner 5 +
User Access Administrator 1). 구독 Contributor 30여 명은 읽지 못합니다(RBAC 모드라
데이터평면이 분리됨). 볼트는 soft delete·purge protection 활성, 90일 보존입니다.
꺼내는 방법과 주의사항은 Vault 재해 복구 §2 를 보십시오.
:::danger 보관보다 기록이 먼저 무너진다
vault operator init 은 root 토큰과 recovery key 를 운영자 홈의 임시 파일
(~/vault-init-output-<TS>.json) 로 내보냅니다. 그것을 금고로 옮기고 원본을 shred
하는 것은 스크립트가 아니라 사람이 합니다.
2026-08-08 에 이 절차의 어느 쪽이 실제로 취약한지가 드러났습니다(#3207).
옮기는 것은 됐습니다 — root 토큰과 recovery key 는 Azure Key Vault
kv-gend-prod-recovery 에 안전하게 있었습니다. 빠진 것은 어디에 넣었는지 적는 일
이었습니다. 어떤 런북에도 그 볼트 이름이 없었고, 조사 과정에서 다른 볼트
(kv-gend-prod-unseal) 하나만 확인하고 "Azure 경로는 소진됐다" 고 오판해 몇 시간을
잃었으며, 하마터면 멀쩡한 Vault 를 재초기화할 뻔했습니다.
교훈: 위치는 비밀이 아니라 포인터입니다. 값은 금고에, 포인터는 문서에 적으십시오 — 포인터 없는 금고는 없는 금고와 같습니다.
★ 그리고 여러 개일 수 있는 것(Key Vault·네임스페이스·저장소)은 전수 목록부터
뜨십시오(az keyvault list). 하나를 보고 범주 전체를 판정하면 이 사고가 재현됩니다.
:::
운영 규칙
- init 직후 즉시 옮기고 원본을
shred -u— 미루면 잊습니다. - 보관 위치를 이 문서에 적습니다 — 누가·언제·어느 볼트의 어느 이름으로. 값이 아니라 좌표를 적습니다. 새 시크릿을 금고에 넣었다면 위 "현재 보관 상태" 를 갱신하는 것까지가 한 작업입니다.
- 값을 화면에 출력하지 않습니다. 파일로 받아 stdin 으로 넘기고
shred -u로 지웁니다 — 명령 인자로 주면 파드 안ps와 셸 히스토리에 남습니다. - root 토큰은 상시 사용하지 않습니다. 일상 운영은 userpass / Kubernetes auth 로
발급받은 단명 토큰을 씁니다 (
admin정책). root 는 그 경로가 깨졌을 때의 최후 수단입니다. - 최후 수단이 하나뿐이면 안 됩니다 — root 토큰을 잃어도 recovery key 3/5 로
vault operator generate-root가 가능합니다. 둘 다 잃으면 재초기화 외에 길이 없습니다.
시크릿 주입 경로
주입 패턴 비교
| 패턴 | 사용 사례 | 장점 | 단점 |
|---|---|---|---|
| External Secrets (ESO) | DB 비밀번호, OAuth·SMTP secret, API 키 | Vault 단일 원본, 자동 재동기화, GitOps 친화 | Vault 가용성에 의존 · refreshInterval 만큼 반영 지연 |
| SealedSecret (레거시) | ESO 이관 전 잔여분 | Git 저장 가능 | 회전 수동 · Epic #1107 로 청산 예정 |
| cert-manager | TLS 인증서 (외부·내부) | 자동 갱신 | DNS-01 또는 HTTP-01 필요 |
Vault Transit 키
| 키 이름 | 알고리즘 | 회전 | 용도 | 상태 |
|---|---|---|---|---|
datax-connector | AES-256-GCM | 90일 자동 | 커넥터 비밀번호 암호화 | ✅ 사용 중 (2026-08-09 활성화, #3210) |
gend-chunk-hmac | HMAC (exportable=false) | 수동 | 벡터 청크 서명 | ✅ 사용 중 (guard_service, #3210) |
datax-ceph-kms | AES-256-GCM | 수동 | Ceph RGW SSE-KMS | ENABLE_CEPH_KMS=true 일 때만 생성 |
:::caution 키가 있다는 것과 쓰인다는 것은 다르다
05-enable-transit.sh 는 위 키를 만들지만, 암호화를 켜는 것은 애플리케이션 쪽
설정(GEND_VAULT_ENABLED)입니다. 2026-08-09 에 활성화했습니다(#3210) — 그 전까지는
키만 있고 앱이 쓰지 않아 커넥터 비밀번호가 평문으로 저장됐습니다.
★ 켜기 전 전제가 넷이었습니다: gend-api 정책·k8s role 부재, 주소 스킴(http→400),
TLS CA 미신뢰, 토큰 전달 경로(정적 토큰만 지원). 특히 정책이 HMAC 경로 2개를
빠뜨리고 있어 그대로 켰다면 커넥터 암호화는 되는데 청크 서명이 403 으로 죽었습니다.
감사 로그 HMAC 체인은 Transit 이 아니라 K8s SealedSecret
(gend-audit-hmac-key) 을 사용합니다.
:::
단일 원본 원칙 (#490 교훈)
같은 비밀이 여러 ConfigMap/Secret에 복제될수록 동기화 사고 위험이 증가합니다. GenD는 다음 규칙을 강제합니다:
- 공유 자격증명: K8s Secret 하나만 만들고
valueFrom: secretKeyRef로 직접 참조 - ConfigMap 평문 금지: 비밀 값은 ConfigMap에 절대 복제하지 않음
- bootstrap 스크립트:
2>/dev/null || true같은 무음 처리 금지, phase 폴링 + envFrom override 패턴 사용
관련 문서
- Vault 설정
- 매니페스트 평문 자격증명 차단 — 리포에 자격증명이 들어가는 것을 CI 가 막는다
- Keycloak 설정
- CORS · TLS