Files
keycloak-pattern/docs/experiment-d3-secret-management.md
DongHyeonkaandClaude Opus 5 74c9b3cea7 docs: replace prose placeholders in reproduction steps with executable commands
The audit found ~80 placeholders, and the damaging ones were where the
measuring apparatus itself was prose rather than a command:

  a6  "( curl ... ) & 를 20개 띄우고 wait"  — the 22.2s headline came from this
  a3  "<로그인 반복, sid 를 /tmp/sids 에>"  — the whole RPO measurement
  a3  "<sid 목록>"                          — the control it is compared against
  a5  "<수신 파드IP>"                       — the injection
  a8  writes /tmp/tok, reads /tmp/rt        — self-inconsistent, sent an empty token
  b3  $KC / $RT / $NEW never assigned
  c2  bare kcadm.sh with no kubectl exec
  a1  conntrack tuples written by hand, though the direction flips per restart

Each is now a shell-expandable form: pod IPs from jsonpath, the admin password
from the secret, ids from kcadm --format csv, conntrack tuples derived from
"conntrack -L" with awk rather than transcribed.

Then the rewritten commands were executed against the live cluster, and one
of them failed — the 20-way load generator, written as "kubectl run --rm -i",
lost its output stream twice in a row. That is a trap this series already hit
once, and the rewrite reintroduced it. A-6 now uses a resident probe pod that
collects into a file and is cat-ed once; verified 20/20 lines.

Evidence: docs/evidence/followup/05-command-reproducibility.txt

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 17:01:54 +09:00

7.6 KiB
Raw Permalink Blame History

D-3 — Secret 은 정말 감춰지는가

브랜치 feature/keycloak-d3-secret-management · 증거 docs/evidence/d3-secret-management/ · 2026-09-04 17:1517:25 KST


구조

D-3 구조 — 네 경로 중 하나만 막는다

다이어그램 규약은 diagrams/_style.md. 실험대 전체 구조는 diagrams/lab-topology.svg.


0. 결론부터

경로 감춰지는가
kubectl get secret -o jsonpath | base64 -d ★ 한 줄로 읽힌다
kubectl describe secret 값을 숨긴다 — 그래서 안전하다고 착각한다
저장소(at rest) ★ 암호화 꺼져 있음. 저장 파일에 평문이 있다
파드 안 ★ 평범한 환경변수다
RBAC 기본값 막는다default 서비스계정은 못 읽는다

"Secret 이니까 안전하다" 는 네 가지 중 하나(RBAC)만 맞다.


1. 한 줄로 읽힌다

kubectl -n keycloak-lab get secret keycloak-lab-secrets \
  -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d
  keycloak-lab-secrets/POSTGRES_PASSWORD            = lab-postgres-change-me
  keycloak-lab-secrets/KC_BOOTSTRAP_ADMIN_PASSWORD  = lab-admin-change-me
  bff-secrets/KEYCLOAK_CLIENT_SECRET                = bff-lab-secret
  oauth2-proxy-secrets/COOKIE_SECRET_A              = lab-cookie-secret-aaaaaaaaaaaaaa

실험대의 모든 비밀이 명령 네 줄로 나온다.

describe 는 감춘다 — 그것이 함정이다

Type:  Opaque

Data
====
KEYCLOAK_CLIENT_SECRET:  14 bytes

바이트 수만 보여준다. 이것만 보면 "가려져 있구나" 싶다. get -o jsonpath 한 번이면 값이 나온다.

개념 — base64 는 인코딩이지 암호화가 아니다

목적 되돌리기
인코딩 (base64) 바이너리를 텍스트로 안전하게 옮기기 키 없이 누구나
암호화 키 없이는 못 읽게 하기 키가 있어야

Secret 이 base64 를 쓰는 이유는 감추려는 것이 아니라 YAML 에 임의 바이트를 담기 위해서다.


2. 저장소에는 평문으로 있다

ssh kc-lab-1 'sudo k3s secrets-encrypt status'
  Encryption Status: Disabled, no configuration file found

k3s 의 저장소 암호화가 꺼져 있다. 기본값이다.

  /var/lib/rancher/k3s/server/db/state.db      13MB
  /var/lib/rancher/k3s/server/db/state.db-wal  10MB
ssh kc-lab-1 'sudo grep -c "lab-postgres-change-me" /var/lib/rancher/k3s/server/db/state.db'
  state.db 안의 평문 일치: 2

저장 파일 안에 비밀번호가 그대로 있다.

그래서 무엇이 위험한가
노드 디스크를 얻으면 전 클러스터의 비밀
노드 백업/스냅샷 같은 것을 복사한다
A-4 에서 본 local-path PVC 같은 디스크에 있다

D-1 에서 "덤프를 같은 장애 도메인에 두면 백업이 아니다" 라고 썼는데, 여기서는 "노드 디스크 하나가 모든 비밀" 이다. 백업을 잘 챙겨도 그 백업 안에 비밀이 평문으로 들어간다.

k3s 는 --secrets-encryption 플래그로 켤 수 있다. 지금은 안 켜져 있다.


3. 파드 안에서는 환경변수다

kubectl -n keycloak-lab exec $(kubectl -n keycloak-lab get pod -l app=bff --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') -- sh -c 'env | grep -iE "secret|password"'
  KEYCLOAK_CLIENT_SECRET=bff-lab-secret
  BFF_DB_PASSWORD=lab-postgres-change-me

env 한 번이면 나온다.

새는 경로
kubectl exec 권한이 있는 사람 바로 본다
같은 파드의 다른 프로세스 /proc/<pid>/environ
크래시 덤프 · 오류 리포트 환경변수를 함께 담는 도구가 많다
자식 프로세스 상속된다

볼륨으로 마운트하면 이 중 몇 가지가 줄어든다 — 파일 권한으로 제한할 수 있고 환경변수 덤프에 안 들어간다.

volumeMounts:
  - name: secrets
    mountPath: /etc/secrets
    readOnly: true

4. RBAC 은 실제로 막는다

kubectl auth can-i get secrets -n keycloak-lab \
  --as=system:serviceaccount:keycloak-lab:default
  default SA: no

기본 서비스계정은 Secret 을 못 읽는다. 쿠버네티스의 기본값이 제한적이다.

네 가지 중 유일하게 제 역할을 하는 것이 RBAC 다. 그러므로 "누가 get secrets 를 할 수 있는가" 가 실질적인 방어선이며, 관리자 권한을 가진 사람에게는 아무 방어가 없다.

A-0 의 관측 스택에서 nodes/proxy 서브리소스를 따로 줘야 했던 것처럼, Secret 접근도 리소스 단위로 나눌 수 있다.


5. 그래서 무엇을 해야 하는가

   지금:  매니페스트에 stringData 평문  →  git 에 커밋되면 끝
          k3s 저장소 암호화 꺼짐
          파드 환경변수
단계 얻는 것
① 매니페스트에서 값을 빼고 .example 만 커밋 git 유출을 막는다
k3s --secrets-encryption 활성화 노드 디스크 유출을 막는다
③ 환경변수 대신 볼륨 마운트 프로세스·덤프 유출을 줄인다
SealedSecret / 외부 KMS 매니페스트에 암호문만 남는다
RBAC 최소화 유일하게 이미 동작하는 방어선을 좁힌다

이 실험대는 ①~④ 중 아무것도 안 하고 있다. 실험 목적으로는 의도적이지만, 그 사실을 기록해두지 않으면 그대로 운영에 옮겨간다.

이 실험대의 비밀들은 이미 문서에 있다

lab-postgres-change-me, bff-lab-secret 같은 값이 이 저장소의 매니페스트와 문서에 그대로 적혀 있다. 실험대 전용이며 외부에서 접근할 수 없는 값이지만, "실험대니까 괜찮다" 가 습관이 되면 위험하다. 이름에 change-me 를 넣은 것이 그 최소한의 표시다.



증거 파일

증거 수집 시각: 2026-09-04 15:05 15:06 KST (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)

파일 종류
01-base64-not-encryption.txt 터미널 원문
02-at-rest.txt 터미널 원문

파일별 상세는 evidence/d3-secret-management/README.md.

6. 재현 절차 (명령어)

# 1. 한 줄로 읽힌다
kubectl -n keycloak-lab get secret keycloak-lab-secrets \
  -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d

# 2. describe 는 감춘다 — 대조
kubectl -n keycloak-lab describe secret bff-secrets

# 3. 저장소 암호화 여부
ssh kc-lab-1 'sudo k3s secrets-encrypt status'

# 4. 저장 파일에 평문이 있는가
ssh kc-lab-1 'sudo grep -c "lab-postgres-change-me" /var/lib/rancher/k3s/server/db/state.db'

# 5. 파드 안에서는 환경변수
kubectl -n keycloak-lab exec keycloak-0 -- sh -c 'env | grep -i secret'

# 6. 누가 읽을 수 있는가
kubectl auth can-i get secrets -n keycloak-lab \
  --as=system:serviceaccount:keycloak-lab:default

7. 다음에 남기는 것

D-4 인증서 갱신 인증서 개인키도 같은 문제다
B-6 암호화 key key 를 Secret 에 두면 이 실험의 결론이 그대로 적용된다
운영 RBAC 이 유일하게 동작하는 방어선이다