기반 가이드 7단계로 실험대를 철거하고 다시 세운 뒤 virtualization setup 9편과 keycloak-session-store 26편을 순서대로 밟았다. 24편은 끝까지, 11편은 되는 데까지 밟았고 밟은 범위를 편마다 적었다. 명령이 못 도는 것을 고쳤다. - kubectl 을 `kc-lab-1` 에서 치라고 적었는데 그 기계에 kubeconfig 가 없다. 라벨 639개와 각 편의 「어디서 치는가」를 `[lab host]` 로 옮겼다 - `-o custom-columns=…[0]…` 이 zsh 에서 글로브로 읽혀 안 돈다. 28곳에 따옴표 - busybox `sed` 가 끝 개행을 안 붙여 A-3 의 측정이 언제나 0 이었다 - `--token-file ~/node-token` 뒤에 그 파일을 지우면 k3s agent 가 재부팅을 못 견딘다. `/etc/rancher/node-token` 으로 옮기는 처방을 재서 넣었다 - 게스트에 없는 도구를 전제로 한 명령 넷 — `conntrack`·`dig`·`strings`·`nginx -v` - `echo` 와 JWT 헤더가 `"이름" : [ 값 ]` 으로 찍는데 문서는 공백 없이 옮겨 적어 그 실측으로 만든 grep·sed 가 한 줄도 못 잡는다 - B-0 이 `directAccessGrantsEnabled` 와 계정 완성을 빠뜨려 B-3 이 못 돈다 - D-4·D-4a 가 `test-server` 와 `certbot-renew.*` 를 가리키는데 실제로는 `kc-lab-edge` 의 `certbot.service` 다 - `virsh setmaxmem --config` 를 `dominfo` 로 판정하면 틀린다. `--inactive` 로 - `LIBVIRT_DEFAULT_URI` 를 rc 에만 넣으면 `ssh host '명령'` 에서 안 먹는다 결과가 조건부인 것을 갈랐다. - readiness 는 즉시 안 뒤집힌다. A-1·A-2 의 60초 창을 적었다 - 03 의 층 ②③ `301` 은 04 이후의 값이고 그 단계에서는 `404` 다 - A-0 의 로그 필터를 요청 직후에 치면 정반대 결론이 나온다 - A-5 의 한 방향 차단은 잠깐 `1` 이었다 `2` 로 돌아온다 증거는 두 프로젝트의 `evidence/raw/` 에 99벌을 README 와 함께 남겼다. 비밀은 길이만 적었고 화면에 찍힌 토큰은 가렸다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
698 lines
41 KiB
Markdown
698 lines
41 KiB
Markdown
---
|
||
id: d53181b9-bb28-4703-97ed-98adfb5b18dc
|
||
kind: SETUP
|
||
slug: reproduce-b7-cookie-secret-rotation
|
||
title: cookie secret 을 갈아치우고 로그인해 있던 세션이 어떻게 되는지 본다
|
||
topic: trust-handed-over-at-the-edge
|
||
topicName: 위조 신원 헤더와 로그아웃 전파
|
||
project: keycloak-session-store
|
||
status: 게시 전
|
||
studio: "https://hyeonworks.com/studio/documents/d53181b9-bb28-4703-97ed-98adfb5b18dc/edit"
|
||
pinnedVersions:
|
||
- name: Redis
|
||
version: 7.4.x
|
||
- name: curl
|
||
version: 8.5.0
|
||
source:
|
||
- final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-7
|
||
sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
|
||
---
|
||
|
||
# cookie secret 을 갈아치우고 로그인해 있던 세션이 어떻게 되는지 본다
|
||
|
||
oauth2-proxy 의 cookie secret 을 A 에서 B 로 바꿨을 때 로그인해 있던 세션이 어떻게 되는지 보는 절차다. Grafana 의 Ingress 를 빌려 띄우고 결과를 로그로 가른다. 남의 도메인을 빌리므로 끝나면 반드시 돌려준다. 약 20분.
|
||
|
||
## 관계
|
||
|
||
- **쿠키에 세션을 담으면 지울 대상을 잃는다 — TTL 로 되찾은 고아 세션**
|
||
이 절차가 만드는 고아 세션을 그 기록이 결론으로 적는다. 여기서는 그것이 생기는 데까지만 친다.
|
||
- **nginx 는 자기가 설정하지 않은 헤더를 덮어쓰지 않았다**
|
||
로그인 뒤 업스트림이 받는 `x-forwarded-*` 네 줄을 그 기록은 밖에서 위조해 보내 통과시켰다.
|
||
- **TTL 로 고아 세션을 골라내 지운다**
|
||
같은 배포를 이어 쓰지만 치는 것이 다르다. 여기는 회전을 A → B 로 한 번만 치고 고아가 생기는 데까지 보며, 그쪽은 1차와 2차를 자기 손으로 치고 기동 로그의 `refresh:disabled` 를 맨 앞에서 확인한 뒤 TTL 을 역산해 고아를 골라 지운다. 그 확인이 깨진 환경에서는 그쪽 절차를 쓰면 안 된다. 되돌리기도 다르다 — 여기는 secret 참조와 Grafana Ingress 를 되돌리고, 그쪽은 지운 세션이 돌아오지 않는다.
|
||
- **서명 키를 더한 뒤 옛 키를 지우고 옛 토큰이 언제 끊기는지 본다**
|
||
같은 회전을 식별자가 있는 쪽에서 치는 편이다. 거기서는 토큰 헤더의 `kid` 가 겹치는 구간을 만들어 줬고, 여기서는 그 식별자가 없어 겹칠 수단 자체가 없다.
|
||
- **아무 저장소도 주지 않고 Spring 이 무엇을 고르는지 찍어서 확인한다**
|
||
먼저 해 둬야 하는 편이다. 이 절차가 세션을 넣는 Redis 를 거기서 띄운다.
|
||
|
||
## 본문
|
||
|
||
<!-- body:start -->
|
||
|
||
## 읽기 전에 — 어디서 치는가
|
||
|
||
기계가 둘이고 표시가 둘이다. `kubectl` 과 `redis-cli` 는 `[lab host]` 에서 치고, 앞단 nginx 를 건너뛰고 Traefik 을 직접 두드리는 `curl` 과 `sudo` 는 `[test-server]` 에서 친다. 앞단 nginx 가 그 호스트에 있고 `192.168.122.11:80` 으로 넘겨주므로, 호스트에서 그 주소를 바로 치면 nginx 를 건너뛴다.
|
||
|
||
**원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||
|
||
브라우저도 필요하다. 쿠키가 `HttpOnly` 이고 OIDC(OpenID Connect, OAuth2 위에 신원 확인을 얹은 규격) 흐름을 폼까지 걸어야 세션이 생긴다. `curl` 로 완주하려던 시도는 실패했다.
|
||
|
||
| 무엇 | 값 |
|
||
|---|---|
|
||
| 네임스페이스 | `keycloak-lab` · Grafana 는 `observability` |
|
||
| 빌리는 이름 | `app2.hyeonworks.com` — 평소 Grafana 로 간다 |
|
||
| 주입 수단 | `patch deployment` 로 `secretKeyRef.key` 를 `COOKIE_SECRET_A` 에서 `COOKIE_SECRET_B` 로 바꾼다 |
|
||
| 세션 저장소 | Redis. `redis.keycloak-lab.svc:6379` |
|
||
| replica | oauth2-proxy 파드 둘. 서로 다른 노드 |
|
||
| 시각 표시 | oauth2-proxy 로그는 UTC, `kubectl` 출력은 KST. 회전 시각을 UTC 로 적는다 |
|
||
| 전 구간 | 약 20분 |
|
||
| 도구 | `jq` 도 `yamllint` 도 이 실험대에 없다 |
|
||
|
||
## 이 실험이 가르는 것
|
||
|
||
앞선 작업이 남긴 열린 질문 Q1 의 미지수 7 이 이렇게 물었다.
|
||
|
||
> *"OAuth2-Proxy 구조의 replica 들이 같은 cookie secret 을 어떻게 공유하고 교체하게 되는가.
|
||
> 교체하는 동안 로그인해 있던 사람은 어떻게 되는가."*
|
||
|
||
B-6 에서 Keycloak 은 두 키를 동시에 들고 무중단으로 회전했다. 토큰 헤더에 `kid` 가 있어서 읽기는 여러 키, 쓰기는 하나가 됐기 때문이다.
|
||
|
||
| 무엇을 기대했나 | 무엇이 나왔나 |
|
||
|---|---|
|
||
| B-6 의 모양대로라면 oauth2-proxy 도 겹치는 구간을 만들 수 있을 것 | `--cookie-secret` 은 단수이고 쿠키에 키 식별자가 없다 |
|
||
|
||
예측하지 않았던 것이 하나 더 나온다. 사용자는 아무것도 못 느끼는데 서버 쪽에 지워지지 않는 세션이 생긴다. 그 「지우지 못한다」를 이어서 재는 것이 B-7a 이고, 이 절차는 거기까지 가지 않는다.
|
||
|
||
두 구조는 인가 요청을 어디에 두는지가 다르다.
|
||
|
||
```text
|
||
BFF 인가 요청을 서버 메모리(HttpSession)에 둔다 → replica 를 넘으면 실패
|
||
oauth2-proxy 인가 요청을 쿠키에 두고 secret 으로 봉인한다 → replica 를 넘어도 성공
|
||
대신 secret 이 단일 지점
|
||
```
|
||
|
||
절차를 끝까지 밟으면 `--cookie-secret` 이 단수라는 도움말 한 줄, 회전만으로는 Redis 가 그대로인 것, 로그인 화면이 안 뜨는데 로그에는 재인증이 찍혀 있는 것, 세션 키가 하나에서 둘로 늘어난 것, 흐름을 시작한 파드와 콜백을 받은 파드가 다른데도 성공한 것을 자기 화면에서 보게 된다.
|
||
|
||
## 전제와 되돌리기
|
||
|
||
- `05-keycloak` 이 끝나 있고 realm `keycloak-patterns` 에 클라이언트 `oauth2-proxy` 와 사용자 `labuser`(비밀번호 `labpass`)가 있다.
|
||
- B-0 이 끝나 있어야 한다. Redis 를 거기서 띄우고 `redis.keycloak-lab.svc:6379` 로 떠 있다.
|
||
- 브라우저가 있어야 한다.
|
||
|
||
**`oauth2-proxy` 는 인증서가 서기 전에는 못 뜬다.** 기동할 때 OIDC 디스커버리를 먼저 하는데 그 주소가 `https://auth.hyeonworks.com/...` 이라, 인증서 단계를 안 끝낸 실험대에서는 `CrashLoopBackOff` 로 끝난다. 2026-09-17 에 그대로 겪었다(observed).
|
||
|
||
```text
|
||
[provider.go:55] Performing OIDC Discovery...
|
||
[main.go:59] ERROR: Failed to initialise OAuth2 Proxy: ... error while discovery OIDC configuration:
|
||
Get "https://auth.hyeonworks.com/realms/keycloak-patterns/.well-known/openid-configuration":
|
||
dial tcp 100.83.212.4:443: connect: connection refused
|
||
```
|
||
|
||
`oauth2-proxy-secrets` 가 있고 이미지도 받아지는데 기동 자체가 안 된다 — **이 편의 전제는 배포가 아니라 TLS 다.**
|
||
|
||
**이건 남의 도메인을 빌리고 남의 세션을 끊는 실험이다.** 둘을 건드린다. 인증서가 `auth` · `app1` · `app2` 세 이름만 덮어서 네 번째 이름을 못 만들기 때문에 Grafana 의 Ingress 를 잠시 내리고 `app2` 를 빌린다. 그리고 secret 을 바꾸면 그때 로그인해 있던 사람의 쿠키가 전부 무효가 된다.
|
||
|
||
되돌리기는 둘이고 먼저 읽어 둔다.
|
||
|
||
```bash label="[lab host] ① secret 참조를 A 로 되돌린다"
|
||
kubectl -n keycloak-lab patch deployment oauth2-proxy --type=json \
|
||
-p '[{"op":"replace",
|
||
"path":"/spec/template/spec/containers/0/env/1/valueFrom/secretKeyRef/key",
|
||
"value":"COOKIE_SECRET_A"}]'
|
||
```
|
||
|
||
①의 `env/1` 은 매니페스트의 env 배열 순서에 달린 숫자다. 중간에 멈춰서 되돌리는 사람은 주입 2 절 ①의 `env[*].name` 을 먼저 쳐서 `OAUTH2_PROXY_COOKIE_SECRET` 이 0 부터 세어 몇 번째인지 보고, 두 번째가 아니면 위 경로의 `1` 을 그 숫자로 바꾼다. 숫자가 틀리면 클라이언트 비밀을 쿠키 secret 으로 덮어쓴다.
|
||
|
||
```bash label="[lab host] ② 빌린 Ingress 를 걷고 Grafana 것을 올린다"
|
||
kubectl -n keycloak-lab delete ingress oauth2-proxy
|
||
kubectl apply -f ~/grafana-ingress-backup.yaml
|
||
```
|
||
|
||
②가 올리는 백업 파일은 아래 주입 전 1 절이 만든다. B-4 로 app2 를 먼저 빌린 적이 있으면 그 편은 같은 백업을 `/tmp/grafana-ingress-backup.yaml` 에 떠 두므로, `~` 쪽이 없을 때 그쪽을 본다.
|
||
|
||
## 주입 전에 같은 명령으로 먼저 본다
|
||
|
||
```text
|
||
Ingress 백업 → 배포 → replica 배치 → secret 키 이름 → 로그인 → Redis → 쿠키 모양
|
||
```
|
||
|
||
### 1. 지금 app2 가 무엇인지 보고 Grafana Ingress 를 백업한다
|
||
|
||
**목적** — 실험이 끝났을 때 되돌릴 파일을 만든다.
|
||
|
||
**행동** — 먼저 지금 상태를 보고, 백업을 뜨고, 그 백업이 비어 있지 않은지 확인한다.
|
||
|
||
```bash label="[test-server] ① 지금 app2 가 어디로 가는지 본다"
|
||
curl -sI https://app2.hyeonworks.com/ | head -3
|
||
```
|
||
|
||
**②를 치기 전에 읽는다.** 셸은 `>` 를 kubectl 보다 먼저 처리한다. `~/grafana-ingress-backup.yaml` 은 kubectl 이 돌기도 전에 0바이트가 되고, `get` 이 실패하면 앞서 떠 둔 백업이 그때 없어진다. 뒤따르는 `wc -l` 과 `grep -c` 는 이미 비어 버린 파일을 센다. 그래서 이 절을 두 번째로 치는 사람은 — B-4 로 app2 를 먼저 빌렸거나 실험을 중간에 다시 시작했다면 — `wc -l ~/grafana-ingress-backup.yaml` 을 먼저 쳐서 쓸 만한 백업을 이미 갖고 있는지 보고, 갖고 있으면 ②를 건너뛴다.
|
||
|
||
```bash label="[lab host] ② 백업을 뜨고 내용이 있는지 센다"
|
||
kubectl -n observability get ingress grafana -o yaml > ~/grafana-ingress-backup.yaml
|
||
wc -l ~/grafana-ingress-backup.yaml
|
||
grep -c 'app2.hyeonworks.com' ~/grafana-ingress-backup.yaml
|
||
```
|
||
|
||
**예상 결과** — ① 은 Grafana 로 가고 있으면 `302` 로 `/login` 을 가리킨다. ② 는 줄 수가 0 이 아니고 `app2.hyeonworks.com` 이 1회 이상 잡힌다.
|
||
|
||
**왜 필요한가** — 백업 파일이 빈 채로 원본을 지우면 복구할 것이 없다. `wc -l` 과 `grep -c` 가 그 사고를 여기서 막는다.
|
||
|
||
**문제가 생기면** — 둘 중 하나라도 `0` 이면 그대로 진행하지 않는다. 네임스페이스와 Ingress 이름을 다시 본다. Grafana Ingress 자체가 없다고 나오면 B-4 가 먼저 app2 를 빌려 갔다. 그 편은 같은 백업을 `/tmp/grafana-ingress-backup.yaml` 에 떠 두므로 그쪽을 본다. 양쪽 다 비어 있으면 여기서 멈춘다 — 지금은 떠 둘 원본이 없고, 지운 Ingress 를 되살리는 절차는 가이드에 없다(unknown).
|
||
|
||
### 2. Grafana Ingress 를 내리고 oauth2-proxy 를 배포한다
|
||
|
||
**목적** — `app2.hyeonworks.com` 을 oauth2-proxy 쪽으로 돌린다.
|
||
|
||
**행동** — 내리고, 올리고, 롤아웃이 끝날 때까지 기다린다. ②의 `deploy/lab/k8s/b7-oauth2-proxy.yaml` 은 저장소 체크아웃의 루트에서 푸는 상대 경로다. 체크아웃을 어디에 뒀는지는 가이드에 없으므로(unknown), 그 경로가 풀리는 디렉터리로 옮긴 다음 ②를 친다.
|
||
|
||
```bash label="[lab host] ① Grafana Ingress 를 내린다"
|
||
kubectl -n observability delete ingress grafana
|
||
```
|
||
|
||
```bash label="[lab host] ② oauth2-proxy 를 배포한다"
|
||
kubectl apply -f deploy/lab/k8s/b7-oauth2-proxy.yaml
|
||
kubectl -n keycloak-lab rollout status deploy/oauth2-proxy --timeout=180s
|
||
```
|
||
|
||
**예상 결과** — 실측은 이렇다(observed, `01-deploy.txt`).
|
||
|
||
```text
|
||
=== Grafana ingress 를 잠시 내린다 (app2 를 빌린다) ===
|
||
grafana ingress 삭제
|
||
```
|
||
|
||
```text
|
||
secret/oauth2-proxy-secrets created
|
||
deployment.apps/oauth2-proxy created
|
||
service/oauth2-proxy created
|
||
ingress.networking.k8s.io/oauth2-proxy created
|
||
deployment "oauth2-proxy" successfully rolled out
|
||
```
|
||
|
||
**왜 필요한가** — 같은 이름을 두 Ingress 가 주장하면 어느 쪽으로 갈지가 컨트롤러 판단에 맡겨진다. 내리는 것이 먼저다.
|
||
|
||
**문제가 생기면** — 매니페스트를 못 찾는다고 끝나면 체크아웃 루트가 아닌 디렉터리에서 쳤다. 롤아웃이 타임아웃이면 `kubectl -n keycloak-lab get pods -l app=oauth2-proxy` 로 파드 상태부터 본다.
|
||
|
||
### 3. replica 가 둘인지, 어느 노드에 있는지 본다
|
||
|
||
**무엇을 보는가** — 파드 수와 배치.
|
||
|
||
```bash label="[lab host] 파드 배치를 본다"
|
||
kubectl -n keycloak-lab get pods -l app=oauth2-proxy -o wide
|
||
```
|
||
|
||
**어디를 보나** — 실측은 이렇다(observed, `01-deploy.txt`).
|
||
|
||
```text
|
||
oauth2-proxy-c76b49c59-8p5hl true kc-lab-1
|
||
oauth2-proxy-c76b49c59-b9928 true kc-lab-2
|
||
```
|
||
|
||
**이 값이 뜻하는 것** — 파드 두 개가 서로 다른 노드에 있다. 파드 이름의 끝 다섯 글자를 적어 둔다. 관찰 절에서 어느 replica 가 무엇을 했는지 그 글자로 가른다. replica 가 하나면 「공유」라는 말이 성립하지 않는다.
|
||
|
||
### 4. 진입점 두 곳이 갈라지는지 본다
|
||
|
||
**무엇을 보는가** — 인증을 거치는 경로와 안 거치는 경로.
|
||
|
||
```bash label="[test-server] 두 경로의 상태 코드를 뽑는다"
|
||
curl -s -o /dev/null -w '/ %{http_code}\n' https://app2.hyeonworks.com/
|
||
curl -s -o /dev/null -w '/ping %{http_code}\n' https://app2.hyeonworks.com/ping
|
||
```
|
||
|
||
**어디를 보나** — 실측은 이렇다(observed, `01-deploy.txt`).
|
||
|
||
```text
|
||
=== 진입점 확인 ===
|
||
https://app2.hyeonworks.com/ HTTP 302
|
||
/ping HTTP 200
|
||
```
|
||
|
||
| 경로 | 정상 | 뜻 |
|
||
|---|---|---|
|
||
| `/` | `302` | 인증이 없으니 Keycloak 으로 보낸다 — 프록시가 일하고 있다 |
|
||
| `/ping` | `200` | 인증을 거치지 않는 헬스 경로 — 프록시 자체는 살아 있다 |
|
||
|
||
**이 값이 뜻하는 것** — `/ping` 도 안 되면 프록시가 안 떴고, `/ping` 만 되면 프록시는 떴는데 앞단이 무언가를 막고 있다.
|
||
|
||
### 5. 502 를 만나면 한 겹씩 벗겨 좁힌다
|
||
|
||
**무엇을 보는가** — 502 를 낸 것이 앞단 nginx 인지, 그 뒤 Traefik 인지, 파드인지. 원래 구성에서 콜백이 계속 502 였다.
|
||
|
||
```bash label="[test-server] 앞단 nginx 를 건너뛰고 Traefik 에 바로 친다"
|
||
curl -H "Host: app2.hyeonworks.com" http://192.168.122.11/ping
|
||
curl -H "Host: app2.hyeonworks.com" http://192.168.122.11/
|
||
```
|
||
|
||
**어디를 보나** — 실측은 이렇다(observed).
|
||
|
||
```text
|
||
curl -H "Host: app2.hyeonworks.com" http://192.168.122.11/ping → 200
|
||
curl -H "Host: app2.hyeonworks.com" http://192.168.122.11/ → 302
|
||
```
|
||
|
||
**이 값이 뜻하는 것** — Traefik 직접은 정상이므로 502 를 내는 것은 그 앞의 nginx 이고, 502 는 쿠키를 설정하는 응답에서만 났다. oauth2-proxy 는 기본적으로 세션 전체를 쿠키에 담는데 그 `Set-Cookie` 가 nginx 의 `proxy_buffer_size` 를 넘겼다. B-4 에서 본 헤더 크기 절벽이 이번에는 응답 쪽에서 나타났다 — 거기서는 요청 헤더가 8KB 에서 400 이 됐고, 여기서는 응답 헤더가 프록시 버퍼를 넘겨 502 가 됐다. 해결은 세션을 Redis 로 옮기는 것이고 매니페스트에 이미 들어 있다.
|
||
|
||
```bash label="[lab host] 세션 저장소가 Redis 인지 인자에서 본다"
|
||
kubectl -n keycloak-lab get deploy oauth2-proxy \
|
||
-o jsonpath='{.spec.template.spec.containers[0].args}' | tr ',' '\n' | grep -i session
|
||
```
|
||
|
||
모양은 이렇다(모양은 observed).
|
||
|
||
```text
|
||
"--session-store-type=redis"
|
||
"--redis-connection-url=redis://redis.keycloak-lab.svc:6379"
|
||
```
|
||
|
||
nginx 설정을 직접 보려던 시도는 계속 빈 결과였다. 호스트에서 무언가가 빈 결과를 주면 먼저 이것을 친다.
|
||
|
||
```bash label="[test-server] 빈 결과의 원인이 권한인지부터 본다"
|
||
sudo -n true
|
||
```
|
||
|
||
실측은 이렇다(observed).
|
||
|
||
```text
|
||
$ sudo -n true
|
||
sudo: a password is required
|
||
```
|
||
|
||
`test-server` 의 sudo 는 비밀번호를 요구한다. 게스트(`kc-lab-1` 과 `kc-lab-2`)는 무암호라 A층에서 `conntrack` 과 `tc` 를 문제없이 썼는데 호스트는 다르다. 앞선 「nginx 로그가 비어 있다」는 관측은 로그가 없던 것이 아니라 sudo 가 조용히 실패한 것이었다.
|
||
|
||
### 6. secret 의 키 이름과 길이를 본다
|
||
|
||
**무엇을 보는가** — 회전 대상이 준비되어 있는지. 값은 찍지 않는다.
|
||
|
||
```bash label="[lab host] ① Secret 의 키 이름만 뽑는다"
|
||
kubectl -n keycloak-lab get secret oauth2-proxy-secrets \
|
||
-o jsonpath='{.data}' | tr ',' '\n' | grep -o '"[A-Z_]*"'
|
||
```
|
||
|
||
모양은 이렇다(모양은 observed).
|
||
|
||
```text
|
||
"CLIENT_SECRET"
|
||
"COOKIE_SECRET_A"
|
||
"COOKIE_SECRET_B"
|
||
```
|
||
|
||
길이도 본다. 가이드가 아래 두 줄을 미검증으로 표시했다(unknown) — 원래 실행 기록에 이 명령의 출력이 없다.
|
||
|
||
```bash label="[lab host] ② 두 cookie secret 의 길이만 센다 (unknown)"
|
||
kubectl -n keycloak-lab get secret oauth2-proxy-secrets \
|
||
-o jsonpath='{.data.COOKIE_SECRET_A}' | base64 -d | wc -c
|
||
kubectl -n keycloak-lab get secret oauth2-proxy-secrets \
|
||
-o jsonpath='{.data.COOKIE_SECRET_B}' | base64 -d | wc -c
|
||
```
|
||
|
||
**어디를 보나** — oauth2-proxy 는 정확히 16 · 24 · 32 바이트만 받는다. 매니페스트의 값은 32바이트짜리이고, 다른 수가 나오면 프록시가 기동에서 죽는다.
|
||
|
||
**이 값이 뜻하는 것** — 회전 대상이 미리 두 개 준비되어 있고, 그래서 이 실험이 한 번 바꾸고 되돌릴 수 있는 형태가 된다. 16 · 24 · 32 라는 제약은 oauth2-proxy 의 것이지 이 실험대가 잰 값이 아니다.
|
||
|
||
### 7. 브라우저로 로그인하고 업스트림이 받는 헤더를 본다
|
||
|
||
**무엇을 보는가** — 세션이 생겼는지, 그리고 프록시가 업스트림에 무엇을 붙이는지.
|
||
|
||
브라우저에서 `https://app2.hyeonworks.com/api/echo` 를 열고 `labuser` / `labpass` 로 로그인한다. Keycloak 로그인 화면이 뜨고, 통과하면 업스트림(echo)의 JSON 이 보인다.
|
||
|
||
**어디를 보나** — 실측은 이렇다(observed, `b7-oauth2proxy-login-success.png`).
|
||
|
||
```json
|
||
"x-forwarded-email" : [ "labuser@example.com" ],
|
||
"x-forwarded-preferred-username" : [ "labuser" ],
|
||
"x-forwarded-user" : [ "27df5ea9-8703-4ec5-badd-d972c583e1ff" ],
|
||
"x-forwarded-proto" : [ "https" ]
|
||
```
|
||
|
||
**이 값이 뜻하는 것** — B-4 에서 위조가 통한다고 측정한 바로 그 헤더를 oauth2-proxy 가 붙인다. Forward-Auth 구조의 신원 전달 방식이고 B-4 의 결론이 그대로 적용된다 — 엣지가 붙인 것과 공격자가 보낸 것을 업스트림은 구별하지 못한다.
|
||
|
||
### 8. 세션이 Redis 에 들어갔는지, 쿠키가 티켓인지 본다
|
||
|
||
**무엇을 보는가** — 한 번은 통째로 본 다음 접두사로 좁힌다.
|
||
|
||
```bash label="[lab host] ① Redis 전체를 본다"
|
||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern '*'
|
||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli dbsize
|
||
```
|
||
|
||
**어디를 보나** — 실측은 이렇다(observed, `03-rotation.txt`).
|
||
|
||
```text
|
||
=== 세션이 Redis 에 들어갔는가 ===
|
||
b5:pvc
|
||
_oauth2_proxy-b26111fbd1fdab3ae2182e287001b02a
|
||
dbsize: 2
|
||
```
|
||
|
||
`dbsize` 는 2 인데 세션은 하나다. `b5:pvc` 는 B-5 가 남긴 키이고 이 실험과 무관하다. 이 Redis 는 이 실험 전용이 아니므로 `dbsize` 로 세션을 세면 틀린다.
|
||
|
||
```bash label="[lab host] ② 세션만 세려면 접두사로 좁힌다"
|
||
kubectl -n keycloak-lab exec deploy/redis -- \
|
||
redis-cli --scan --pattern '_oauth2_proxy-*'
|
||
```
|
||
|
||
`KEYS` 대신 `--scan` 을 쓴다. `KEYS` 는 Redis 를 블로킹하고, 실험대에서는 티가 안 나지만 습관을 여기서 들인다.
|
||
|
||
**이 값이 뜻하는 것** — 마지막으로 쿠키가 티켓인지 확인한다. 세션 저장소를 Redis 로 옮기면 쿠키에는 세션 전체가 아니라 티켓만 담긴다. 브라우저 개발자 도구에서 Application 또는 저장소 → Cookies → `_oauth2_proxy` 로 본다. 값은 지금 쓸 수 있는 세션 자격증명이라 모양과 길이만 적는다(observed).
|
||
|
||
```text
|
||
_oauth2_proxy=<ticket>|<timestamp>|<mac>
|
||
└─ Redis 키를 여기서 계산한다
|
||
세션 전체가 아니라 티켓이다 (약 180자)
|
||
```
|
||
|
||
`|` 로 나뉜 세 토막과 전체 길이를 본다. 쿠키가 짧아졌고 그래서 502 가 사라졌다. Redis 키 이름은 이 티켓에서 계산되고, 관찰 절의 「지우지 못한다」가 그 관계에서 나온다.
|
||
|
||
## 주입
|
||
|
||
### 1. 겹칠 수 있는지부터 묻는다
|
||
|
||
**무엇을 보는가** — 회전을 치기 전에, 애초에 두 secret 을 동시에 들 수 있는지.
|
||
|
||
```bash label="[lab host] 도움말에서 cookie-secret 을 찾는다"
|
||
kubectl -n keycloak-lab exec deploy/oauth2-proxy -- \
|
||
/bin/oauth2-proxy --help 2>&1 | grep cookie-secret
|
||
```
|
||
|
||
**어디를 보나** — 실측은 이렇다(observed, `03-rotation.txt`).
|
||
|
||
```text
|
||
--cookie-secret string the seed string for secure cookies (optionally base64 encoded)
|
||
```
|
||
|
||
**이 값이 뜻하는 것** — `string` 이고 복수형이 아니다. `--cookie-secrets` 도 `--old-cookie-secret` 도 목록에 없으므로 겹치는 구간을 만들 수단이 아예 없다. B-6 에서 Keycloak 이 두 키를 동시에 들 수 있었던 것은 토큰 헤더에 `kid` 가 있어서였고, oauth2-proxy 의 쿠키에는 그런 식별자가 없다.
|
||
|
||
```text
|
||
식별자 있음 → 읽기는 여러 key, 쓰기는 하나 → 겹침 가능 (B-6)
|
||
식별자 없음 → 전부 한 번에 바뀐다 → 겹침 불가 (B-7)
|
||
```
|
||
|
||
겹칠 수 있는가에는 이 한 줄이 답했고, 남은 절차는 그래서 실제로 무슨 일이 나는지를 본다.
|
||
|
||
### 2. env 인덱스를 확인하고 secret 참조를 A 에서 B 로 바꾼다
|
||
|
||
**목적** — Deployment 가 읽는 cookie secret 을 갈아치운다.
|
||
|
||
**행동** — patch 가 지목하는 `env/1` 이 맞는지 먼저 보고, 시각을 남기고, 바꾼다.
|
||
|
||
```bash label="[lab host] ① env 배열의 이름 순서를 본다"
|
||
kubectl -n keycloak-lab get deploy oauth2-proxy \
|
||
-o jsonpath='{.spec.template.spec.containers[0].env[*].name}'; echo
|
||
```
|
||
|
||
모양은 이렇다(모양은 observed).
|
||
|
||
```text
|
||
OAUTH2_PROXY_CLIENT_SECRET OAUTH2_PROXY_COOKIE_SECRET
|
||
```
|
||
|
||
`OAUTH2_PROXY_COOKIE_SECRET` 이 몇 번째인지 0부터 센다. 위 모양에서는 두 번째이므로 `env/1` 이고, 순서가 다르면 patch 의 숫자를 고친다.
|
||
|
||
```bash label="[lab host] ② 시각을 UTC 로 남기고 참조를 바꾼다"
|
||
date -u '+%H:%M:%S UTC 회전'
|
||
kubectl -n keycloak-lab patch deployment oauth2-proxy --type=json \
|
||
-p '[{"op":"replace",
|
||
"path":"/spec/template/spec/containers/0/env/1/valueFrom/secretKeyRef/key",
|
||
"value":"COOKIE_SECRET_B"}]'
|
||
kubectl -n keycloak-lab rollout status deploy/oauth2-proxy --timeout=180s
|
||
```
|
||
|
||
**예상 결과** — 실측은 이렇다(observed, `03-rotation.txt`).
|
||
|
||
```text
|
||
=== ★ secret 을 A → B 로 교체한다 ===
|
||
deployment.apps/oauth2-proxy patched
|
||
deployment "oauth2-proxy" successfully rolled out
|
||
```
|
||
|
||
**왜 필요한가** — `env/1` 은 매니페스트 순서에 달린 값이라 그대로 믿지 않는다. 틀리면 클라이언트 비밀을 쿠키 secret 으로 덮어쓴다. 그리고 시각을 UTC 로 적어 두는 까닭은 프록시 로그가 UTC 이고 B-7a 의 정리 규칙이 이 값을 기준으로 고아를 고르기 때문이다.
|
||
|
||
**문제가 생기면** — patch 뒤에 프록시가 기동에서 죽으면 인덱스를 잘못 짚었다. `env[*].name` 순서를 다시 본다.
|
||
|
||
## 주입 검증
|
||
|
||
### 1. Deployment 의 참조가 실제로 바뀌었는가
|
||
|
||
```bash label="[lab host] 지금 참조하는 키 이름을 뽑는다"
|
||
kubectl -n keycloak-lab get deploy oauth2-proxy \
|
||
-o jsonpath='{.spec.template.spec.containers[0].env[1].valueFrom.secretKeyRef.key}'; echo
|
||
```
|
||
|
||
실측은 이렇다(observed, `03-rotation.txt`).
|
||
|
||
```text
|
||
현재 secret 키: COOKIE_SECRET_B
|
||
```
|
||
|
||
바뀐 것은 Deployment 의 참조이지 Secret 의 내용이 아니다. 두 값 다 그대로 있고 어느 쪽을 읽을지만 바뀌었으며, 그래서 되돌리기가 한 줄이다.
|
||
|
||
### 2. Redis 는 그대로인가
|
||
|
||
```bash label="[lab host] 주입 전과 똑같은 줄을 친다"
|
||
kubectl -n keycloak-lab exec deploy/redis -- \
|
||
redis-cli --scan --pattern '_oauth2_proxy-*'
|
||
```
|
||
|
||
실측은 이렇다(observed, `03-rotation.txt`).
|
||
|
||
```text
|
||
Redis 세션은 그대로인가: 2 키
|
||
```
|
||
|
||
**어디를 보나** — 나오는 줄의 개수를 주입 전 8 절 ②에서 본 것과 견준다. 위 실측의 「2 키」는 원래 실행이 적어 낸 숫자인데, 같은 실행이 8 절에서 접두사로 좁혀 본 세션은 `_oauth2_proxy-b26111fbd1fdab3ae2182e287001b02a` 하나였고 `b5:pvc` 를 더해야 둘이 된다. 원래 실행이 여기서 무엇을 셌는지는 기록에 없다(unknown). 따라 하는 사람 화면에는 `_oauth2_proxy-` 로 시작하는 줄이 8 절 ②와 같은 수만큼 나온다. 줄이 하나만 나와도 주입은 걸린 상태이고, 판정 기준은 숫자 2 가 아니라 회전 전과 같은 수인가다.
|
||
|
||
세션 수가 회전 전과 같다. 회전 자체는 아무 일도 일으키지 않으므로 여기서 「실험 실패」라고 결론 내리면 틀린다. 무슨 일이 나려면 누군가 옛 쿠키를 들고 와야 한다.
|
||
|
||
### 3. 파드가 실제로 새로 떴는가
|
||
|
||
```bash label="[lab host] 파드 이름이 바뀌었는지 본다"
|
||
kubectl -n keycloak-lab get pods -l app=oauth2-proxy -o wide
|
||
```
|
||
|
||
파드 이름이 주입 전과 다르다. 같으면 patch 가 아무 필드도 안 바꿨다. 이미 B 였거나 경로가 틀렸다.
|
||
|
||
그래서 주입 전 3 절에서 적어 둔 끝 다섯 글자는 여기서 쓸모가 없어진다. 이 출력에 나온 새 이름 둘의 끝 다섯 글자를 다시 적어 둔다 — 아래 관찰 절에서 어느 replica 가 흐름을 시작하고 어느 replica 가 콜백을 받았는지 그 글자로 가른다.
|
||
|
||
## 관찰
|
||
|
||
로그인했던 그 브라우저 그대로 `https://app2.hyeonworks.com/api/echo` 를 연다. 볼 것은 로그인 화면이 뜨는가다.
|
||
|
||
실측은 뜨지 않았다(observed). 화면이 잠깐 깜빡이고 그대로 열린다.
|
||
|
||
Keycloak SSO 세션이 살아 있어서 조용히 재인증이 일어났다. 쿠키는 분명히 무효가 됐는데 사용자 눈에는 아무 일도 없었다. **여기서 읽는 방향이 갈린다.** 「로그인 화면이 안 떴으니 교체가 무중단이구나」로 읽으면 정확히 뒤집어 읽는다. 쿠키는 죽었고 사용자는 실제로 재인증을 거쳤다. SSO 가 그 사실을 가려 준 것이고, IdP SSO 가 없거나 만료됐으면 전원이 로그인 화면을 본다.
|
||
|
||
로그가 무슨 일이 났는지 말한다. 먼저 최근 로그를 통째로 본다.
|
||
|
||
```bash label="[lab host] ① 최근 3분을 파드 이름과 함께 본다"
|
||
kubectl -n keycloak-lab logs -l app=oauth2-proxy --since=3m --prefix
|
||
```
|
||
|
||
`--prefix` 는 각 줄 앞에 파드 이름을 붙여 준다. replica 가 둘이므로 이것이 없으면 누가 무엇을 했는지 못 가린다. `--since=3m` 은 최근 3분만 보므로 브라우저로 접속한 뒤 3분을 넘겨 치면 아무 줄도 안 나온다. 그때 나온 빈 결과는 「로그가 없다」가 아니라 창을 놓쳤다는 뜻이니, 브라우저를 한 번 더 열고 곧바로 친다. 그다음 좁힌다.
|
||
|
||
```bash label="[lab host] ② 세션 저장소 쪽 줄만 좁힌다"
|
||
kubectl -n keycloak-lab logs -l app=oauth2-proxy --since=3m | grep -i stored_session
|
||
```
|
||
|
||
실측은 이렇다(observed, `03-rotation.txt`).
|
||
|
||
```text
|
||
[2026/09/04 05:42:18] [stored_session.go:94] Error loading cookied session: session ticket cookie failed validation: <nil>, removing session
|
||
[2026/09/04 05:42:18] [stored_session.go:97] Error removing session: error decoding ticket to clear session: session ticket cookie failed validation: <nil>
|
||
```
|
||
|
||
두 줄이 다른 말을 하고 있다.
|
||
|
||
| 줄 | 뜻 |
|
||
|---|---|
|
||
| `stored_session.go:94` | 쿠키를 열 수 없다 → 세션을 지우겠다 |
|
||
| `stored_session.go:97` | 그 지우기가 실패했다 → `error decoding ticket to clear session` |
|
||
|
||
94 만 보고 「정리됐구나」로 읽으면 틀린다. 97 이 진짜 결과다. 이어지는 줄이 사용자 쪽 이야기다(observed).
|
||
|
||
```text
|
||
[2026/09/04 05:42:18] [oauthproxy.go:1024] No valid authentication in request. Initiating login.
|
||
... [AuthSuccess] Authenticated via OAuth2: Session{email:labuser@example.com ...
|
||
```
|
||
|
||
`Initiating login` 과 `AuthSuccess` 가 같은 초에 있다. 로그인 흐름이 실제로 돌았고 사람 손이 안 들어갔다. 그 두 줄 사이에 화면이 깜빡였다.
|
||
|
||
그다음 Redis 를 본다.
|
||
|
||
```bash label="[lab host] 세션 키를 다시 센다"
|
||
kubectl -n keycloak-lab exec deploy/redis -- \
|
||
redis-cli --scan --pattern '_oauth2_proxy-*'
|
||
```
|
||
|
||
실측은 이렇다(observed, `03-rotation.txt`).
|
||
|
||
```text
|
||
=== Redis 세션 수 (옛 세션이 남아 있는가) ===
|
||
_oauth2_proxy-978dfaefbdadccb96c7be1625dba5616
|
||
_oauth2_proxy-b26111fbd1fdab3ae2182e287001b02a
|
||
총: 2 개
|
||
```
|
||
|
||
키가 둘이다. 뒤엣것(`b26111f…`)은 회전 전의 세션이고 앞엣것은 방금 새로 생겼다. 사용자는 하나인데 서버 세션이 둘이다. 옛 것은 아무도 쓸 수 없고 프록시도 지우지 못한다.
|
||
|
||
못 지우는 까닭은 티켓과 키의 관계에 있다. Redis 세션 저장소를 쓰면 쿠키에는 티켓만 담기고, 티켓은 두 부분이다.
|
||
|
||
```text
|
||
티켓 = <세션 ID>.<암호화 키>
|
||
│ └─ 값을 복호화할 키
|
||
└─ Redis 키 이름을 만든다 → _oauth2_proxy-<ID>
|
||
```
|
||
|
||
티켓 전체가 cookie secret 으로 봉인되어 있다. secret 을 바꾸면 티켓을 열 수 없고, 그러면 세션 ID 조차 못 읽는다. 프록시는 「이 세션은 못 쓴다」까지는 알지만 그 세션이 Redis 어디에 있는지를 모른다. 그래서 `removing session` 을 시도하고 실패한다.
|
||
|
||
```text
|
||
secret 교체
|
||
└─ 옛 티켓을 못 푼다
|
||
├─ 사용자는 재로그인 (SSO 가 있으면 조용히)
|
||
└─ ★ 서버 세션은 TTL 만료까지 고아로 남는다
|
||
```
|
||
|
||
로그인한 사용자 수만큼 고아가 생긴다. 이 절차는 여기서 멈춘다. 정말 사라지는지, 운영자는 지울 수 있는지, 어느 것이 고아인지는 B-7a 가 이어서 잰다.
|
||
|
||
덤으로, 로그를 파드별로 갈라 보면 BFF 와 정반대인 성질이 보인다.
|
||
|
||
```bash label="[lab host] 흐름을 시작한 파드와 콜백을 받은 파드를 가른다"
|
||
kubectl -n keycloak-lab logs -l app=oauth2-proxy --since=10m --prefix \
|
||
| grep -E 'Initiating login|AuthSuccess'
|
||
```
|
||
|
||
실측은 해설 문서에 이 모양으로 남아 있다(observed).
|
||
|
||
```text
|
||
--- replica 8p5hl ---
|
||
[oauthproxy.go:1024] No valid authentication in request. Initiating login.
|
||
GET "/api/echo" ← 흐름을 시작한 replica
|
||
|
||
--- replica b9928 ---
|
||
[AuthSuccess] Authenticated via OAuth2: Session{email:labuser@example.com ...}
|
||
GET "/oauth2/callback?state=..." ← 콜백을 받은 replica
|
||
```
|
||
|
||
예시의 `8p5hl` · `b9928` 은 주입 전 3 절이 보여 준 회전 전 파드 이름과 같다. 회전이 파드를 새로 띄웠으므로 따라 하는 사람 화면에는 주입 검증 3 절에서 다시 적어 둔 이름이 나온다. 글자만 다르고 읽는 법은 같다.
|
||
|
||
시작한 파드와 콜백을 처리한 파드가 다른데 성공했다.
|
||
|
||
| 어느 쪽인가 | 인가 요청(state, CSRF)을 어디에 두는가 | replica 간 |
|
||
|---|---|---|
|
||
| BFF | 서버 메모리(HttpSession) | 콜백이 다른 인스턴스로 가면 실패 (B-0) |
|
||
| oauth2-proxy | 쿠키 (secret 으로 봉인) | secret 만 같으면 성공 |
|
||
|
||
「어떻게 공유하는가」에 이 로그가 답한다 — replica 들이 나눠 가질 상태가 없고, 같아야 하는 값은 k8s Secret 하나다. 대신 그 하나가 단일 지점이 된다.
|
||
|
||
## 복구와 원상복구 확인표
|
||
|
||
### 1. secret 참조를 A 로 되돌린다
|
||
|
||
**목적** — 실험 전 상태로 돌린다.
|
||
|
||
**행동** — 시각을 남기고 되돌린다.
|
||
|
||
```bash label="[lab host] 되돌리는 것도 회전이다"
|
||
date -u '+%H:%M:%S UTC 되돌림'
|
||
kubectl -n keycloak-lab patch deployment oauth2-proxy --type=json \
|
||
-p '[{"op":"replace",
|
||
"path":"/spec/template/spec/containers/0/env/1/valueFrom/secretKeyRef/key",
|
||
"value":"COOKIE_SECRET_A"}]'
|
||
kubectl -n keycloak-lab rollout status deploy/oauth2-proxy --timeout=180s
|
||
```
|
||
|
||
**예상 결과** — 롤아웃이 끝나고 파드 이름이 또 바뀐다.
|
||
|
||
**왜 필요한가** — 되돌리기도 회전이므로 B 로 만든 세션이 이번에는 고아가 된다.
|
||
|
||
**문제가 생기면** — 참조가 안 바뀌면 `env` 인덱스를 다시 본다.
|
||
|
||
### 2. 고아를 어떻게 할지 고른다
|
||
|
||
고아를 정리하는 선택지는 셋이다.
|
||
|
||
| 무엇을 | 언제 | 어떻게 |
|
||
|---|---|---|
|
||
| 그냥 둔다 | 실험대 | TTL(1시간)이 지나면 사라진다 |
|
||
| TTL 로 골라 지운다 | 산 세션을 살리고 싶을 때 | B-7a 의 규칙 |
|
||
| 전부 지운다 | 어차피 다 무효일 때 | 아래 |
|
||
|
||
전부 지울 때는 `b5:pvc` 같은 남의 키를 같이 죽이지 않도록 패턴으로 좁힌다. `FLUSHDB` 를 쓰지 않는다 — 이 Redis 는 BFF 세션도 담고 있다.
|
||
|
||
```bash label="[lab host] 접두사에 걸린 것만 지운다"
|
||
kubectl -n keycloak-lab exec deploy/redis -- \
|
||
redis-cli --scan --pattern '_oauth2_proxy-*' | while read K; do
|
||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli del "$K"
|
||
done
|
||
```
|
||
|
||
### 3. Grafana Ingress 를 돌려준다
|
||
|
||
**목적** — 빌린 도메인을 원래 주인에게 돌린다.
|
||
|
||
**행동** — oauth2-proxy 것을 먼저 지우고 Grafana 것을 올린 뒤 밖에서 확인한다.
|
||
|
||
**지우기 전에 백업 파일이 손에 있는지 본다.** 지금은 Grafana Ingress 가 이미 없으니 파일이 비어 있으면 다시 뜰 원본도 없고, 되살리는 절차는 가이드에 없다(unknown). 아래 두 줄의 첫째가 줄 수를 내고 둘째가 `0` 이 아니면 그 파일로 돌려줄 수 있다. `No such file or directory` 가 나오면 ①을 치지 않는다 — B-4 로 app2 를 빌린 적이 있으면 그 편은 같은 백업을 `/tmp/grafana-ingress-backup.yaml` 에 떠 두므로 그쪽을 본다.
|
||
|
||
```bash label="[lab host] ⓪ 백업 파일이 쓸 만한지 본다"
|
||
wc -l ~/grafana-ingress-backup.yaml
|
||
grep -c 'app2.hyeonworks.com' ~/grafana-ingress-backup.yaml
|
||
```
|
||
|
||
```bash label="[lab host] ① 빌린 것을 걷고 백업을 올린다"
|
||
kubectl -n keycloak-lab delete ingress oauth2-proxy
|
||
kubectl apply -f ~/grafana-ingress-backup.yaml
|
||
```
|
||
|
||
```bash label="[kc-lab-1 → test-server] ② Ingress 와 밖에서 본 응답을 함께 본다"
|
||
kubectl -n observability get ingress grafana
|
||
curl -sI https://app2.hyeonworks.com/ | head -3
|
||
```
|
||
|
||
첫 줄은 `kc-lab-1` 에서 치고, `curl` 은 1 절 ①과 같게 `test-server` 에서 친다. 기계가 다르면 1 절에서 본 응답과 견줄 수 없다.
|
||
|
||
**예상 결과** — Ingress 가 `observability` 에 다시 있고 `app2` 응답이 1 절에서 처음 본 모양으로 돌아온다.
|
||
|
||
**왜 필요한가** — 순서를 바꾸면 어느 쪽으로 갈지가 컨트롤러 판단에 맡겨진다. 그리고 되돌리지 않으면 실험이 끝나도 Grafana 가 안 열린다.
|
||
|
||
**문제가 생기면** — oauth2-proxy 전체를 걷어내려면 `kubectl delete -f deploy/lab/k8s/b7-oauth2-proxy.yaml` 인데, B-7a 와 C-1 이 이 배포를 그대로 쓴다. 이어서 할 생각이면 남겨 두고, 그때는 Grafana Ingress 복구도 그 실험이 끝난 뒤로 미룬다.
|
||
|
||
| 항목 | 명령 | 돌아왔을 때 |
|
||
|---|---|---|
|
||
| secret 참조 | `get deploy oauth2-proxy -o jsonpath='{...env[1]...key}'` | `COOKIE_SECRET_A` |
|
||
| 파드 | `kubectl -n keycloak-lab get pods -l app=oauth2-proxy` | 둘 다 `1/1 Running` |
|
||
| Redis | `redis-cli --scan --pattern '_oauth2_proxy-*'` | 남기기로 한 만큼만 |
|
||
| Ingress (빌린 것) | `kubectl -n keycloak-lab get ingress` | oauth2-proxy 것이 없다 (걷어냈다면) |
|
||
| Ingress (Grafana) | `kubectl -n observability get ingress grafana` | 있다 |
|
||
| 밖 | `curl -sI https://app2.hyeonworks.com/ \| head -3` | Grafana 로 간다 |
|
||
|
||
## 막히면
|
||
|
||
원래 실행이 실제로 겪은 증상이고 지어낸 것은 없다고 가이드가 적는다.
|
||
|
||
| 증상 | 원인 | 확인 |
|
||
|---|---|---|
|
||
| 콜백이 `502 Bad Gateway` | 쿠키가 크다. `Set-Cookie` 가 nginx 버퍼를 넘겼다 | Traefik 직접이 200 인지. Redis 세션 저장소로 옮긴다 |
|
||
| 호스트에서 nginx 설정과 로그가 빈 결과 | `sudo` 가 조용히 실패했다 | `sudo -n true` → `sudo: a password is required` |
|
||
| `--cookie-secrets` 를 찾는데 없다 | 단수다. 겹치는 구간이 애초에 없다 | `--help \| grep cookie-secret` |
|
||
| patch 뒤 프록시가 기동에서 죽는다 | env 인덱스를 잘못 짚어 클라이언트 비밀을 덮었다 | `env[*].name` 순서 확인 |
|
||
| secret 을 바꿨는데 Redis 가 그대로 | 정상이다. 옛 쿠키를 들고 오는 요청이 있어야 벌어진다 | 브라우저로 접근 |
|
||
| 로그인 화면이 안 떠서 「무중단」이라 읽었다 | SSO 가 재인증을 가렸다. 쿠키는 죽었다 | 로그의 `Initiating login` 과 `AuthSuccess` |
|
||
| 로그가 파드마다 섞여 못 읽겠다 | replica 가 둘이다 | `logs -l app=oauth2-proxy --prefix` |
|
||
| `dbsize` 로 세션을 셌더니 안 맞는다 | `b5:pvc` 등 다른 키가 섞인다 | `--scan --pattern '_oauth2_proxy-*'` |
|
||
| 파드 IP 로 `/oauth2/auth` 를 쳤더니 `HTTP 000` | 호스트에서 파드 IP 는 안 닿는다 | 공개 이름으로 치거나 클러스터 안 임시 파드를 쓴다 |
|
||
| `curl` 로 OIDC 흐름을 완주하려다 실패 | 쿠키가 `HttpOnly` 이고 폼을 거쳐야 한다 | 브라우저를 쓴다 |
|
||
| 로그 시각이 9시간 어긋난다 | 프록시 로그는 UTC | KST = UTC+9 |
|
||
| `app2` 가 Grafana 로 간다 | Ingress 를 안 만들었거나 이미 복구했다 | `kubectl -n keycloak-lab get ingress` |
|
||
| 실험이 끝났는데 Grafana 가 안 열린다 | Ingress 복구를 안 했다 | oauth2-proxy Ingress 를 먼저 지우고 백업을 올린다 |
|
||
|
||
## 이 실험대에서 아직 못 밟은 단계
|
||
|
||
2026-09-17 에 기반 가이드로 실험대를 새로 세우고 이 편을 어디까지 밟고 멈췄는지 적는다. **못 밟은 것을 밟은 것처럼 적지 않으려고 남긴다.**
|
||
|
||
- **남은 것** — 쿠키 비밀 회전 전부. `oauth2-proxy` 가 기동 시 OIDC 디스커버리를 `https` 로 하므로 인증서가 서야 뜬다.
|
||
- **막는 것** — `https://auth.hyeonworks.com` 이 서지 않는다. 와일드카드 인증서(Cloudflare API 토큰이 필요한 DNS-01)와, 밖에서 실험대에 닿는 길(호스트의 libvirt `guest_input` 구멍 — A-4 에서 확인한 `ExecStartPost` 누락)이 둘 다 있어야 한다.
|
||
- **그때까지 이 편의 실측 가운데 `(observed)` 로 적힌 2026-09-17 값은 위 「지금까지 밟은 것」 범위뿐이다.** 나머지는 원래 실행의 값이다.
|
||
|
||
## 무엇이 관측이고 무엇이 아닌가
|
||
|
||
이 절차의 숫자는 `2026-09-04 14:35–14:42 KST` 에 돈 한 번의 실행에서 나왔다(observed). 증거의 로그가 `[2026/09/04 05:41:46]` 인 것과 수집 시각이 `14:35–14:42 KST` 인 것은 같은 순간이다(KST = UTC+9). 이 어긋남을 모르고 로그를 뒤지면 9시간 전을 뒤지게 된다.
|
||
|
||
- (observed) Grafana Ingress 삭제 한 줄과 배포 출력 여섯 줄, 파드 두 개(`oauth2-proxy-c76b49c59-8p5hl` @ `kc-lab-1` · `oauth2-proxy-c76b49c59-b9928` @ `kc-lab-2`), 진입점 `/ HTTP 302` · `/ping HTTP 200`, Traefik 직접의 `200` 과 `302`, `sudo -n true` → `sudo: a password is required`, 로그인 뒤 업스트림이 받은 헤더 네 줄, 회전 전 Redis 의 `b5:pvc` · `_oauth2_proxy-b26111fbd1fdab3ae2182e287001b02a` · `dbsize: 2`, `--cookie-secret string` 도움말 한 줄, 교체 출력 두 줄과 `현재 secret 키: COOKIE_SECRET_B`, 교체 직후 `Redis 세션은 그대로인가: 2 키`, `stored_session.go:94` 와 `97` 두 줄과 `Initiating login` · `AuthSuccess`, 회전 뒤 Redis 의 키 둘과 `총: 2 개`, replica 를 갈라 본 로그.
|
||
- 비밀은 이름과 길이만 적었다. Secret 의 키 이름 셋(`CLIENT_SECRET` · `COOKIE_SECRET_A` · `COOKIE_SECRET_B`)과 16 · 24 · 32 바이트라는 제약만 옮겼고 값은 어디에도 안 적었다. 쿠키도 `<ticket>|<timestamp>|<mac>` 이라는 모양과 약 180자라는 길이만 옮겼다 — 지금 쓸 수 있는 세션 자격증명이라 원문은 해설 문서에 있다. Redis 키 이름과 파드 이름은 식별자라 그대로 적었다.
|
||
- (unknown) `COOKIE_SECRET_A` 와 `B` 의 길이를 재는 두 줄. 가이드가 미검증으로 표시했고 원래 실행 기록에 이 명령의 출력이 없다. 16 · 24 · 32 바이트라는 제약은 oauth2-proxy 의 것이지 이 실험대가 잰 값이 아니다.
|
||
- 예상이 빗나간 대목(observed) — 브라우저에서 로그인 화면이 안 떴다. 그것을 「무중단」으로 읽으면 뒤집어 읽은 것이고, 로그의 `Initiating login` 과 `AuthSuccess` 가 재인증이 실제로 돌았다는 값이다. IdP SSO 가 없거나 만료된 경우에 전원이 로그인 화면을 보는지는 재지 않았다(unknown).
|
||
- 이 절차가 재지 않은 것 — 고아가 정말 사라지는지, 운영자가 지울 수 있는지, 어느 것이 고아인지는 여기서 재지 않고 B-7a 로 넘겼다. 502 를 고치는 다른 길(nginx 의 `proxy_buffer_size` 를 키우는 것)도 재지 않았다. 세션을 Redis 로 옮기는 쪽만 쟀다.
|
||
|
||
<!-- body:end -->
|