10. 시크릿 — 값을 넣고 · 지키고 · 바꾸기
시크릿이 복잡하게 느껴지는 건 성질이 다른 네 문제가 한 단어에 묶여 있기 때문이다
이 장에서 처음 나오는 말9개
Secret- 쿠버네티스의 비밀 저장 오브젝트.
data필드는 암호문이 아니라 base64 인코딩이라 읽을 권한만 있으면 원문을 본다. 저장 시 암호화는 클러스터에 별도로 설정해야 한다. 평문 · 암호문Plaintext · Ciphertext- 사람이 읽을 수 있는 원래 값과, 키 없이는 읽을 수 없게 바꾼 값. base64 결과는 암호문이 아니다. 누구나 되돌릴 수 있기 때문이다.
공개키 · 개인키Public Key · Private Key- 공개키로 잠그고 개인키로 푸는 한 쌍. 공개키는 나눠 줘도 되지만, 개인키는 복호화 권한 그 자체라서 숨기고 백업해야 한다.
SealedSecret- 클러스터의 공개키로 잠근 암호문 오브젝트. Git에는 이것을 두고, 클러스터 안 컨트롤러가 개인키로 풀어 평범한
Secret을 만든다. sealing key 세트봉인 키 세트- SealedSecret을 푸는 개인키들의 묶음. 키가 주기적으로 추가되므로 백업도 한 파일이 아니라 현재 키 세트 전체를 떠야 한다.
스코프Scope- 암호문을 어느 이름·어느 네임스페이스에서 풀 수 있는지의 결합 범위. 기본은 이름+네임스페이스에 고정(strict)이라, 옮기면 복호화가 거부된다.
SOPSSecrets OPerationS- YAML·JSON 같은 설정 파일의 값을 암호화해 편집하는 도구. 이 장의 기본 경로에는 필수가 아니며, 아래에서는 sealing key 백업 파일을 암호화하는 선택 레시피로 쓴다.
age- 파일 암호화 도구이자 SOPS가 사용할 수 있는 키 방식. recipient(공개키) 로 암호화하고 대응하는 identity(개인키) 로 복호화한다. 약어가 아니라 제품 이름이다.
ESOExternal Secrets Operator- 외부 금고(Vault 등)의 값을 읽어 쿠버네티스
Secret으로 만들어 주는 오퍼레이터. Git에는 값이 아니라 참조만 올라간다.
먼저 한 값의 일생을 본다
섹션 제목: “먼저 한 값의 일생을 본다”도구를 고르기 전에 DB 비밀번호 하나가 어디를 지나는지만 잡는다. 아래 다섯 자리는 Sealed Secrets를 쓰든 외부 금고를 쓰든 바뀌지 않는다.
| 자리 | DB 비밀번호의 상태 |
|---|---|
| 원천 | DB에서 실제 비밀번호를 발급한다. 여기의 값이 진짜다 |
| Git | 평문 대신 암호문(SealedSecret·SOPS) 또는 금고 참조(ESO)를 둔다 |
| 클러스터 | 컨트롤러가 평범한 Kubernetes Secret을 만든다 |
| 앱 | 파드가 환경변수나 볼륨으로 값을 읽는다 |
| 변경·복구 | 원천 값을 바꾸고 다시 전달한다. 클러스터를 잃으면 복호화 키나 금고부터 되살린다 |
이 장의 네 문제는 이 경로에서 나온다 — Git에 무엇을 둘지, 여러 네임스페이스에 어떻게 전달할지, 원천 값이 바뀌면 앱까지 어떻게 반영할지, 클러스터를 잃으면 어떻게 되살릴지다.
문제 — Secret은 암호화가 아니다
섹션 제목: “문제 — Secret은 암호화가 아니다”먼저 오해 하나를 걷어야 한다.
kubectl -n database get secret pg-backup-s3 -o jsonpath='{.data.SECRET_ACCESS_KEY}' | base64 -d# → 그냥 원문이 나온다. 암호가 아니라 인코딩이다읽을 권한이 있으면 누구나 본다. 그래서 세 가지가 동시에 문제가 된다.
| 문제 | 왜 |
|---|---|
| Git에 못 올린다 | GitOps는 Git이 단일 소스인데(9장), 평문 YAML을 올리면 저장소를 읽는 모두가 본다 |
| etcd에 평문으로 남는다 | 저장 시 암호화를 안 켰다면 etcd 스냅샷에 원문이 들어 있다 (11장) |
| 누가 읽을 수 있는지 모른다 | 네임스페이스 안에서 Secret 읽기 권한은 생각보다 넓게 퍼져 있다 |
네 개의 다른 문제 — 여기서부터 갈라서 본다
섹션 제목: “네 개의 다른 문제 — 여기서부터 갈라서 본다”“시크릿 관리”라는 말에 성질이 다른 네 문제가 섞여 있다. 이걸 안 가르면 도구 이름만 늘어난다.
| 문제 | 헷갈리는 지점 |
|---|---|
| ① 저장 | Sealed Secrets와 SOPS+age는 둘 다 Git에 암호문을 둘 수 있다. 차이는 누가 언제 푸는가다 |
| ② 배포 | Secret은 네임스페이스를 못 넘는다. 와일드카드 TLS·이미지 pull 시크릿이 전부 여기 걸린다 |
| ③ 갱신 | 값을 바꿔도 이미 뜬 파드는 모른다. 자동 반영은 별도 장치가 필요하다 |
| ④ 복구 | 이게 제일 무섭다 — 복호화 키 세트를 잃으면 Git의 암호문을 못 푼다 |
① Git에 두는 법 — 세 방식
섹션 제목: “① Git에 두는 법 — 세 방식”| 방식 | Git에 올라가는 것 | 누가 푸나 | 장점 | 대가 |
|---|---|---|---|---|
| Sealed Secrets | 암호문 | 클러스터 안 컨트롤러가 자동 | 추가 인프라 없음. 시작이 가장 쉽다 | sealing key 세트를 잃으면 전부 못 푼다. 값 조회·수정이 불편 |
| ESO + 외부 금고 | 참조만 | 오퍼레이터가 금고에서 읽어 온다 | 회전·감사·만료가 금고에서 관리된다 | Vault 같은 걸 하나 더 운영해야 |
| SOPS + age | 암호문(파일) | 배포 시 사람·CI(Continuous Integration, 지속적 통합) 파이프라인이 복호화 | 파일 단위로 유연. Git 밖에서도 쓴다 | 복호화 키 배포를 사람이 관리 |
Sealed Secrets 실무 — 실제로 데는 곳
섹션 제목: “Sealed Secrets 실무 — 실제로 데는 곳”봉인·복호화가 안에서 어떻게 도는지 — 하이브리드 암호화, 스코프가 강제되는 원리, 키 갱신의 실제 의미 — 는 Sealed Secrets의 동작 원리에 따로 정리했다. 여기서는 손에 잡히는 조작과 함정만 본다.
# 평문 Secret을 "적용하지 말고" 매니페스트로만 만들어 바로 kubeseal에 넘긴다kubectl create secret generic loki-s3 \ --namespace observability \ --from-literal=AWS_ACCESS_KEY_ID=loki-user \ --from-file=AWS_SECRET_ACCESS_KEY=/dev/stdin \ --dry-run=client -o yaml \| kubeseal --controller-namespace sealed-secrets --format yaml \ > platform/observability/loki/sealedsecret-s3.yaml# 값은 표준 입력으로 — 실행 후 입력하고 Ctrl-D, 또는 앞에 printf '%s' "$VALUE" | 를 붙인다
git add platform/observability/loki/sealedsecret-s3.yaml # 암호문이라 안전하다--from-file=...=/dev/stdin으로 값을 파이프로 넣는 이유는 아래 “평문이 새는 곳” 절에 있다 —
--from-literal에 비밀번호를 그대로 쓰면 셸 히스토리에 남는다.
암호문은 직접 편집할 수 없고, 로컬에는 개인키가 없어 기존 값을 열어 볼 수도 없다. 그래서 바꿀 키만 다시 봉인해 갈아끼운다.
# (A) --merge-into — 기존 파일에 그 키만 병합. 다른 키는 그대로 유지된다 (가장 흔함)kubectl create secret generic loki-s3 --namespace observability \ --from-file=AWS_SECRET_ACCESS_KEY=/dev/stdin --dry-run=client -o yaml \| kubeseal --merge-into platform/observability/loki/sealedsecret-s3.yaml
# (B) --raw — 값 하나만 암호화해서 encryptedData에 손으로 붙인다printf '%s' "$NEW" | kubeseal --raw --name loki-s3 --namespace observability이름과 네임스페이스를 기존과 똑같이 줘야 한다(아래 스코프). 그리고 결과는 커밋해야 반영된다 — 파일이 곧 소스다.
개발자마다 kubeseal과 클러스터 접근을 갖게 하고 싶지 않다면,
CI(Continuous Integration, 지속적 통합) 파이프라인으로 봉인을 옮긴다.
# CI: 금고·환경변수에서 값을 받아 봉인한 결과만 커밋한다kubeseal --cert sealed-secrets-public.pem --format yaml \ --name "$NAME" --namespace "$NAMESPACE" < secret.yaml > "sealedsecret-$NAME.yaml"--cert로 공개키 파일만 주면 클러스터 접근 없이도 봉인된다.
공개키는 이름 그대로 공개해도 안전하니 저장소에 두면 된다.
선택 레시피 — sealing key 백업을 SOPS와 age로 지키기
섹션 제목: “선택 레시피 — sealing key 백업을 SOPS와 age로 지키기”위 백업 파일에는 개인키가 평문으로 들어 있다. 그대로 두면 그 파일 자체가 최대 위험이다. 조직에서 이미 쓰는 암호화 금고가 있다면 거기에 넣으면 된다. 그런 체계가 없을 때 쓸 수 있는 작은 오프라인 구성이 SOPS + age다 — age 공개키로 잠그고, 클러스터 밖의 age 개인키로 푼다. SOPS 공식 문서에서 지원 파일 형식과 age 키 사용법을 확인할 수 있다.
# 1) age 키 한 벌 (recipient 공개키는 공유, identity 개인키는 오프라인 금고로)age-keygen -o age.key # 안에 AGE-SECRET-KEY-... 가 들어 있다
# 2) sealing key 세트 백업을 age로 암호화 → 이제 저장소에 둬도 된다sops --encrypt --age age1ql3z...ac8r sealed-secrets-keys.yaml > sealed-secrets-keys.yaml.enc
# 3) 복구할 때SOPS_AGE_KEY_FILE=age.key sops --decrypt sealed-secrets-keys.yaml.enc > sealed-secrets-keys.yaml② 네임스페이스 경계 — Secret은 경계를 못 넘는다
섹션 제목: “② 네임스페이스 경계 — Secret은 경계를 못 넘는다”의외로 자주 부딪히는 벽이다. 파드는 같은 네임스페이스의 Secret만 참조할 수 있다. 그런데 여러 곳에서 같은 값을 써야 하는 경우가 계속 생긴다.
| 여러 NS가 필요한 값 | 어디서 나왔나 |
|---|---|
| 와일드카드 TLS 인증서 | 4장 — 앱마다 Gateway를 쓰면 각자 NS에 필요 |
| 사내 레지스트리 pull 시크릿 | 9장 — 모든 네임스페이스에 필요 |
| 오브젝트 스토리지 접근 키 | 6장 — Loki · Tempo · Velero · CNPG가 각각 다른 네임스페이스 |
| 사내 CA 번들 | 4장 |
선택지는 셋이다.
| 방법 | 어떻게 | 언제 |
|---|---|---|
| 네임스페이스마다 따로 봉인 | 같은 값을 네임스페이스 수만큼 봉인해 Git에 | 대상이 두세 개고 잘 안 바뀔 때. 가장 단순하고 명시적 |
| Reflector | 원본에 annotation을 달면 지정한 네임스페이스로 자동 복제·동기화 | 대상이 많거나 값이 자주 바뀔 때 |
| cluster-wide 스코프 | 암호문 하나를 어느 네임스페이스에든 배치 | 편하지만 어디서든 풀린다 — 남용 금지 |
# Reflector — 원본이 "퍼져라"라고 지정하는 방식apiVersion: v1kind: Secretmetadata: name: wildcard-tls namespace: gateway-system annotations: reflector.v1.k8s.emberstack.com/reflection-allowed: "true" reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: "team-.*" reflector.v1.k8s.emberstack.com/reflection-auto-enabled: "true" reflector.v1.k8s.emberstack.com/reflection-auto-namespaces: "team-.*"③ 값이 바뀌면 — 회전과 반영
섹션 제목: “③ 값이 바뀌면 — 회전과 반영”-
값을 바꾼다 — 원천에서 먼저. S3 키라면 스토리지에서 새 키를 발급하고, DB 비밀번호라면 DB에서 바꾼다.
-
다시 봉인해 커밋한다(
--merge-into) 또는 금고에 새 값을 넣는다(ESO면 이걸로 끝). -
파드가 새 값을 읽게 한다. 여기가 빠지기 쉬운 단계다 — 환경변수로 주입한 Secret은 파드를 재시작해야 바뀐다. (볼륨 마운트는 시간이 지나면 갱신되지만, 앱이 파일을 다시 읽어야 한다.)
# Reloader — Secret이 바뀌면 그 워크로드를 자동 롤링 재시작metadata:annotations:reloader.stakater.com/auto: "true" -
옛 값을 폐기한다. 새 키가 도는 것을 확인한 뒤에 — 순서를 바꾸면 그 사이에 인증 실패가 난다.
④ 잃었을 때 — 복구 순서
섹션 제목: “④ 잃었을 때 — 복구 순서”11장의 클러스터 복구 절차 안에서 시크릿은 아주 이른 단계에 온다. Argo CD가 동기화를 시작하기 전에 컨트롤러가 옛 키를 들고 있어야 하기 때문이다. 먼저 금고에서 sealing key 세트 백업을 꺼낸다. 아래는 앞의 SOPS/age 선택 레시피를 썼을 때의 예다. 다른 금고를 골랐다면 1~2단계만 그 금고의 복원 절차로 바꾼다.
-
age 개인키를 오프라인 금고에서 꺼낸다. 이게 없으면 암호화된 백업을 못 푼다
-
SOPS로 sealing key 세트 백업을 복호화한다
-
Sealed Secrets 컨트롤러를 설치하고, 그 위에 옛 키를 주입한 뒤 재기동한다
터미널 창 kubectl apply -f sealed-secrets-keys.yamlkubectl -n sealed-secrets rollout restart deploy/sealed-secrets-controller -
Argo CD를 동기화한다 → Git의 SealedSecret이 런타임 Secret으로 되살아난다
-
Reflector 사본은 자동으로 다시 생긴다. 확인만 한다
-
그래도 안 풀리는 값이 있으면 그 값은 새로 발급하고, 원천(스토리지·DB·IdP)에도 반영한다
# ① 컨트롤러와 키 상태 (키가 여러 개면 갱신이 일어난 것 — 백업을 다시 떠야 한다)kubectl -n sealed-secrets get podskubectl -n sealed-secrets get secret -l sealedsecrets.bitnami.com/sealed-secrets-key \ -o custom-columns='NAME:.metadata.name,CREATED:.metadata.creationTimestamp'
# ② 봉인이 실제로 풀렸나 (SealedSecret → Secret이 생겼나)kubectl -n observability get sealedsecret,secret loki-s3kubectl -n sealed-secrets logs deploy/sealed-secrets-controller | tail -20# "no key could decrypt secret" → 스코프(이름·네임스페이스) 불일치 또는 키 유실
# ③ 백업이 최신인가 — 키 생성 시각과 백업 파일 시각을 비교한다sops --decrypt sealed-secrets-keys.yaml.enc | grep -c 'kind: Secret'
# ④ 복제가 됐나kubectl get secret wildcard-tls -A
# ⑤ 누가 읽을 수 있나kubectl auth can-i --list -n database --as=system:serviceaccount:apps:web | grep secret| 증상 | 흔한 원인 | 확인 |
|---|---|---|
no key could decrypt secret | 이름·네임스페이스가 봉인 때와 다르다 | 스코프 표. 해당 위치로 다시 봉인 |
| SealedSecret은 있는데 Secret이 안 생김 | 컨트롤러 미기동 · 네임스페이스 오타 | 컨트롤러 로그 |
| 값을 바꿨는데 앱이 옛 값을 씀 | 파드가 재시작 안 됨 | Reloader annotation, rollout restart |
| 다른 네임스페이스에서 Secret을 못 찾음 | 네임스페이스 경계 | 따로 봉인 / Reflector / 스코프 |
| 복구 후 일부만 안 풀림 | 그 값이 더 새 키로 봉인됐다 | 백업 시점 확인 → 그 값만 재발급 |
| ESO가 값을 못 가져옴 | 금고 인증·경로·권한 | kubectl describe externalsecret |
10장 요약
섹션 제목: “10장 요약”Secret의data는 암호문이 아니라 base64다. 저장 시 암호화와 접근 통제는 별도로 설계한다- “시크릿 관리”에는 네 개의 다른 문제가 있다 — 저장 · 네임스페이스 배포 · 갱신 · 복구
- 이 덱의 기본 경로는 Sealed Secrets, 회전·감사가 업무가 되면 ESO다
- SOPS/age는 선택지다 — Git 암호화 경로로 쓰거나 sealing key 백업을 지키는 데 쓸 수 있다
- 제일 잘 데는 곳은 스코프다 — 이름·네임스페이스를 바꾸면 복호화가 거부된다
- Secret은 네임스페이스를 못 넘는다. 따로 봉인 / Reflector / cluster-wide 중에 고른다
- 값을 바꾸면 파드를 재시작해야 반영된다 (Reloader)
- 값보다 주변으로 새는 것을 조심한다 — 셸 히스토리 · etcd 스냅샷 · UI · RBAC
- SOPS/age 레시피를 골랐다면 복구는 age 개인키 → sealing key 세트 → 컨트롤러 → 동기화 순이다