Tech Artifacts/K8s Security

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장에서 본 한계를 보안 관점에서 재구성:

  1. etcd 평문 저장(기본) — Secret은 base64로 etcd에. etcd 접근자/RBAC Secret 읽기 권한자는 원문 그대로 본다.
  2. 매니페스트에 평문 노출 — 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의 볼륨 마운트 갱신으로 자동화 패턴.

생각해 볼 문제

  1. etcd 암호화를 켰다. apiserver RBAC으로 Secret 읽기 권한 있는 SA가 탈취됐다. 비밀 노출되나?
  2. Git에 평문 Secret을 커밋한 히스토리가 있다. 이미 공개. 어떻게 수습하나?
  3. ESO로 Vault 비밀을 끌어온다. Vault의 비밀이 회전됐다. Pod는 새 비밀을 언제 쓰나? (10장 갱신)
  4. Sealed Secrets로 봉인한 매니페스트를 다른 클러스터에 적용하면? (키 종속)
  5. 비밀을 환경 변수로 주입했다. 회전되어도 앱이 못 쓴다. 볼륨 마운트로 바꾸면 달라지는 것은?
  6. etcd 암호화 + External Secrets를 둘 다 쓰면 얻는 "심층 방어"를 시나리오로 설명하라.

참고