콘텐츠로 이동
Study Note온프렘 쿠버네티스

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 읽기 권한은 생각보다 넓게 퍼져 있다

네 개의 다른 문제 — 여기서부터 갈라서 본다

섹션 제목: “네 개의 다른 문제 — 여기서부터 갈라서 본다”

“시크릿 관리”라는 말에 성질이 다른 네 문제가 섞여 있다. 이걸 안 가르면 도구 이름만 늘어난다.

Git에 무엇을 두나·여러 곳에 어떻게 주나·원천 값이 바뀌면·클러스터를 잃으면 네 질문과 각각의 답을 짝지은 표
문제헷갈리는 지점
① 저장Sealed Secrets와 SOPS+age는 둘 다 Git에 암호문을 둘 수 있다. 차이는 누가 언제 푸는가다
② 배포Secret은 네임스페이스를 못 넘는다. 와일드카드 TLS·이미지 pull 시크릿이 전부 여기 걸린다
③ 갱신값을 바꿔도 이미 뜬 파드는 모른다. 자동 반영은 별도 장치가 필요하다
④ 복구이게 제일 무섭다 — 복호화 키 세트를 잃으면 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에 비밀번호를 그대로 쓰면 셸 히스토리에 남는다.

선택 레시피 — sealing key 백업을 SOPS와 age로 지키기

섹션 제목: “선택 레시피 — sealing key 백업을 SOPS와 age로 지키기”

위 백업 파일에는 개인키가 평문으로 들어 있다. 그대로 두면 그 파일 자체가 최대 위험이다. 조직에서 이미 쓰는 암호화 금고가 있다면 거기에 넣으면 된다. 그런 체계가 없을 때 쓸 수 있는 작은 오프라인 구성이 SOPS + age다 — age 공개키로 잠그고, 클러스터 밖의 age 개인키로 푼다. SOPS 공식 문서에서 지원 파일 형식과 age 키 사용법을 확인할 수 있다.

오프라인 금고의 age 개인키로 sealing key 백업을 복호화해 Sealed Secrets 컨트롤러에 주입하고, Git의 SealedSecret 암호문이 그 컨트롤러를 거쳐 런타임 Secret과 파드에 닿는 사슬
터미널 창
# 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: v1
kind: Secret
metadata:
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-.*"
  1. 값을 바꾼다 — 원천에서 먼저. S3 키라면 스토리지에서 새 키를 발급하고, DB 비밀번호라면 DB에서 바꾼다.

  2. 다시 봉인해 커밋한다(--merge-into) 또는 금고에 새 값을 넣는다(ESO면 이걸로 끝).

  3. 파드가 새 값을 읽게 한다. 여기가 빠지기 쉬운 단계다 — 환경변수로 주입한 Secret은 파드를 재시작해야 바뀐다. (볼륨 마운트는 시간이 지나면 갱신되지만, 앱이 파일을 다시 읽어야 한다.)

    # Reloader — Secret이 바뀌면 그 워크로드를 자동 롤링 재시작
    metadata:
    annotations:
    reloader.stakater.com/auto: "true"
  4. 옛 값을 폐기한다. 새 키가 도는 것을 확인한 뒤에 — 순서를 바꾸면 그 사이에 인증 실패가 난다.

11장의 클러스터 복구 절차 안에서 시크릿은 아주 이른 단계에 온다. Argo CD가 동기화를 시작하기 전에 컨트롤러가 옛 키를 들고 있어야 하기 때문이다. 먼저 금고에서 sealing key 세트 백업을 꺼낸다. 아래는 앞의 SOPS/age 선택 레시피를 썼을 때의 예다. 다른 금고를 골랐다면 1~2단계만 그 금고의 복원 절차로 바꾼다.

  1. age 개인키를 오프라인 금고에서 꺼낸다. 이게 없으면 암호화된 백업을 못 푼다

  2. SOPS로 sealing key 세트 백업을 복호화한다

  3. Sealed Secrets 컨트롤러를 설치하고, 그 위에 옛 키를 주입한 뒤 재기동한다

    터미널 창
    kubectl apply -f sealed-secrets-keys.yaml
    kubectl -n sealed-secrets rollout restart deploy/sealed-secrets-controller
  4. Argo CD를 동기화한다 → Git의 SealedSecret이 런타임 Secret으로 되살아난다

  5. Reflector 사본은 자동으로 다시 생긴다. 확인만 한다

  6. 그래도 안 풀리는 값이 있으면 그 값은 새로 발급하고, 원천(스토리지·DB·IdP)에도 반영한다

터미널 창
# ① 컨트롤러와 키 상태 (키가 여러 개면 갱신이 일어난 것 — 백업을 다시 떠야 한다)
kubectl -n sealed-secrets get pods
kubectl -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-s3
kubectl -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
  • Secret의 data는 암호문이 아니라 base64다. 저장 시 암호화와 접근 통제는 별도로 설계한다
  • “시크릿 관리”에는 네 개의 다른 문제가 있다 — 저장 · 네임스페이스 배포 · 갱신 · 복구
  • 이 덱의 기본 경로는 Sealed Secrets, 회전·감사가 업무가 되면 ESO다
  • SOPS/age는 선택지다 — Git 암호화 경로로 쓰거나 sealing key 백업을 지키는 데 쓸 수 있다
  • 제일 잘 데는 곳은 스코프다 — 이름·네임스페이스를 바꾸면 복호화가 거부된다
  • Secret은 네임스페이스를 못 넘는다. 따로 봉인 / Reflector / cluster-wide 중에 고른다
  • 값을 바꾸면 파드를 재시작해야 반영된다 (Reloader)
  • 값보다 주변으로 새는 것을 조심한다 — 셸 히스토리 · etcd 스냅샷 · UI · RBAC
  • SOPS/age 레시피를 골랐다면 복구는 age 개인키 → sealing key 세트 → 컨트롤러 → 동기화 순이다