Files
keycloak-pattern/docs/guides/experiments/d3-secret-management.md
DongHyeonkaandClaude Opus 5 6f6ab86345 docs(guides): reproduction guides for all 26 experiments
Written by subagents running under the writing-practitioner-guides skill,
one guide per experiment, 22,566 lines. Each walks a reader from baseline
capture through injection, injection verification, observation and recovery.

Section 3 carries the weight in most of them. Injection failed silently nine
times in this lab, and a failed injection looks exactly like no effect — so
the guides verify the target is actually in the intended state before
reading any result. A-4 makes virsh list the only proof because the node
reads Ready for 40 seconds after the machine is off; A-5 makes the packet
counter the sole go/no-go because a rule on the wrong node produces an empty
result that reads like a finding; A-6 quotes the run where 적용완료 was
printed between four Cannot find device "eth0" lines.

The traps the guides are built around are ones that invert a conclusion
rather than merely annoy:

  A-0   emptying the session table without a restart leaves cache entries
        that get counted as replication arriving
  A-2   dropping -o /dev/null fuses body and status into one string
  A-3   presence of "ready to accept connections" instead of its timestamp
  B-2   row count alone reads an UPDATE as nothing having happened
  B-4   tr ',' '\n' splits ["admin","editor"] so only admin is seen
  B-7   no login screen means the cookie died and SSO re-authenticated
  C-1   counting sessions without joining realm counts your own kcadm one
  D-1   kubectl exec without -i restores nothing and still exits 0
  D-4a  "ran with error output" is what success looks like

Every quoted block is copied from docs/evidence/ and marked 실측; reshaped
commands are marked 미검증 rather than passed off as measured. Where a source
document carries a ★ correction the guides follow the corrected claim — A-7's
REVOKED_TOKEN hypothesis, C-1's session count, B-2's schema attribution.

Two hazards are stated rather than smoothed over: B-6 deletes a key that
cannot be recreated, and D-1/D-4 need host sudo, which asks for a password,
so those steps say a person must type them.

Audit over all 26: 672 interpretation pairs, 486 evidence citations, 117
undo sections, and zero occurrences of the patterns the skill forbids —
no python data processing, no deprecated kubectl get endpoints, no
placeholders, no bare kcadm.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 18:29:00 +09:00

563 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# D-3 재현 가이드 — Secret 이 어디까지 감춰지는지 네 경로로 직접 본다
해설 문서: [`docs/experiment-d3-secret-management.md`](../../experiment-d3-secret-management.md) ·
증거 원문: [`docs/evidence/d3-secret-management/`](../../evidence/d3-secret-management/)
## 이 가이드가 끝나면
당신 터미널에서 이것들을 **직접 본다.**
| 보게 되는 것 | 어디서 |
|---|---|
| `describe``14 bytes` 만 보여주는 것 | `describe secret` |
| 같은 값이 **한 줄로** 평문이 되는 것 | `get -o jsonpath \| base64 -d` |
| 저장소 암호화가 **꺼져 있는** 것 | `k3s secrets-encrypt status` |
| 노드 디스크의 저장 파일 안에 **평문이 있는** 것 | `grep -c` on `state.db` |
| **그 grep 이 `0` 을 돌려주는데도 안전하지 않은** 것 | 같은 명령, 다른 키 |
| 파드 안에서는 그냥 **환경변수**인 것 | `env` · `/proc/1/environ` |
| RBAC 은 실제로 막는 것 | `auth can-i` |
## 전제
- 명령은 **`kc-lab-1` 에서** 친다. `kubectl``sudo` 로 쓴다.
**k3s 서버의 저장 파일도 이 노드에 있다** — 그래서 4-2 를 여기서 칠 수 있다.
- 게스트(`kc-lab-1`/`kc-lab-2`)의 `sudo` 는 **무암호**다. 호스트와 다르다.
- 네임스페이스는 `keycloak-lab` 이다.
- `jq` 는 이 실험대 어디에도 없다. 이 가이드는 `jq` 를 쓰지 않는다.
- [`B-6`](../../experiment-b6-key-rotation.md) ·
[`B-7`](../../experiment-b7-cookie-secret-rotation.md) 를 이미 했다면 이 실험의
결론이 그 key 들에도 그대로 적용된다는 것을 알고 있을 것이다.
## 주의 — 이건 비밀을 화면에 띄우는 실험이다
**이 실험의 명령 몇 개는 비밀번호를 터미널에 그대로 찍는다.** 그게 결론이기
때문에 피할 수 없지만, 그 값은 **스크롤백·화면 공유·터미널 로그**에 남는다.
이 가이드는 그래서 이렇게 한다.
- **남의 진짜 비밀은 길이(`wc -c`)와 키 이름까지만 본다.**
- **값을 찍어 봐야 하는 곳은 이 실험용으로 직접 만든 카나리아 Secret 을 쓴다**
(2절). 지워도 되는 값이므로 찍어도 된다.
- 실측으로 실린 값들은 **이 저장소의 매니페스트와 문서에 이미 적혀 있는
실험대 전용 값**이다(`change-me` 가 이름에 들어 있는 이유가 그것이다).
파괴적인 단계는 없다. 만드는 것은 카나리아 Secret 하나뿐이고
[5. 복구](#5-복구) 에서 지운다. 전 구간 약 15분.
## 표시 규약
| 표시 | 뜻 |
|---|---|
| **실측** | 2026-09-04 15:0515:06 KST 실행 기록의 **출력 원문**. 증거 파일에 그대로 있다 |
| **형태** | 값이 매번 달라지는 출력. 모양만 보이고 숫자는 당신 것과 다르다 |
| **미검증** | 손으로 치기 좋게 이 가이드에서 고친 형태이거나, 이 실험이 하지 않은 확장 |
---
# 0. 왜 이 실험을 하는가
「비밀번호를 Secret 으로 옮겼습니다」는 리뷰에서 통과 도장을 받는 문장이다.
**그 문장이 실제로 무엇을 막아 주는지**를 네 경로로 나눠 판정한다.
| # | 경로 | 누가 쓰나 | 예측 |
|---|---|---|---|
| ① | 쿠버네티스 API (`get secret`) | 클러스터에 접근하는 사람 | ? |
| ② | **노드 디스크의 저장 파일** | 디스크·백업·스냅샷을 얻은 사람 | ? |
| ③ | **파드 안의 프로세스** | `exec` 권한이 있는 사람, 크래시 덤프 | ? |
| ④ | RBAC | 권한이 없는 주체 | ? |
**핵심 개념부터 짚는다.**
| | 목적 | 되돌리기 |
|---|---|---|
| **인코딩** (base64) | 바이너리를 텍스트로 안전하게 **옮기기** | **키 없이 누구나** |
| 암호화 | 키 없이는 못 **읽게** 하기 | 키가 있어야 |
**Secret 이 base64 를 쓰는 이유는 감추려는 것이 아니라 YAML 에 임의 바이트를
담기 위해서다.** 그런데 `kubectl describe` 가 값을 가려서 보여주기 때문에
「가려져 있구나」라는 인상이 남는다 — 이 실험은 그 인상과 사실 사이의 거리를
잰다.
---
# 1. 기준선 — 무엇이 있는지부터 본다
## 1-1. Secret 목록
**확인**
```bash
sudo kubectl -n keycloak-lab get secret
```
**실측** — [`01-base64-not-encryption.txt`](../../evidence/d3-secret-management/01-base64-not-encryption.txt)
```
bff-secrets Opaque keys=1
keycloak-lab-secrets Opaque keys=2
oauth2-proxy-secrets Opaque keys=3
```
> 실측 줄은 원래 실행 스크립트가 정리해 찍은 것이다. 손으로 치면
> `NAME / TYPE / DATA / AGE` 네 칸이 나오고, `DATA` 열이 위의 `keys=` 에 해당한다.
**어디를 봐야 하는가** — 이름과 `DATA` 열(키 개수). **`TYPE``Opaque`
것도 본다** — 「불투명」이라는 이름이지만 그건 쿠버네티스가 내용 구조를 모른다는
뜻이지 **감춘다는 뜻이 아니다.**
## 1-2. `describe` 는 값을 감춘다
**확인**
```bash
sudo kubectl -n keycloak-lab describe secret bff-secrets
```
**실측** — [`01-base64-not-encryption.txt`](../../evidence/d3-secret-management/01-base64-not-encryption.txt)
```
Type: Opaque
Data
====
KEYCLOAK_CLIENT_SECRET: 14 bytes
```
**어디를 봐야 하는가****키 이름과 바이트 수만 나온다.** 값이 없다.
**이 결과가 의미하는 것** — 이 화면이 「Secret 은 감춰진다」는 인상의 출처다.
`describe`**일부러** 값을 안 찍는다. 그런데 그건 `describe` 라는 명령의
동작이지, **저장이나 전송의 성질이 아니다.**
## 1-3. 값을 안 보고 확인하는 법 — 평소에는 이렇게 한다
**남의 비밀을 다룰 때 기본 자세다.** 키 이름과 길이만 본다.
**확인** — 키 이름만
```bash
sudo kubectl -n keycloak-lab get secret keycloak-lab-secrets -o jsonpath='{.data}' \
| tr ',' '\n' | grep -o '"[A-Z_]*"'
```
**형태**
```
"KC_BOOTSTRAP_ADMIN_PASSWORD"
"POSTGRES_PASSWORD"
```
**확인** — 길이만
```bash
sudo kubectl -n keycloak-lab get secret keycloak-lab-secrets \
-o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d | wc -c
```
**형태**
```
22
```
**어디를 봐야 하는가** — 숫자 하나. **값이 화면에 없다.**
**이 결과가 의미하는 것** — 「Secret 이 제대로 들어갔는가」를 확인하는 데는
길이면 충분한 경우가 대부분이다. 배포가 안 될 때 진짜로 궁금한 것은 대개
**「비었는가 아닌가」**이지 값 자체가 아니다.
> `wc -c` 는 개행까지 세므로 `base64 -d` 결과에 개행이 없으면 실제 길이와
> 같다. 값이 비었으면 `0` 이 나온다 — **`0` 은 「Secret 은 있는데 그 키가
> 비었다」는 뜻이고, 배포 실패의 흔한 원인이다.**
---
# 2. 주입 — 카나리아 Secret 하나를 만든다
**여기부터 상태가 바뀐다.** 바뀌는 것은 Secret 하나뿐이다.
**되돌리기**
```bash
sudo kubectl -n keycloak-lab delete secret d3-canary
```
## 2-1. 왜 카나리아를 쓰나
4절에서 **저장 파일 안을 grep 해야 한다.** 그러려면 **찾을 문자열을 알고
있어야** 하는데, 진짜 비밀번호를 grep 인자로 쓰면 그 값이 셸 히스토리와
프로세스 목록(`ps` 로 다른 사용자에게도 보인다)에 남는다.
**그래서 「찾아도 아무 피해가 없는 값」을 하나 심는다.** 실험 대상이 값 자체가
아니라 **경로**이기 때문에 이렇게 해도 결론은 같다.
**하기**
```bash
sudo kubectl -n keycloak-lab create secret generic d3-canary \
--from-literal=CANARY=d3-canary-zq7v-do-not-use
```
**형태**
```
secret/d3-canary created
```
**어디를 봐야 하는가**`created`. 이미 있다면 `AlreadyExists` 가 나온다 —
그럼 지우고 다시 만든다.
> **이 값은 아무 데도 쓰이지 않는다.** 어떤 파드도 참조하지 않으므로 지워도
> 아무것도 안 깨진다. 값에 `do-not-use` 를 넣어 둔 이유는, 나중에 저장 파일
> 어딘가에서 이 문자열을 다시 만났을 때 **무엇인지 알아보기 위해서**다.
---
# 3. 주입이 실제로 걸렸는지 확인한다
**확인**
```bash
sudo kubectl -n keycloak-lab get secret d3-canary
sudo kubectl -n keycloak-lab describe secret d3-canary
```
**형태**
```
Data
====
CANARY: 26 bytes
```
**어디를 봐야 하는가** — 여기서도 `describe` 는 바이트 수만 준다. **1-2 와
같은 화면이다.** 값을 아는 것은 당신뿐이고, 그래서 다음 절의 비교가 성립한다.
---
# 4. 효과를 관찰한다 — 네 경로를 하나씩 연다
## 4-1. ① API — 한 줄로 읽힌다
**하기** — 카나리아로 먼저 해 본다. 값을 알고 있으므로 무엇이 나올지 예측된다
```bash
sudo kubectl -n keycloak-lab get secret d3-canary \
-o jsonpath='{.data.CANARY}' | base64 -d; echo
```
**형태**
```
d3-canary-zq7v-do-not-use
```
**어디를 봐야 하는가****2-1 에서 심은 값이 그대로 나온다.**
같은 명령이 실제 비밀에도 그대로 듣는다. 원래 실행이 네 개를 뽑은 결과가 이것이다.
**실측** — [`01-base64-not-encryption.txt`](../../evidence/d3-secret-management/01-base64-not-encryption.txt)
```
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``14 bytes` 라고 했던 그 값이 `bff-lab-secret` (14자)이다.
**이 결과가 의미하는 것** — ① 은 **막지 않는다.** base64 는 인코딩이고
`base64 -d` 는 누구나 칠 수 있다. 여기서 실질적인 방어선은 **누가 이 명령을
칠 수 있는가**이며, 그건 ④(RBAC)의 문제로 넘어간다.
> **이 네 줄을 당신 환경에서 그대로 재현할 필요는 없다.** 카나리아로 한 번
> 확인했으면 기제는 같다. 진짜 비밀은 1-3 의 길이 확인으로 충분하다.
## 4-2. ② 저장소 — 노드 디스크에 평문이 있다
### 먼저 암호화 설정을 본다
**확인**
```bash
sudo k3s secrets-encrypt status
```
**실측** — [`02-at-rest.txt`](../../evidence/d3-secret-management/02-at-rest.txt)
```
Encryption Status: Disabled, no configuration file found
```
**어디를 봐야 하는가**`Disabled`, 그리고 **`no configuration file found`**.
설정 파일이 아예 없다 — 껐다기보다 **켠 적이 없다**는 뜻이고, 이게 기본값이다.
### 저장 파일이 어디 있는지 본다
**확인**
```bash
sudo ls -l /var/lib/rancher/k3s/server/db/
```
**실측** — [`02-at-rest.txt`](../../evidence/d3-secret-management/02-at-rest.txt)
```
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` | 본체 |
| `state.db-wal` | **아직 본체에 합쳐지지 않은 최근 쓰기** |
| `state.db-shm` | 공유 메모리 인덱스 |
**k3s 는 etcd 대신 SQLite 를 쓴다.** 「저장소(at rest)」의 자리는 같다 —
etcd 를 쓰는 클러스터라면 이 자리가 etcd 의 데이터 디렉터리다.
> **`-wal` 이 10MB 나 되는 것을 봐 둔다.** 방금 만든 카나리아는 **아직 본체에
> 없을 가능성이 높다.** 아래에서 이게 함정이 된다.
### ★ 파일 안을 찾아본다
**하기** — 카나리아부터
```bash
sudo grep -c 'd3-canary-zq7v-do-not-use' /var/lib/rancher/k3s/server/db/state.db
sudo grep -c 'd3-canary-zq7v-do-not-use' /var/lib/rancher/k3s/server/db/state.db-wal
```
**미검증** — 이 실험대는 카나리아 대신 실제 값으로 쟀다. 그 결과가 아래다.
**실측** — [`02-at-rest.txt`](../../evidence/d3-secret-management/02-at-rest.txt)
```
=== ★ 저장 파일에서 비밀번호가 그대로 보이는가 ===
state.db 안의 평문 일치: 2
=== 평문이 저장 파일에 있다는 것을 눈으로 ===
client secret 평문 등장 횟수: 0
```
**어디를 봐야 하는가****두 줄의 값이 다르다. `2` 와 `0`.**
같은 파일, 같은 명령, 다른 키인데 하나는 두 번 나오고 하나는 안 나온다.
**이 결과가 의미하는 것 — 이 절에서 제일 중요한 문장이다.**
> **`grep` 이 `0` 을 돌려준 것은 「평문이 없다」가 아니라 「이 파일의 이 시점에
> 이 형태로는 못 찾았다」이다.**
`2` 가 나온 순간 ②의 답은 이미 정해졌다 — **저장 파일에 평문이 있다.**
`0` 이 나온 키에 대해 「그건 안전한가 보다」라고 읽으면, **같은 파일에 평문이
들어 있는 것을 이미 본 뒤에 그러는 것이다.**
**0 이 나왔을 때 다음에 볼 곳****미검증**. 이 실험은 원인을 가리지 않았다.
```bash
sudo grep -c 'bff-lab-secret' /var/lib/rancher/k3s/server/db/state.db-wal
sudo strings /var/lib/rancher/k3s/server/db/state.db | grep -c 'bff-lab-secret'
```
| 왜 안 나올 수 있나 | 확인 |
|---|---|
| 아직 `-wal` 에만 있다 | `-wal` 을 같이 grep |
| 값이 페이지 경계를 넘어 잘렸다 | `strings` 로 한 번 더 |
| 그 키가 그 시점에 없었다 | `get secret` 으로 존재 확인 |
> **`grep -c` 는 바이너리 파일에도 듣는다.** 평소의 `grep` 은 바이너리를 만나면
> `Binary file ... matches` 한 줄만 찍고 내용을 안 보여주는데, `-c` 는 개수만
> 세므로 그대로 숫자가 나온다. **값 자체를 화면에 안 띄운다는 점에서도 이
> 형태가 맞다** — 여기서 궁금한 것은 「있는가」이지 「무엇인가」가 아니다.
**그래서 무엇이 위험한가**
| | |
|---|---|
| 노드 디스크를 얻으면 | **전 클러스터의 비밀** |
| 노드 백업/스냅샷 | 같은 것을 복사한다 |
| A-4 에서 본 `local-path` PVC | **같은 디스크에 있다** |
| D-1 의 덤프 | 같은 기계에 뒀다면 **거기도 같이** |
**D-1 에서 「덤프를 같은 장애 도메인에 두면 백업이 아니다」라고 했는데, 여기서는
「노드 디스크 하나가 모든 비밀」이다.** 백업을 잘 챙길수록 비밀도 잘 복사된다.
**k3s 는 `--secrets-encryption` 플래그로 켤 수 있다.** 지금은 안 켜져 있고,
**이 가이드는 켜지 않는다** — 켜는 것은 서버 재시작과 기존 Secret 재암호화를
수반하고, 이 실험대에서 시험하지 않았다(**미검증**).
## 4-3. ③ 파드 안 — 평범한 환경변수다
**확인** — 어느 파드를 볼지 먼저 정한다
```bash
sudo kubectl -n keycloak-lab get pods -l app=bff
```
**하기****미검증**(원래 실행은 파드 이름을 직접 지정했다)
```bash
sudo kubectl -n keycloak-lab exec deploy/bff -- sh -c 'env | grep -iE "secret|password"'
```
**실측** — [`02-at-rest.txt`](../../evidence/d3-secret-management/02-at-rest.txt)
```
KEYCLOAK_CLIENT_SECRET=bff-lab-secret
BFF_DB_PASSWORD=lab-postgres-change-me
```
**어디를 봐야 하는가****`env` 한 번이면 나온다.** 그리고 `bff-lab-secret`
은 4-1 에서 API 로 뽑은 값과 **같다** — 두 경로가 같은 평문에 닿는다.
`exec``deploy/bff` 로 안 되면(파드가 종료 중이거나 여럿이면) 이름을 골라
친다.
```bash
sudo kubectl -n keycloak-lab get pod -l app=bff \
--field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}'; echo
```
**같은 파드 안의 다른 프로세스도 본다.** 이게 「환경변수」의 진짜 성질이다.
**하기****미검증**
```bash
sudo kubectl -n keycloak-lab exec deploy/bff -- \
sh -c 'tr "\0" "\n" < /proc/1/environ | grep -i secret'
```
**어디를 봐야 하는가** — 같은 값이 나오는가. `/proc/<pid>/environ` 은 그
프로세스의 환경변수를 그대로 담고 있고, **같은 UID 의 아무 프로세스나 읽는다.**
| 새는 경로 | |
|---|---|
| `kubectl exec` 권한이 있는 사람 | 바로 본다 |
| 같은 파드의 다른 프로세스 | `/proc/<pid>/environ` |
| **크래시 덤프 · 오류 리포트** | 환경변수를 함께 담는 도구가 많다 |
| 자식 프로세스 | 상속된다 |
**볼륨으로 마운트하면 이 중 몇 가지가 줄어든다** — 파일 권한으로 제한할 수
있고, 환경변수 덤프에 안 들어간다.
```yaml
volumeMounts:
- name: secrets
mountPath: /etc/secrets
readOnly: true
```
**줄어드는 것이지 없어지는 것이 아니다.** `exec` 권한이 있으면 파일도 읽는다.
## 4-4. ④ RBAC — 유일하게 막는다
**확인**
```bash
sudo kubectl auth can-i get secrets -n keycloak-lab \
--as=system:serviceaccount:keycloak-lab:default
```
**실측** — [`02-at-rest.txt`](../../evidence/d3-secret-management/02-at-rest.txt)
```
default SA: no
```
**어디를 봐야 하는가****`no`** 한 단어.
**어떤 권한이 있는지 통째로 보려면****미검증**
```bash
sudo kubectl auth can-i --list -n keycloak-lab \
--as=system:serviceaccount:keycloak-lab:default
sudo kubectl -n keycloak-lab get role,rolebinding
```
**이 결과가 의미하는 것** — 기본 서비스계정은 Secret 을 못 읽는다.
**명시적으로 거부해서가 아니라 아무 권한도 주지 않았기 때문**이다. RBAC 은
기본이 거부이고, Role 을 붙여야 할 수 있게 된다.
> **네 가지 중 유일하게 제 역할을 하는 것이 RBAC 다.** 그러므로 실질적인
> 방어선은 「누가 `get secrets` 를 할 수 있는가」이며,
> **관리자 권한을 가진 사람에게는 아무 방어가 없다.**
>
> A-0 의 관측 스택에서 `nodes/proxy` 서브리소스를 따로 줘야 했던 것처럼,
> Secret 접근도 **리소스 단위로 나눌 수 있다.**
## 4-5. 네 경로 정리
| # | 경로 | 감춰지는가 | 무엇이 뚫나 |
|---|---|---|---|
| ① | `get -o jsonpath \| base64 -d` | **아니다** | 클러스터 접근 권한 |
| — | `describe secret` | 값을 숨긴다 | **그래서 안전하다고 착각한다** |
| ② | 저장 파일(`state.db`) | **아니다.** 암호화 꺼짐 | 노드 디스크·백업·스냅샷 |
| ③ | 파드 안 | **아니다.** 평범한 환경변수 | `exec` · `/proc` · 크래시 덤프 |
| ④ | RBAC | **막는다** | 관리자 권한 |
**「Secret 이니까 안전하다」는 네 가지 중 하나(RBAC)만 맞다.**
그리고 ②·③ 은 **쿠버네티스 API 를 한 번도 거치지 않고** 평문에 닿는다.
---
# 5. 복구
## 5-1. 카나리아를 지운다
**하기**
```bash
sudo kubectl -n keycloak-lab delete secret d3-canary
sudo kubectl -n keycloak-lab get secret
```
**형태**
```
secret "d3-canary" deleted
```
**어디를 봐야 하는가** — 1-1 의 목록으로 돌아왔는가. 세 개다.
## 5-2. ★ 지웠다고 파일에서 없어지지는 않는다
**확인****미검증**. 이 실험은 삭제 후를 재지 않았다
```bash
sudo grep -c 'd3-canary-zq7v-do-not-use' /var/lib/rancher/k3s/server/db/state.db
sudo grep -c 'd3-canary-zq7v-do-not-use' /var/lib/rancher/k3s/server/db/state.db-wal
```
**어디를 봐야 하는가** — 0 이 나오면 「이 시점에 이 형태로는 안 보인다」이고,
0 이 아니면 **지운 Secret 의 평문이 아직 파일에 남아 있는 것**이다.
어느 쪽이든 **4-2 의 결론은 안 바뀐다** — 판정은 이미 `2` 에서 났다.
**이 결과가 의미하는 것** — 데이터베이스 파일은 지운 행의 자리를 즉시
0으로 덮어쓰지 않는다. **「Secret 을 지웠다」와 「그 값이 디스크에서 사라졌다」는
다른 사건**이며, 비밀이 유출됐을 때 실제로 해야 하는 일은 삭제가 아니라
**회전(rotation)**인 이유가 여기 있다 — B-6·B-7 의 주제다.
## 5-3. 원상복구 확인표
| 항목 | 명령 | 돌아왔을 때 |
|---|---|---|
| 카나리아 | `sudo kubectl -n keycloak-lab get secret d3-canary` | `NotFound` |
| Secret 목록 | `sudo kubectl -n keycloak-lab get secret` | 세 개 |
| 파드 | `sudo kubectl -n keycloak-lab get pods` | 전부 `Running` (아무것도 안 건드렸다) |
| **터미널** | `history \| tail -40` | **비밀번호가 찍힌 줄이 어디까지 남았는지 본다** |
> **이 실험의 진짜 뒷정리는 스크롤백이다.** 4-1 을 실제 비밀로 쳤다면 그 값이
> 터미널 버퍼와 셸 히스토리에 남아 있다. 실험대 값이라 지금은 상관없지만,
> **같은 절차를 운영에서 하면 그게 유출 경로가 된다.**
---
# 6. 그래서 무엇을 해야 하는가
```
지금: 매니페스트에 stringData 평문 → git 에 커밋되면 끝
k3s 저장소 암호화 꺼짐
파드 환경변수
```
| 단계 | 얻는 것 | 이 실험대 |
|---|---|---|
| ① 매니페스트에서 값을 빼고 **`.example` 만 커밋** | git 유출을 막는다 | 안 함 |
| ② **k3s `--secrets-encryption`** 활성화 | 노드 디스크 유출을 막는다 | 안 함 (**미검증**) |
| ③ 환경변수 대신 **볼륨 마운트** | 프로세스·덤프 유출을 줄인다 | 안 함 |
| ④ **SealedSecret / 외부 KMS** | 매니페스트에 암호문만 남는다 | 안 함 |
| ⑤ **RBAC 최소화** | 유일하게 이미 동작하는 방어선을 좁힌다 | 기본값 그대로 |
**이 실험대는 ①~④ 중 아무것도 안 하고 있다.** 실험 목적으로는 의도적이지만,
**그 사실을 기록해두지 않으면 그대로 운영에 옮겨간다.** 값 이름에 `change-me`
를 넣어 둔 것이 그 최소한의 표시다.
---
# 막히면
| 증상 | 원인 | 확인 |
|---|---|---|
| `grep``0` 인데 안전하다고 읽힌다 | **`0` 은 「이 파일의 이 시점에 이 형태로는 못 찾았다」** | `-wal``strings` 로 한 번 더 — 4-2 |
| `grep``Binary file matches` 만 찍는다 | 바이너리 파일이다 | `-c` 를 쓴다(개수만). 값을 안 띄우는 이점도 있다 |
| `k3s secrets-encrypt` 가 없다 | **서버 노드가 아니다** | `kc-lab-1`(control-plane)에서 친다 |
| `state.db``Permission denied` | root 전용 디렉터리 | 게스트 sudo 는 무암호다. `sudo` 를 붙인다 |
| `exec deploy/bff` 가 실패한다 | 파드가 종료 중이거나 여럿이다 | `--field-selector=status.phase=Running` 으로 이름을 고른다 — 4-3 |
| `auth can-i``yes` 라고 한다 | 그 SA 에 Role 이 붙어 있다 | `get rolebinding -o wide` 로 누가 줬는지 본다 |
| 값이 `0 bytes` 로 나온다 | Secret 은 있는데 키가 비었다 | `describe` 의 바이트 수를 본다 — 배포 실패의 흔한 원인 |
| 비밀번호를 화면에 찍어 버렸다 | 4-1 을 실제 값으로 쳤다 | 스크롤백·히스토리를 지우고, **운영이면 회전한다** |
---
# 다음
| 실험 | D-3 이 남긴 것 |
|---|---|
| [B-6](../../experiment-b6-key-rotation.md) key 회전 | **key 를 Secret 에 두면 이 실험의 결론이 그대로 적용된다** |
| [B-7](../../experiment-b7-cookie-secret-rotation.md) 쿠키 비밀 회전 | 유출 대응은 삭제가 아니라 **회전**이다 — 5-2 |
| [D-1](d1-backup-restore.md) 백업 | **덤프에도 같은 문제가 있다.** 백업을 잘 챙길수록 비밀도 잘 복사된다 |
| [D-4](d4-certificate-renewal.md) 인증서 | **개인키(`privkey.pem`)도 같은 비밀 관리 문제다** |
| 운영 | **RBAC 이 유일하게 동작하는 방어선이다.** 관리자에게는 아무 방어가 없다 |