K8s Security - 06. secrets management
Kubernetes의 Secret은 "비밀"이 아니다 — 그럼 진짜 비밀은 어디에 두나
config-secret 에서 Secret이 base64 인코딩일 뿐 "비밀"이 아니라고 했다. etcd엔 사실상 평문. RBAC으로 읽기 권한 있으면 그냥 본다. 그렇다면 진짜 비밀(DB 비밀번호, API 키)은 어디에 어떻게 두나? 한 팀이 이 질문에 답하며 거칠게 세 가지를 시도했다 — etcd 암호화, Vault 연동, 그리고 External Secrets Operator. 이 글은 그 답들을 정리한다.
이 글이 푸는 것은: Kubernetes Secret의 보안 한계를 극복하는 세 접근(etcd 저장 암호화, 외부 비밀 저장소 연동, Gitops 친화적 sealed-secrets)이다.
Secret의 두 한계 — 다시 한 번
10장에서 본 한계를 보안 관점에서 재구성:
- etcd 평문 저장(기본) — Secret은 base64로 etcd에. etcd 접근자/RBAC Secret 읽기 권한자는 원문 그대로 본다.
- 매니페스트에 평문 노출 — Git에 Secret YAML을 커밋하면 비밀이 평문으로 저장소에.
이 두 한계를 각각 다른 도구가 해결한다.
접근 1: etcd 저장 암호화 (encryption at rest)
apiserver가 etcd에 쓰기 전에 Secret(및 다른 리소스 선택)을 암호화. etcd에 저장되는 건 암호문.
# Kubernetes 1.36 — EncryptionConfiguration (apiserver에 적용)
# etcd에 저장되는 Secret이 암호화됨
설정은 apiserver의 --encryption-provider-config로. 키(KMS/외부)로 암호화. etcd가 탈취돼도 키 없이는 복호화 불가.
한계: apiserver(또는 그 키)를 쥔 사람은 여전히 복호화 가능. etcd 단독 탈취엔 강하지만, apiserver 권한 침해엔 약함. "심층"이지 "완전"은 아니다.
etcd 암호화는 최소한의 보호로 권장. 단독으론 부족 — 외부 비밀 저장소(다음)와 함께.
접근 2: External Secrets Operator + Vault/AWS Secrets Manager
External Secrets Operator(ESO)가 외부 비밀 저장소(Vault, AWS Secrets Manager, GCP Secret Manager 등)에서 비밀을 끌어와 Kubernetes Secret으로 동기화.
flowchart LR
GIT["Git<br/>(ExternalSecret 매니페스트만,<br/>평문 비밀 없음)"] --> ESO["External Secrets Operator"]
ESO -. SecretStore 참조 .-> SS["SecretStore<br/>(Vault/AWS SM 연결)"]
SS --> VAULT["외부 저장소<br/>(Vault/AWS SM)"]
VAULT -. 비밀 끌어옴 .-> ESO
ESO --> KS["Kubernetes Secret<br/>(런타임에만 생성)"]
이점:
- Git에 평문 비밀 없음 — Git엔
ExternalSecret객체(어느 외부 비밀을 참조할지만)만. - 외부 중앙 관리 — Vault/AWS SM이 비밀 회전/감사/접근 통제.
- 런타임 생성 — Secret은 클러스터에 런타임에만 생기고, Git 히스토리에 평문이 안 남.
# ExternalSecret — 외부 비밀을 참조 (평문 값 없음)
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata: {name: db-creds}
spec:
secretStoreRef: {name: vault-backend, kind: SecretStore}
target: {name: db-creds}
data:
- secretKey: password
remoteRef: {key: secret/data/db, property: password}
접근 3: Sealed Secrets — Git에 암호문 커밋
GitOps(ArgoCD/Flux)를 쓰되 Git에 비밀을 커밋하고 싶을 때. Sealed Secrets(Bitnami)는 비밀을 클러스터 전용 키로 암호화해 Git에 안전하게 커밋.
kubectl create secret generic db --dry-run=client --from-literal=pw=x -o yaml | \
kubeseal --controller-namespace=kube-system -o yaml > db-sealed.yaml
db-sealed.yaml은 암호문이라 Git에 커밋해도 안전(오직 그 클러스터의 컨트롤러 키로만 복호화). 클러스터의 Sealed Secrets 컨트롤러가 이것을 다시 평문 Secret으로 풀어 Pod에 주입.
차이: ESO는 외부 저장소에서 끌어오고, Sealed Secrets는 Git에 암호문을 둔다. GitOps 철학에 따라 선택.
세 접근 비교
| etcd 암호화 | External Secrets | Sealed Secrets | |
|---|---|---|---|
| 해결 | etcd 평문 | Git 평문 + 중앙 관리 | Git 평문 (암호문 커밋) |
| 외부 의존 | KMS/키 | Vault/AWS SM | 클러스터 컨트롤러 키 |
| 회전 | 어려움 | 외부에서 쉬움 | 재봉인 필요 |
| 감사 | 약함 | 외부 저장소가 제공 | 약함 |
실무: etcd 암호화(기본 보호) + External Secrets(비밀 회전/감사) 조합이 흔한 정석.
비밀 회전(rotation) — 잊히는 운영
비밀은 정기적으로 바뀌어야 한다. 그런데 Kubernetes Secret을 수동으로 바꾸면 — 그것을 쓰는 Pod가 갱신을 안 한다(10장, 환경 변수는 재시작까지 고정). ESO/Vault는 회전을 외부에서 자동화하고, 회전 시 Secret 갱신을 Pod가 감지(볼륨 마운트 시)하는 패턴으로 연결.
직접 확인하기 (개념 중심)
# Kubernetes 1.36 — Secret이 평문(base64)인 것 다시 확인
kubectl create secret generic s --from-literal=pw=topsecret
kubectl get secret s -o jsonpath='{.data.pw}' | base64 -d # topsecret 그대로
# → etcd에도 이 base64가 저장. "비밀" 아님.
ESO/Vault/Sealed Secrets 실측은 각 공식 문서. 설치 후 ExternalSecret/SecretStore 객체로 연동.
ESO의 SecretStore 백엔드 — 어느 외부 저장소와 연결하나
External Secrets Operator(ESO)가 지원하는 백엔드는 다양:
- HashiCorp Vault: 엔터프라이즈 비밀 관리 표준. 동적 비밀(아래), KV 스토어, PKI 인증서.
- AWS Secrets Manager / GCP Secret Manager / Azure Key Vault: 각 클라우드 관리형.
- GitHub/GitLab Secrets, 1Password: 개발팀 친화적.
- 외부 password manager(Bitwarden 등).
SecretStore(namespace 한정)/ClusterSecretStore(전체) 객체로 백엔드 연결 설정. 그 위에 ExternalSecret이 "이 백엔드의 이 비밀을 끌어와라"를 선언. 백엔드를 바꾸면 SecretStore만 교체 — ExternalSecret은 동일(이식성).
Vault의 동적 비밀 — 비밀이 미리 존재하지 않는 모델
Vault의 강력한 기능 중 하나: 동적 비밀(dynamic secret). 미리 만들어둔 비밀이 아니라, 요청 시 그 자리에서 생성되는 비밀:
- DB 자격증명: "이 앱을 위해 DB 계정을 지금 생성, 1시간 뒤 자동 삭제."
- 클라우드 임시 토큰: "이 작업을 위한 AWS 임시 자격증명, 만료 시 자동 무효."
flowchart LR
APP["앱 시작"] --> ESO["ESO가 Vault에 요청"]
ESO --> VLT["Vault: DB 계정 동적 생성"]
VLT --> DB["DB에 임시 계정 생성<br/>(1시간 TTL)"]
VLT --> KS["K8s Secret에 임시 자격증명"]
KS --> APP
NOTE["1시간 뒤"] -. DB 계정 자동 삭제 .-> DB
이 모델의 가치: 비밀이 미리 존재하지 않는다. 탈취돼도 TTL 뒤 자동 무효. 비밀 회전이 "다시 생성"으로 끝(이전 비밀을 무효화할 필요 없음 — 어차피 만료). 이것이 정적 비밀(미리 만들어둔 비밀번호)과 동적 비밀의 근본 차이.
회전과 Pod 갱신 — 비밀이 바뀌면 앱은 언제 쓰나
비밀이 회전(Vault/ESO)돼도 — 10장(config-secret)에서 환경 변수 주입은 Pod 재시작까지 안 바뀐다고 했다. 그래서 회전의 효과가 앱에 닿으려면:
- 볼륨 마운트: 비밀이 파일로 마운트되면 kubelet이 수분 단위로 갱신. 앱이 파일 변경을 감지하면 새 비밀 사용.
- 자동 재시작: Reloader 같은 도구가 Secret 변경 감지 → Deployment 자동 rollout. 새 Pod가 새 비밀로.
- 앱이 직접 폴링: 앱이 apiserver에서 Secret을 주기적 읽기. 가장 제어 정밀하나 앱 부담.
이 세 가지 중 어느 것도 안 하면 — 회전된 비밀이 Secret 객체엔 반영돼도 기존 Pod는 옛 비밀을 계속 쓴다. 회전의 보안 가치가 실현되지 않는다. 회전 전략에는 갱신 전략이 짝이어야.
sealed-secrets의 키 관리 — 봉인 키가 사라지면
Sealed Secrets가 비밀을 클러스터 전용 키로 암호화한다고 했다. 이 키 관리가 운영 포인트:
- 봉인 키는 클러스터의 Sealed Secrets 컨트롤러가 생성/보관.
- 클러스터가 재구축(새 클러스터)되면 — 이전 봉인 키가 없으면, Git에 커밋된 봉인 비밀들을 풀 수 없다. 복구 불가.
- 그래서 봉인 키를 외부에 백업하는 것이 필수. 키 없이 봉인 비밀만 있는 건 쓸모없는 암호문.
이것이 Sealed Secrets의 치명적 단일 장애점 — 키. ESO(Vault/AWS SM)는 비밀이 외부*에 있어 키 문제가 없지만, Sealed Secrets는 *클러스터 안 키에 의존. 재해 복구 계획(DR)에 봉인 키 백업이 반드시 포함돼야.
ESO vs Sealed Secrets — 상황별 선택
| ESO | Sealed Secrets | |
|---|---|---|
| 비밀 저장 | 외부(Vault/AWS SM) | Git에 암호문 |
| 회전 | 외부에서 자동 | 재봉인(수동/스크립트) |
| 감사 | 외부 저장소가 제공 | 약함 |
| 백업 부담 | 외부 저장소가 책임 | 봉인 키 직접 백업(치명적) |
| GitOps 친화 | 매니페스트에 참조만 | 매니페스트에 암호문(완전 자급) |
조직에 Vault/AWS SM이 이미 있으면 ESO. 순수 GitOps 자급을 원하면 Sealed Secrets. 단 Sealed Secrets는 키 백업이 생명.
"Git에 평문이 없다"를 어떻게 검증하나
비밀 관리 전략의 성공 여부를 확인하는 법:
# Git 히스토리에서 Secret 데이터가 평문으로 있는지
git log -p --all -S 'password:' -- '*.yaml' | grep -i password | head
# kubesec/trufflehog 같은 스캐너로 비밀 스캔
trufflehog git file://. --only-verified
이 검증이 지속적이어야 — 실수로 평문 비밀을 커밋하는 것을 자동 탐지(pre-commit hook + 스캐너). "우리는 ESO 쓴다"가 충분이 아니라, 실제로 평문이 안 들어가는지를 자동 검증하는 것이 보안 운영.
흔히 묻는 것, 흔히 틀리는 것
| 오해 | 정정 |
|---|---|
| "etcd 암호화하면 비밀이 완전히 안전하다" | apiserver/키 침해엔 약함. 심층일 뿐 |
| "Secret은 항상 암호화돼 저장된다" | 기본 평문(base64). 암호화는 별도 설정 |
| "External Secrets는 Kubernetes 비밀을 없앤다" | Secret 객체는 여전히 생김(런타임). Git 평문이 사라짐 |
| "Sealed Secrets는 비밀을 더 안전하게 저장한다" | Git에 암호문 커밋. 클러스터에선 평문 Secret으로 풀림 |
| "비밀 회전은 Kubernetes가 알아서" | 아님. 외부 저장소(Vault) 또는 수동 |
| "Vault만 쓰면 된다" | Vault에서 Kubernetes Secret으로 주입이 필요. ESO 등이 그 다리 |
요약 — 이 글의 결론
- Kubernetes Secret은 기본 "비밀"이 아니다 — etcd 평문(base64). RBAC으로 읽으면 그냥 본다. 진짜 비밀 보호는 별도 도구.
- etcd 저장 암호화가 최소 보호 — apiserver가 etcd 쓰기 전 암호화. etcd 단독 탈취엔 강하나 apiserver 침해엔 약함.
- External Secrets Operator(Vault/AWS SM 연동)이 외부 중앙 관리 — Git엔 참조만, 비밀은 외부에서 회전/감사, 런타임에 Secret 생성.
- Sealed Secrets가 GitOps 친화적 대안 — 비밀을 클러스터 키로 암호화해 Git에 커밋, 컨트롤러가 풀어 Secret 생성.
- 실무 정석: etcd 암호화(기본) + External Secrets(회전/감사). 둘 다 심층.
- 비밀 회전이 잊히는 운영. 외부 저장소(Vault) + Pod의 볼륨 마운트 갱신으로 자동화 패턴.
생각해 볼 문제
- etcd 암호화를 켰다. apiserver RBAC으로 Secret 읽기 권한 있는 SA가 탈취됐다. 비밀 노출되나?
- Git에 평문 Secret을 커밋한 히스토리가 있다. 이미 공개. 어떻게 수습하나?
- ESO로 Vault 비밀을 끌어온다. Vault의 비밀이 회전됐다. Pod는 새 비밀을 언제 쓰나? (10장 갱신)
- Sealed Secrets로 봉인한 매니페스트를 다른 클러스터에 적용하면? (키 종속)
- 비밀을 환경 변수로 주입했다. 회전되어도 앱이 못 쓴다. 볼륨 마운트로 바꾸면 달라지는 것은?
- etcd 암호화 + External Secrets를 둘 다 쓰면 얻는 "심층 방어"를 시나리오로 설명하라.
참고
- Kubernetes 공식 문서 - Encrypting Secret Data at Rest - 접근 2026-07-12
- External Secrets Operator - 접근 2026-07-12
- HashiCorp Vault - 접근 2026-07-12
- Sealed Secrets (Bitnami) - 접근 2026-07-12
- Kubernetes 공식 문서 - Secret 보안 모델 - 접근 2026-07-12
'Tech Artifacts > K8s Security' 카테고리의 다른 글
| K8s Security - 07. cert-manager (0) | 2026.07.16 |
|---|---|
| K8s Security - 05. pod-security-admission (0) | 2026.07.16 |
| K8s Security - 04. admission-control (0) | 2026.07.16 |
| K8s Security - 03. fine-grained kubelet API authorization (0) | 2026.07.16 |
| K8s Security - 02. authentication (0) | 2026.07.16 |