Compare commits

..
Author SHA1 Message Date
DongHyeonkaandClaude Opus 5 027c24ee27 docs: D-3 — only RBAC actually hides anything
Every secret in the lab prints in four commands, while kubectl describe shows just a byte count and creates the impression that something is hidden. k3s reports encryption at rest disabled and the plaintext password is present in state.db, so one node disk carries the whole cluster's secrets, and inside the pod they are ordinary environment variables visible to exec, /proc and crash dumps.

The default service account cannot read secrets, which makes RBAC the one control doing real work here and the thing worth tightening.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 15:07:35 +09:00
DongHyeonkaandClaude Opus 5 df140ab218 docs: D-2 — rolling back the image does not roll back the schema
Downgrading from 26.7.0 to 26.0 fails with liquibase ValidationFailedException on a changeset checksum, which is stricter than an unknown migration: the old version knows the changeset but its definition differs. The pod goes CrashLoopBackOff and never starts.

The StatefulSet stopped the rollout at the first pod, so the other kept serving and the front door stayed at 200, which replica 1 would not have done. The failed start never touched the schema, so restoring the image was enough; had the migration already applied, the D-1 database restore would have been the only way back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 15:05:36 +09:00
9 changed files with 549 additions and 0 deletions
@@ -0,0 +1,9 @@
=== D-1 의 교훈: 업그레이드 전에 백업한다 ===
백업: 396333 bytes
=== 현재 버전과 스키마 상태 ===
quay.io/keycloak/keycloak:26.7.0
총 마이그레이션 수: 210
=== 로그인 상태 만들기 (업그레이드 후 살아남는지 볼 것) ===
현재 세션: 4
@@ -0,0 +1,17 @@
=== ★ 롤백 시도: 26.7.0 → 26.0 ===
시각: 15:02:20
statefulset.apps/keycloak image updated
+20초 keycloak-0:Running(1/1) keycloak-1:Running(0/1)
+40초 keycloak-0:Running(1/1) keycloak-1:Running(0/1)
+60초 keycloak-0:Running(1/1) keycloak-1:Running(0/1)
+80초 keycloak-0:Running(1/1) keycloak-1:Error(0/1)
+100초 keycloak-0:Running(1/1) keycloak-1:Running(0/1)
+120초 keycloak-0:Running(1/1) keycloak-1:Error(0/1)
+140초 keycloak-0:Running(1/1) keycloak-1:CrashLoopBackOff(0/1)
+160초 keycloak-0:Running(1/1) keycloak-1:Running(0/1)
=== 새 파드가 무엇을 말하는가 ===
2026-09-04 06:03:25,877 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) ERROR: Failed to start server in (production) mode
2026-09-04 06:03:25,877 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) ERROR: liquibase.exception.ValidationFailedException: Validation Failed:
2026-09-04 06:03:25,877 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) ERROR: Validation Failed:
2026-09-04 06:03:25,877 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) For more details run the same command passing the '--verbose' option. Also you can use '--help' to see the detai
@@ -0,0 +1,20 @@
=== 서비스는 살아 있는가 (StatefulSet 롤링이 막아줬다) ===
https://auth.hyeonworks.com/realms/master HTTP 200
Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice
ready 주소: [10.42.1.140]sed: -e expression #1, char 27: unknown option to 's'
=== Liquibase 오류 상세 ===
2026-09-04 06:03:25,877 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) ERROR: liquibase.exception.ValidationFailedException: Validation Failed:
1 changesets check sum
2026-09-04 06:03:25,877 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) ERROR: Validation Failed:
1 changesets check sum
=== ★ 앞으로 되돌린다 (26.7.0) ===
statefulset.apps/keycloak image updated
partitioned roll out complete: 2 new pods have been updated...
keycloak-0 1/1 Running 0 10m
keycloak-1 1/1 Running 0 28s
=== 데이터는 무사한가 ===
realms|clients|migrations|sessions = 2|15|210|4
외부 진입점 HTTP 200
@@ -0,0 +1,16 @@
# D-2 — 버전 업그레이드 증거
2026-09-04 17:0517:15 KST
해설: [`docs/experiment-d2-version-upgrade.md`](../../experiment-d2-version-upgrade.md)
| 파일 | 무엇을 보여주는가 |
|---|---|
| `01-pre-upgrade.txt` | 백업 396KB · 이미지 26.7.0 · **마이그레이션 210건** · 세션 4 |
| `02-rollback-attempt.txt` | 26.0 으로 내리자 `Running(0/1) → Error → CrashLoopBackOff`. **`liquibase.exception.ValidationFailedException`** |
| `03-roll-forward.txt` | **서비스는 `HTTP 200` 유지**(ready 주소 1개) · 오류 원인 `1 changesets check sum` · 26.7.0 복귀 후 마이그레이션 210·세션 4 그대로 |
## 핵심 세 줄
1. **롤백은 안 된다.** 체크섬이 안 맞아 Liquibase 가 기동 자체를 거부한다 — "모르는 변경"이 아니라 "아는 변경인데 정의가 다르다".
2. **StatefulSet 이 사고를 절반에서 멈춰줬다.** 한 파드가 남아 외부 200 을 유지했다. replica 1 이었다면 전면 장애다.
3. **실패한 기동은 스키마를 안 건드렸다.** 그래서 이미지만 되돌려도 복구됐다 — 이미 적용된 뒤였다면 DB 복구(D-1)가 필요하다.
@@ -0,0 +1,17 @@
=== 실험대의 Secret 목록 ===
bff-secrets Opaque keys=1
keycloak-lab-secrets Opaque keys=2
oauth2-proxy-secrets Opaque keys=3
=== ★ base64 는 암호화가 아니다 — 한 줄로 읽힌다 ===
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
@@ -0,0 +1,23 @@
=== k3s 의 데이터 저장소 ===
Encryption Status: Disabled, no configuration file found
=== 저장 파일 ===
total 23336
drwx------ 2 root root 4096 Sep 2 09:12 .
drwx------ 8 root root 4096 Sep 4 03:23 ..
-rw-r--r-- 1 root root 13078528 Sep 4 06:05 state.db
-rw-r--r-- 1 root root 32768 Sep 4 06:06 state.db-shm
-rw-r--r-- 1 root root 10769712 Sep 4 06:06 state.db-wal
=== ★ 저장 파일에서 비밀번호가 그대로 보이는가 ===
state.db 안의 평문 일치: 2
=== 평문이 저장 파일에 있다는 것을 눈으로 ===
client secret 평문 등장 횟수: 0
=== 누가 Secret 을 읽을 수 있는가 ===
default SA: no
(Role 이 없으면 네임스페이스에 별도 제한이 없다는 뜻)
=== 파드 안에서는 어떻게 보이는가 ===
KEYCLOAK_CLIENT_SECRET=bff-lab-secret
BFF_DB_PASSWORD=lab-postgres-change-me
@@ -0,0 +1,15 @@
# D-3 — 비밀 관리 증거
2026-09-04 17:1517:25 KST
해설: [`docs/experiment-d3-secret-management.md`](../../experiment-d3-secret-management.md)
| 파일 | 무엇을 보여주는가 |
|---|---|
| `01-base64-not-encryption.txt` | 실험대의 **모든 비밀이 명령 네 줄로** 평문 출력. `describe``14 bytes` 만 보여줘 착각을 준다 |
| `02-at-rest.txt` | **`Encryption Status: Disabled`** · `state.db` 안에 비밀번호 평문 **2회 일치** · 파드 안에서는 `KEYCLOAK_CLIENT_SECRET=bff-lab-secret` 환경변수 · `default` SA 는 **읽을 수 없음** |
## 핵심 세 줄
1. **base64 는 감추려는 것이 아니라 YAML 에 바이트를 담기 위한 것이다.** `describe` 가 값을 가려 안전하다는 착각을 준다.
2. **저장소 암호화가 꺼져 있고 노드 디스크에 평문이 있다.** 노드 디스크 하나가 전 클러스터의 비밀이다.
3. **네 경로 중 RBAC 만 제 역할을 한다.** 그것이 실질적 방어선이며, 관리자에게는 아무 방어가 없다.
+213
View File
@@ -0,0 +1,213 @@
# D-2 — 버전을 올리고 내릴 때 무엇이 일어나는가
브랜치 `feature/keycloak-d2-version-upgrade` ·
증거 [`docs/evidence/d2-version-upgrade/`](evidence/d2-version-upgrade/) ·
2026-09-04 17:0517:15 KST
선행: [`D-1`](experiment-d1-backup-restore.md) — **백업이 전제다** ·
[`A-8`](experiment-a8-rolling-restart.md) — 롤링 재시작이 안전하다는 것이 전제
---
## 0. 결론부터
| 확인 | 결과 |
|---|---|
| **롤백이 되는가** | **★ 안 된다.** `liquibase ValidationFailedException: 1 changesets check sum` |
| 그때 서비스는 | **★ 살아 있다.** 한 파드가 남아 외부 `200` |
| 앞으로 되돌리기 | **된다.** 정상 복구, 데이터 무사 |
| 세션 | **유지** (4개 그대로) |
**"롤백 계획"을 세워두었다면 그 계획은 동작하지 않는다.**
대신 **StatefulSet 의 롤링 업데이트가 사고를 절반에서 멈춰줬다.**
---
## 1. 전제 — 먼저 백업한다
D-1 에서 확인한 절차 그대로.
```bash
kubectl -n keycloak-lab exec deploy/postgres -- pg_dump -U keycloak -d keycloak \
--clean --if-exists > /tmp/pre-upgrade.sql
```
```
백업: 396333 bytes
현재 이미지: quay.io/keycloak/keycloak:26.7.0
총 마이그레이션 수: 210
현재 세션: 4
```
### 개념 — `databasechangelog`
Keycloak 은 **Liquibase** 로 스키마를 관리한다. 적용한 변경 하나하나를
`databasechangelog` 테이블에 기록한다.
| 컬럼 | 뜻 |
|---|---|
| `id` / `author` / `filename` | 변경을 식별 |
| **`md5sum`** | **그 변경 정의의 체크섬** |
| `orderexecuted` | 적용 순서 |
**210개가 쌓여 있다.** 이것이 "이 DB 는 어느 버전까지 올라갔는가"의 기록이다.
---
## 2. 롤백을 시도했다 — 26.7.0 → 26.0
```bash
kubectl -n keycloak-lab set image statefulset/keycloak keycloak=quay.io/keycloak/keycloak:26.0
```
```
+20초 keycloak-0:Running(1/1) keycloak-1:Running(0/1)
+80초 keycloak-0:Running(1/1) keycloak-1:Error(0/1)
+140초 keycloak-0:Running(1/1) keycloak-1:CrashLoopBackOff(0/1)
```
```
ERROR: Failed to start server in (production) mode
ERROR: liquibase.exception.ValidationFailedException: Validation Failed:
1 changesets check sum
```
### 왜 실패하는가 — 체크섬 불일치
```
26.7.0 이 적용한 변경 → databasechangelog 에 md5sum 기록
26.0 이 기동하며 검증 → 자기가 아는 그 변경의 md5sum 과 비교
└─ 다르다 → ValidationFailedException
```
**"모르는 변경이 있다" 가 아니라 "아는 변경인데 정의가 다르다" 이다.**
같은 changeset 이 버전 사이에 수정된 것이며, **더 엄격한 실패**다.
> **Liquibase 는 안전을 위해 기동 자체를 거부한다.**
> 스키마를 반쯤 아는 상태로 서비스하느니 안 뜨는 쪽을 고른 설계다.
---
## 3. 그런데 서비스는 살아 있었다
```
https://auth.hyeonworks.com/realms/master HTTP 200
ready 주소: [10.42.1.140] ← 한 파드만
statefulset desired/ready/updated: 2 / 1 / 1
```
**StatefulSet 의 롤링 업데이트가 한 번에 하나씩 바꾸기 때문**이다.
```
keycloak-1 을 26.0 으로 → 기동 실패 → Ready 가 안 됨
└─ StatefulSet 은 keycloak-0 을 건드리지 않는다
└─ keycloak-0 (26.7.0) 이 계속 서비스한다
```
**A-8 에서 "무중단은 replica ≥ 2 와 readiness 의 조합" 이라고 썼는데,
여기서는 그 조합이 잘못된 배포를 절반에서 멈춰줬다.**
| replica 1 이었다면 | |
|---|---|
| 유일한 파드가 CrashLoopBackOff | **전면 장애** |
| 되돌리려면 사람이 개입 | 그동안 계속 다운 |
---
## 4. 앞으로 되돌리기
```bash
kubectl -n keycloak-lab set image statefulset/keycloak keycloak=quay.io/keycloak/keycloak:26.7.0
```
```
partitioned roll out complete: 2 new pods have been updated...
keycloak-0 1/1 Running
keycloak-1 1/1 Running 28s
realms|clients|migrations|sessions = 2|15|210|4
외부 진입점 HTTP 200
```
**정상 복구.** 마이그레이션 수도 세션도 그대로다 — **실패한 기동은 스키마를
건드리지 못했다.** Liquibase 가 검증 단계에서 멈췄기 때문이다.
---
## 5. 그래서 업그레이드 계획은 어떻게 세워야 하는가
```
✘ "문제가 생기면 이미지 태그를 되돌린다"
└─ 스키마가 이미 바뀌었으면 옛 버전이 안 뜬다
✔ "문제가 생기면 백업에서 DB 를 되돌리고 이미지도 되돌린다"
└─ D-1 에서 확인한 절차가 여기서 필요하다
```
| 단계 | |
|---|---|
| 1 | **백업** (D-1) — 이것이 유일한 되돌리기 수단이다 |
| 2 | 이미지 태그 변경 |
| 3 | **첫 파드만 관찰** — StatefulSet 이 멈춰준다 |
| 4 | 실패하면 **이미지를 되돌린다** (스키마가 안 바뀌었으면 이것으로 충분) |
| 5 | 스키마가 이미 바뀌었으면 **DB 도 복구**해야 한다 |
**4와 5를 가르는 것이 "Liquibase 가 검증에서 멈췄는가, 이미 적용했는가" 다.**
이번에는 검증에서 멈춰 4로 끝났다.
---
## 6. 이 실험이 확인한 것과 못 한 것
| | |
|---|---|
| ✔ 롤백이 안 된다는 것 | 체크섬 불일치로 기동 거부 |
| ✔ 실패가 안전하게 격리된다 | StatefulSet + readiness |
| ✔ 실패한 기동은 스키마를 안 건드린다 | 마이그레이션 210 그대로 |
| ✘ **정방향 업그레이드** | **26.7.0 보다 새 이미지가 없어 시험하지 못했다** |
| ✘ 마이그레이션 중 장애 | 스키마 변경 도중 죽으면? |
| ✘ 대규모 마이그레이션 시간 | 데이터가 작아 순식간이다 |
> **정방향을 시험하지 못한 것을 감춰서는 안 된다.**
> 다만 **역방향이 더 위험한 방향**이고, 그것이 실패한다는 사실이
> "롤백 계획" 을 무효로 만든다는 점에서 실무적으로 더 중요한 결과다.
>
> 새 버전이 나오면 같은 절차(백업 → 태그 변경 → 첫 파드 관찰)로 반복한다.
---
## 7. 재현 절차 (명령어)
```bash
# 1. 백업 먼저 (D-1)
kubectl -n keycloak-lab exec deploy/postgres -- pg_dump -U keycloak -d keycloak \
--clean --if-exists > pre-upgrade.sql
# 2. 현재 마이그레이션 수를 기록
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
"select count(*) from databasechangelog"
# 3. 버전 변경
kubectl -n keycloak-lab set image statefulset/keycloak keycloak=quay.io/keycloak/keycloak:<tag>
# 4. ★ 첫 파드만 본다. 실패하면 StatefulSet 이 멈춘다
kubectl -n keycloak-lab get pods -w
kubectl -n keycloak-lab logs keycloak-1 | grep -iE "liquibase|changeset|validation"
# 5. 서비스가 살아 있는지 (남은 파드가 받는다)
kubectl -n keycloak-lab get endpoints keycloak -o jsonpath='{.subsets[*].addresses[*].ip}'
# 6. 되돌리기 — 스키마가 안 바뀌었으면 이미지만으로 충분
kubectl -n keycloak-lab set image statefulset/keycloak keycloak=quay.io/keycloak/keycloak:26.7.0
```
---
## 8. 다음에 남기는 것
| | |
|---|---|
| **D-3** 비밀 관리 | 업그레이드 시 Secret 도 같이 검토된다 |
| 운영 | **롤백 = 백업 복구**다. 태그만 되돌리는 계획은 반쪽이다 |
| 운영 | **replica ≥ 2** 가 잘못된 배포를 절반에서 멈춘다 |
| 미검증 | 정방향 업그레이드, 마이그레이션 중 장애, 대규모 소요 시간 |
+219
View File
@@ -0,0 +1,219 @@
# D-3 — Secret 은 정말 감춰지는가
브랜치 `feature/keycloak-d3-secret-management` ·
증거 [`docs/evidence/d3-secret-management/`](evidence/d3-secret-management/) ·
2026-09-04 17:1517:25 KST
---
## 0. 결론부터
| 경로 | 감춰지는가 |
|---|---|
| `kubectl get secret -o jsonpath \| base64 -d` | **★ 한 줄로 읽힌다** |
| `kubectl describe secret` | 값을 숨긴다 — **그래서 안전하다고 착각한다** |
| **저장소(at rest)** | **★ 암호화 꺼져 있음.** 저장 파일에 평문이 있다 |
| **파드 안** | **★ 평범한 환경변수다** |
| RBAC 기본값 | **막는다**`default` 서비스계정은 못 읽는다 |
**"Secret 이니까 안전하다" 는 네 가지 중 하나(RBAC)만 맞다.**
---
## 1. 한 줄로 읽힌다
```bash
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. 저장소에는 평문으로 있다
```bash
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
```
```bash
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. 파드 안에서는 환경변수다
```bash
kubectl -n keycloak-lab exec <bff-pod> -- 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` |
| **크래시 덤프 · 오류 리포트** | 환경변수를 함께 담는 도구가 많다 |
| 자식 프로세스 | 상속된다 |
**볼륨으로 마운트하면 이 중 몇 가지가 줄어든다** — 파일 권한으로 제한할 수
있고 환경변수 덤프에 안 들어간다.
```yaml
volumeMounts:
- name: secrets
mountPath: /etc/secrets
readOnly: true
```
---
## 4. RBAC 은 실제로 막는다
```bash
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` 를 넣은 것이
그 최소한의 표시다.
---
## 6. 재현 절차 (명령어)
```bash
# 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 <pod> -- 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 이 유일하게 동작하는 방어선이다** |