Files
document-haness/docs/keycloak-session-store/tech-log-studio/trust-handed-over-at-the-edge/setup/setup-reproduce-b7-cookie-secret-rotation.md
T
DongHyeonkaandClaude Opus 5 2109f726fe feat(pipeline): keycloak-session-store 25편·virtualization 59편을 S3→S5→S6 으로 돌린다
기록 84편을 계약 에이전트로 다시 썼다. 기존 71편(kss 25 · virt 46)과, 계약에만
있고 안 쓰여 있던 새 글감 13편이다. 원장 84개를 열어 단계마다 스킬 영수증과 관문
종료 코드를 적었고 verify-pipeline-run.py 가 error 0 으로 닫는다.

SSOT 결함 둘을 고쳤다.

- kss 의 `약 58일` 이 반입 중 `약 59일` 로 바뀌어 있었다. 원 증거 파일이
  「남은 일수: 88일 … 실제 갱신까지 약 58일」로 산수를 직접 적는다. D-4a 쪽
  `약 59일` 은 강제 갱신 뒤(`VALID: 89 days`)라 맞는 값이라 그대로 뒀다.
- virt §198 의 `11.6GB` 는 §178 의 원 측정 `Mem: 11648`(MiB)과 어긋나는데
  원 가이드의 표기 그대로라 고치지 않고 쓰이는 자리에 대조를 적었다.

기록의 수치 오류 셋을 고쳤다 — CASE 요약의 「게스트 셋에 8240MB」(5120+3120 은
둘이다), k3s 편이 같은 것을 여섯·일곱·여덟로 세던 것, no-docker 편의 「셋을 더
든다」(§281 의 표는 네 행이고 디스크 행이 빠져 있었다).

계약을 셋 고쳤다.

- kss 의 sourceRepository 리비전이 cdac9b8 이었는데 그 커밋에는 docs/guides/**
  28개가 아예 없다. 9465582b 로 바꾸고, 반입한 바이트가 어느 커밋과도 같지 않다는
  것을 측정값과 함께 적었다 — 반입은 커밋이 아니라 그 시점의 작업 트리에서 떠 온
  것이다(kss 297/306 · virt 12/14 가 작업 트리와 같고, 200 커밋을 거슬러 전수
  대조했을 때 가장 가까운 커밋도 28개가 어긋났다).
- virt 계약이 「2026-09-11 재배분」이라고 적는데 SSOT 는 재배분 날짜를 적지 않고
  재배분 뒤 값은 이미 2026-09-10 측정에 찍혀 있다.
- kss 후보 대장이 지나친 절 아홉에 처분을 적었다(warn 9 → 0). 새 글감은 0건이고
  넷은 앵커가 h3 슬러그의 접두가 아니라 중간 토막이라 검사기가 못 본 것이었다.

style_profile.mjs 의 결함 둘을 고쳤다 — frontmatter 가 문장으로 세어져
(실측 398자짜리 「문장」 하나) 평균 길이를 기준 안으로 밀어 올리고 있었고,
engPerSent 의 분자는 목록을 포함한 글에서, 분모는 목록을 걷어낸 글에서 세고
있었다(Question 기록에서 11.94 → 3.86).

verify-pipeline.py 전 항목 PASS · error 0 · unittest 334건 OK.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 11:01:55 +09:00

677 lines
39 KiB
Markdown
Raw 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.
---
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``[kc-lab-1]` 에서 치고, 앞단 nginx 를 건너뛰고 Traefik 을 직접 두드리는 `curl``sudo``[test-server]` 에서 친다. 앞단 nginx 가 그 호스트에 있고 `192.168.122.11:80` 으로 넘겨주므로, 호스트에서 그 주소를 바로 치면 nginx 를 건너뛴다.
브라우저도 필요하다. 쿠키가 `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` 로 떠 있다.
- 브라우저가 있어야 한다.
**이건 남의 도메인을 빌리고 남의 세션을 끊는 실험이다.** 둘을 건드린다. 인증서가 `auth` · `app1` · `app2` 세 이름만 덮어서 네 번째 이름을 못 만들기 때문에 Grafana 의 Ingress 를 잠시 내리고 `app2` 를 빌린다. 그리고 secret 을 바꾸면 그때 로그인해 있던 사람의 쿠키가 전부 무효가 된다.
되돌리기는 둘이고 먼저 읽어 둔다.
```bash label="[kc-lab-1] ① 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="[kc-lab-1] ② 빌린 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="[kc-lab-1] ② 백업을 뜨고 내용이 있는지 센다"
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="[kc-lab-1] ① Grafana Ingress 를 내린다"
kubectl -n observability delete ingress grafana
```
```bash label="[kc-lab-1] ② 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="[kc-lab-1] 파드 배치를 본다"
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="[kc-lab-1] 세션 저장소가 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="[kc-lab-1] ① 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="[kc-lab-1] ② 두 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="[kc-lab-1] ① 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="[kc-lab-1] ② 세션만 세려면 접두사로 좁힌다"
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="[kc-lab-1] 도움말에서 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="[kc-lab-1] ① 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="[kc-lab-1] ② 시각을 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="[kc-lab-1] 지금 참조하는 키 이름을 뽑는다"
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="[kc-lab-1] 주입 전과 똑같은 줄을 친다"
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="[kc-lab-1] 파드 이름이 바뀌었는지 본다"
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="[kc-lab-1] ① 최근 3분을 파드 이름과 함께 본다"
kubectl -n keycloak-lab logs -l app=oauth2-proxy --since=3m --prefix
```
`--prefix` 는 각 줄 앞에 파드 이름을 붙여 준다. replica 가 둘이므로 이것이 없으면 누가 무엇을 했는지 못 가린다. `--since=3m` 은 최근 3분만 보므로 브라우저로 접속한 뒤 3분을 넘겨 치면 아무 줄도 안 나온다. 그때 나온 빈 결과는 「로그가 없다」가 아니라 창을 놓쳤다는 뜻이니, 브라우저를 한 번 더 열고 곧바로 친다. 그다음 좁힌다.
```bash label="[kc-lab-1] ② 세션 저장소 쪽 줄만 좁힌다"
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="[kc-lab-1] 세션 키를 다시 센다"
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="[kc-lab-1] 흐름을 시작한 파드와 콜백을 받은 파드를 가른다"
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="[kc-lab-1] 되돌리는 것도 회전이다"
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="[kc-lab-1] 접두사에 걸린 것만 지운다"
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="[kc-lab-1] ⓪ 백업 파일이 쓸 만한지 본다"
wc -l ~/grafana-ingress-backup.yaml
grep -c 'app2.hyeonworks.com' ~/grafana-ingress-backup.yaml
```
```bash label="[kc-lab-1] ① 빌린 것을 걷고 백업을 올린다"
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-04 14:3514:42 KST` 에 돈 한 번의 실행에서 나왔다(observed). 증거의 로그가 `[2026/09/04 05:41:46]` 인 것과 수집 시각이 `14:3514: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 -->