test-server 를 비우고 다시 세운 뒤 Setup 기록 35편(virtualization 9 ·
keycloak-session-store 26)을 문서에 적힌 명령 그대로 쳤다. 어긋난 자리를
기록과 SSOT 양쪽에 실측과 함께 넣었다.
막히던 것
- 04 의 인증서 경로가 live/hyeonworks.com 이라 nginx 가 [emerg] 로 안 떴다.
실제 계보는 live/auth.hyeonworks.com 이고 「문제가 생기면」은 진단이 거꾸로였다
- 인증서가 와일드카드가 아니다. SAN 이 auth·app1·app2 셋뿐이라 그 밖의 이름은
TLS 에서 끊기고 curl 이 exit 60 · %{http_code} 000 을 낸다. SSOT 안에서
두 문단이 서로 어긋나 있었다
- A-7 14번 ①이 kc-lab-1 에서 여섯 줄 다 실패하는데 마지막 date 만 「차단」을 찍는다
검사가 실패할 수 없던 자리
- B-1 의 세션 키 고르기는 앞 단계가 $KEY 를 채워 둬서 루프가 한 건도 못 맞혀도
통과한다. KEY= 로 비우고 키마다 1/0 을 찍게 바꿨다
- k3s-agent 유닛의 sed -i 는 패턴에 $HOME 이 들어 있어 아무 줄도 안 바꾼 채 성공한다
certbot
- renew --dry-run 의 종료 코드는 성공도 0, 실패도 0, 다른 사유의 실패는 1 이다.
본문의 renew failure(s) 로만 판정할 수 있다
- --dry-run 은 staging 서버를 쓰는데 renewal/*.conf 의 account= 는 운영 계정을
가리킨다. 실패한 dry-run 이 staging 계정을 하나 더 만들어 다음 실행이 계속 멎는다
- 훅을 755 로 놓고 시뮬레이션이 성공해도 Running deploy-hook command 는 안 나온다.
certbot 2.1.0 에는 --run-deploy-hooks 도 없다
- 강제 갱신은 실제로 쳤고 서빙까지 닿았다. serial 06F3E0EF…1373 → 065547…3DF1,
notAfter Dec 3 → Dec 16, nginx worker 2629 4712 → 4745 4754
독자가 칠 수 있는 형태로
- 안 되는 형태가 번호 붙은 단계에 앉아 있던 8곳을 뒤집고, 되는 형태를 ①로 올렸다
- 랩 안에서 공개 이름을 치는 curl 65줄에 --resolve 를 붙였다. 붙인 형태를 실제로
쳐서 문서가 적은 값과 같은지 확인했다
- 힙독·sed -i·echo >>·&&·|| 를 편집기 + 파일 리스팅 + 분할 형태로 바꿨다
- 닫는 코드펜스가 빠져 뒤 200여 줄의 블록 종류가 뒤집혀 있던 곳을 포함해 3곳을 고쳤다
관문: check_body PASS · check_prose error 0 · check_evidence 두 프로젝트 문제 없음 ·
verify-tech-log-tree error 0 · verify-project-layout error 0 · 코드펜스 전수 0건
남은 것: B-0 주입은 keycloak-pattern 저장소의 소스를 고치고 이미지를 다시 구워야
해서 안 했다(unknown).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
46 KiB
id, kind, slug, title, topic, topicName, project, status, studio, pinnedVersions, source, sourceRevision
| id | kind | slug | title | topic | topicName | project | status | studio | pinnedVersions | source | sourceRevision | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| d53181b9-bb28-4703-97ed-98adfb5b18dc | SETUP | reproduce-b7-cookie-secret-rotation | cookie secret 을 갈아치우고 로그인해 있던 세션이 어떻게 되는지 본다 | trust-handed-over-at-the-edge | 위조 신원 헤더와 로그아웃 전파 | keycloak-session-store | 게시 전 | https://hyeonworks.com/studio/documents/d53181b9-bb28-4703-97ed-98adfb5b18dc/edit |
|
|
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 를 거기서 띄운다.
본문
읽기 전에 — 어디서 치는가
기계가 둘이고 표시가 둘이다. 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 도 이 실험대에 없다 |
공개 이름은 랩 안에서 안 풀린다. auth.hyeonworks.com 같은 공개 이름이 랩 호스트에서도 게스트에서도 호스트 자신의 tailnet 주소 100.83.212.4 로 풀리는데 그 주소에는 443 을 듣는 것이 없다. 이름만 치면 curl 이 000 을 낸다(2026-09-17, 랩 호스트와 kc-lab-1 양쪽에서 쳐서 확인했다, observed). 그래서 랩 안에서 치는 curl 에는 --resolve <이름>:443:192.168.122.10 을 붙여 엣지 게스트를 짚었고, 그 형태로는 정문이 200 이다(observed). tailnet 에 붙은 다른 기계에서 치면 이름 그대로 닿으므로 --resolve 가 필요 없다.
이 실험이 가르는 것
앞선 작업이 남긴 열린 질문 Q1 의 미지수 7 이 이렇게 물었다.
"OAuth2-Proxy 구조의 replica 들이 같은 cookie secret 을 어떻게 공유하고 교체하게 되는가. 교체하는 동안 로그인해 있던 사람은 어떻게 되는가."
B-6 에서 Keycloak 은 두 키를 동시에 들고 무중단으로 회전했다. 토큰 헤더에 kid 가 있어서 읽기는 여러 키, 쓰기는 하나가 됐기 때문이다.
| 무엇을 기대했나 | 무엇이 나왔나 |
|---|---|
| B-6 의 모양대로라면 oauth2-proxy 도 겹치는 구간을 만들 수 있을 것 | --cookie-secret 은 단수이고 쿠키에 키 식별자가 없다 |
예측하지 않았던 것이 하나 더 나온다. 사용자는 아무것도 못 느끼는데 서버 쪽에 지워지지 않는 세션이 생긴다. 그 「지우지 못한다」를 이어서 재는 것이 B-7a 이고, 이 절차는 거기까지 가지 않는다.
두 구조는 인가 요청을 어디에 두는지가 다르다.
BFF 인가 요청을 서버 메모리(HttpSession)에 둔다 → replica 를 넘으면 실패
oauth2-proxy 인가 요청을 쿠키에 두고 secret 으로 봉인한다 → replica 를 넘어도 성공
대신 secret 이 단일 지점
절차를 끝까지 밟으면 --cookie-secret 이 단수라는 도움말 한 줄, 회전만으로는 Redis 가 그대로인 것, 로그인 화면이 안 뜨는데 로그에는 재인증이 찍혀 있는 것, 세션 키가 하나에서 둘로 늘어난 것, 흐름을 시작한 파드와 콜백을 받은 파드가 다른데도 성공한 것을 자기 화면에서 보게 된다.
전제와 되돌리기
05-keycloak이 끝나 있고 realmkeycloak-patterns에 클라이언트oauth2-proxy와 사용자labuser(비밀번호labpass)가 있다.
★ 그 클라이언트를 만드는 단계가 어디에도 없다(2026-09-17, observed). 기반 가이드 05 도, 저장소의 realm 임포트(keycloak/import/keycloak-patterns-realm.json)도 oauth2-proxy 를 만들지 않는다. 임포트에 있는 것은 bff-confidential · edge-proxy · mock-google-broker · spa-public · token-mediating-confidential 다섯이다. 새로 세운 실험대에서 이 편을 밟으면 로그인 화면 대신 이 문장을 만난다.
We are sorry...
Client not found.
oauth2-proxy 파드는 멀쩡히 뜨고 app2 도 302 를 내므로 배포는 다 된 것처럼 보이고, 로그인 화면까지 가서야 드러난다. 만드는 한 줄은 이렇다 — 값은 매니페스트가 읽는 Secret 에서 그대로 꺼내 넘긴다.
CS=$(kubectl -n keycloak-lab get secret oauth2-proxy-secrets -o jsonpath='{.data.CLIENT_SECRET}' | base64 -d)
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh create clients -r keycloak-patterns \
-s clientId=oauth2-proxy -s enabled=true -s protocol=openid-connect \
-s publicClient=false -s standardFlowEnabled=true -s directAccessGrantsEnabled=false \
-s "secret=$CS" \
-s 'redirectUris=["https://app2.hyeonworks.com/oauth2/callback"]' \
-s 'webOrigins=["https://app2.hyeonworks.com"]'
redirectUris 는 매니페스트의 --redirect-url 과 한 글자도 달라선 안 된다. B-7a 와 C-2 도 같은 클라이언트를 쓰므로 똑같이 막힌다.
- B-0 이 끝나 있어야 한다. Redis 를 거기서 띄우고
redis.keycloak-lab.svc:6379로 떠 있다. - 브라우저가 있어야 한다.
oauth2-proxy 는 인증서가 서기 전에는 못 뜬다. 기동할 때 OIDC 디스커버리를 먼저 하는데 그 주소가 https://auth.hyeonworks.com/... 이라, 인증서 단계를 안 끝낸 실험대에서는 CrashLoopBackOff 로 끝난다. 2026-09-17 에 그대로 겪었다(observed).
[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 다.
그런데 위 오류의 주소가 원인을 하나 가리고 있었다(2026-09-17, observed). 100.83.212.4 는 랩 호스트의 tailnet 주소이고, 기반 가이드 03 이 nginx 를 엣지 게스트로 옮긴 뒤로 그 주소에는 443 도 80 도 없다. 그러니 인증서를 받아 엣지에 얹었더라도 파드는 여전히 이 주소를 두드렸을 것이다. 클러스터 DNS 가 그 이름을 엣지로 보내게 고친 뒤 같은 배포를 다시 했더니 주소만 바뀌고 증상은 같았다.
[2026/09/17 07:37:18] [provider.go:55] Performing OIDC Discovery...
[2026/09/17 07:37:18] [main.go:59] ERROR: Failed to initialise OAuth2 Proxy: ... dial tcp 192.168.122.10:443: connect: connection refused
이제 두드리는 곳이 엣지(192.168.122.10)이고, 거기 443 이 안 열린 것은 인증서가 없어서다. 같은 connection refused 인데 앞의 것은 원인이 둘이었고 뒤의 것은 하나다. 03 의 「클러스터 안에서도 공개 이름에 못 닿는다」를 먼저 밟고 와야 이 편의 전제가 TLS 하나로 좁혀진다.
이건 남의 도메인을 빌리고 남의 세션을 끊는 실험이다. 둘을 건드린다. 인증서가 auth · app1 · app2 세 이름만 덮어서 네 번째 이름을 못 만들기 때문에 Grafana 의 Ingress 를 잠시 내리고 app2 를 빌린다. 그리고 secret 을 바꾸면 그때 로그인해 있던 사람의 쿠키가 전부 무효가 된다.
되돌리기는 둘이고 먼저 읽어 둔다.
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 으로 덮어쓴다.
kubectl -n keycloak-lab delete ingress oauth2-proxy
kubectl apply -f ~/grafana-ingress-backup.yaml
②가 올리는 백업 파일은 아래 주입 전 1 절이 만든다. B-4 로 app2 를 먼저 빌린 적이 있으면 그 편은 같은 백업을 /tmp/grafana-ingress-backup.yaml 에 떠 두므로, ~ 쪽이 없을 때 그쪽을 본다.
주입 전에 같은 명령으로 먼저 본다
Ingress 백업 → 배포 → replica 배치 → secret 키 이름 → 로그인 → Redis → 쿠키 모양
1. 지금 app2 가 무엇인지 보고 Grafana Ingress 를 백업한다
목적 — 실험이 끝났을 때 되돌릴 파일을 만든다.
행동 — 먼저 지금 상태를 보고, 백업을 뜨고, 그 백업이 비어 있지 않은지 확인한다.
curl -sI --resolve app2.hyeonworks.com:443:192.168.122.10 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 을 먼저 쳐서 쓸 만한 백업을 이미 갖고 있는지 보고, 갖고 있으면 ②를 건너뛴다.
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), 그 경로가 풀리는 디렉터리로 옮긴 다음 ②를 친다.
kubectl -n observability delete ingress grafana
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).
=== Grafana ingress 를 잠시 내린다 (app2 를 빌린다) ===
grafana ingress 삭제
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 가 둘인지, 어느 노드에 있는지 본다
무엇을 보는가 — 파드 수와 배치.
kubectl -n keycloak-lab get pods -l app=oauth2-proxy -o wide
어디를 보나 — 실측은 이렇다(observed, 01-deploy.txt).
oauth2-proxy-c76b49c59-8p5hl true kc-lab-1
oauth2-proxy-c76b49c59-b9928 true kc-lab-2
이 값이 뜻하는 것 — 파드 두 개가 서로 다른 노드에 있다. 파드 이름의 끝 다섯 글자를 적어 둔다. 관찰 절에서 어느 replica 가 무엇을 했는지 그 글자로 가른다. replica 가 하나면 「공유」라는 말이 성립하지 않는다.
4. 진입점 두 곳이 갈라지는지 본다
무엇을 보는가 — 인증을 거치는 경로와 안 거치는 경로.
curl -s -o /dev/null -w '/ %{http_code}\n' --resolve app2.hyeonworks.com:443:192.168.122.10 \
https://app2.hyeonworks.com/
curl -s -o /dev/null -w '/ping %{http_code}\n' --resolve app2.hyeonworks.com:443:192.168.122.10 \
https://app2.hyeonworks.com/ping
어디를 보나 — 실측은 이렇다(observed, 01-deploy.txt).
=== 진입점 확인 ===
https://app2.hyeonworks.com/ HTTP 302
/ping HTTP 200
| 경로 | 정상 | 뜻 |
|---|---|---|
/ |
302 |
인증이 없으니 Keycloak 으로 보낸다 — 프록시가 일하고 있다 |
/ping |
200 |
인증을 거치지 않는 헬스 경로 — 프록시 자체는 살아 있다 |
이 값이 뜻하는 것 — /ping 도 안 되면 프록시가 안 떴고, /ping 만 되면 프록시는 떴는데 앞단이 무언가를 막고 있다.
5. 502 를 만나면 한 겹씩 벗겨 좁힌다
무엇을 보는가 — 502 를 낸 것이 앞단 nginx 인지, 그 뒤 Traefik 인지, 파드인지. 원래 구성에서 콜백이 계속 502 였다.
curl -H "Host: app2.hyeonworks.com" http://192.168.122.11/ping
curl -H "Host: app2.hyeonworks.com" http://192.168.122.11/
어디를 보나 — 실측은 이렇다(observed).
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 로 옮기는 것이고 매니페스트에 이미 들어 있다.
kubectl -n keycloak-lab get deploy oauth2-proxy \
-o jsonpath='{.spec.template.spec.containers[0].args}' | tr ',' '\n' | grep -i session
모양은 이렇다(모양은 observed).
"--session-store-type=redis"
"--redis-connection-url=redis://redis.keycloak-lab.svc:6379"
nginx 설정을 직접 보려던 시도는 계속 빈 결과였다. 호스트에서 무언가가 빈 결과를 주면 먼저 이것을 친다.
sudo -n true
실측은 이렇다(observed).
$ sudo -n true
sudo: a password is required
test-server 의 sudo 는 비밀번호를 요구한다. 게스트(kc-lab-1 과 kc-lab-2)는 무암호라 A층에서 conntrack 과 tc 를 문제없이 썼는데 호스트는 다르다. 앞선 「nginx 로그가 비어 있다」는 관측은 로그가 없던 것이 아니라 sudo 가 조용히 실패한 것이었다.
6. secret 의 키 이름과 길이를 본다
무엇을 보는가 — 회전 대상이 준비되어 있는지. 값은 찍지 않는다.
kubectl -n keycloak-lab get secret oauth2-proxy-secrets \
-o jsonpath='{.data}' | tr ',' '\n' | grep -o '"[A-Z_]*"'
모양은 이렇다(모양은 observed).
"CLIENT_SECRET"
"COOKIE_SECRET_A"
"COOKIE_SECRET_B"
길이도 본다. 원래 실행 기록에는 이 명령의 출력이 없어 미검증이었는데, 2026-09-17 에 쳐서 값이 생겼다(observed). 둘 다 32 다 — 프록시가 안 떠 있어도 Secret 만 읽으면 되므로 인증서 없이 칠 수 있다.
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
32
32
어디를 보나 — 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).
"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 에 들어갔는지, 쿠키가 티켓인지 본다
무엇을 보는가 — 한 번은 통째로 본 다음 접두사로 좁힌다.
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).
=== 세션이 Redis 에 들어갔는가 ===
b5:pvc
_oauth2_proxy-b26111fbd1fdab3ae2182e287001b02a
dbsize: 2
dbsize 는 2 인데 세션은 하나다. b5:pvc 는 B-5 가 남긴 키이고 이 실험과 무관하다. 이 Redis 는 이 실험 전용이 아니므로 dbsize 로 세션을 세면 틀린다.
kubectl -n keycloak-lab exec deploy/redis -- \
redis-cli --scan --pattern '_oauth2_proxy-*'
KEYS 대신 --scan 을 쓴다. KEYS 는 Redis 를 블로킹하고, 실험대에서는 티가 안 나지만 습관을 여기서 들인다.
이 값이 뜻하는 것 — 마지막으로 쿠키가 티켓인지 확인한다. 세션 저장소를 Redis 로 옮기면 쿠키에는 세션 전체가 아니라 티켓만 담긴다. 브라우저 개발자 도구에서 Application 또는 저장소 → Cookies → _oauth2_proxy 로 본다. 값은 지금 쓸 수 있는 세션 자격증명이라 모양과 길이만 적는다(observed).
_oauth2_proxy=<ticket>|<timestamp>|<mac>
└─ Redis 키를 여기서 계산한다
세션 전체가 아니라 티켓이다 (약 180자)
| 로 나뉜 세 토막과 전체 길이를 본다. 쿠키가 짧아졌고 그래서 502 가 사라졌다. Redis 키 이름은 이 티켓에서 계산되고, 관찰 절의 「지우지 못한다」가 그 관계에서 나온다.
주입
1. 겹칠 수 있는지부터 묻는다
무엇을 보는가 — 회전을 치기 전에, 애초에 두 secret 을 동시에 들 수 있는지.
kubectl -n keycloak-lab exec deploy/oauth2-proxy -- \
/bin/oauth2-proxy --help 2>&1 | grep cookie-secret
어디를 보나 — 실측은 이렇다(observed, 03-rotation.txt).
--cookie-secret string the seed string for secure cookies (optionally base64 encoded)
이 값이 뜻하는 것 — string 이고 복수형이 아니다. --cookie-secrets 도 --old-cookie-secret 도 목록에 없으므로 겹치는 구간을 만들 수단이 아예 없다. B-6 에서 Keycloak 이 두 키를 동시에 들 수 있었던 것은 토큰 헤더에 kid 가 있어서였고, oauth2-proxy 의 쿠키에는 그런 식별자가 없다.
식별자 있음 → 읽기는 여러 key, 쓰기는 하나 → 겹침 가능 (B-6)
식별자 없음 → 전부 한 번에 바뀐다 → 겹침 불가 (B-7)
겹칠 수 있는가에는 이 한 줄이 답했고, 남은 절차는 그래서 실제로 무슨 일이 나는지를 본다.
2. env 인덱스를 확인하고 secret 참조를 A 에서 B 로 바꾼다
목적 — Deployment 가 읽는 cookie secret 을 갈아치운다.
행동 — patch 가 지목하는 env/1 이 맞는지 먼저 보고, 시각을 남기고, 바꾼다.
kubectl -n keycloak-lab get deploy oauth2-proxy \
-o jsonpath='{.spec.template.spec.containers[0].env[*].name}'; echo
모양은 이렇다(모양은 observed).
OAUTH2_PROXY_CLIENT_SECRET OAUTH2_PROXY_COOKIE_SECRET
OAUTH2_PROXY_COOKIE_SECRET 이 몇 번째인지 0부터 센다. 위 모양에서는 두 번째이므로 env/1 이고, 순서가 다르면 patch 의 숫자를 고친다.
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).
=== ★ 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 의 참조가 실제로 바뀌었는가
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).
현재 secret 키: COOKIE_SECRET_B
바뀐 것은 Deployment 의 참조이지 Secret 의 내용이 아니다. 두 값 다 그대로 있고 어느 쪽을 읽을지만 바뀌었으며, 그래서 되돌리기가 한 줄이다.
2. Redis 는 그대로인가
kubectl -n keycloak-lab exec deploy/redis -- \
redis-cli --scan --pattern '_oauth2_proxy-*'
실측은 이렇다(observed, 03-rotation.txt).
Redis 세션은 그대로인가: 2 키
어디를 보나 — 나오는 줄의 개수를 주입 전 8 절 ②에서 본 것과 견준다. 위 실측의 「2 키」는 원래 실행이 적어 낸 숫자인데, 같은 실행이 8 절에서 접두사로 좁혀 본 세션은 _oauth2_proxy-b26111fbd1fdab3ae2182e287001b02a 하나였고 b5:pvc 를 더해야 둘이 된다. 원래 실행이 여기서 무엇을 셌는지는 기록에 없다(unknown). 따라 하는 사람 화면에는 _oauth2_proxy- 로 시작하는 줄이 8 절 ②와 같은 수만큼 나온다. 줄이 하나만 나와도 주입은 걸린 상태이고, 판정 기준은 숫자 2 가 아니라 회전 전과 같은 수인가다.
세션 수가 회전 전과 같다. 회전 자체는 아무 일도 일으키지 않으므로 여기서 「실험 실패」라고 결론 내리면 틀린다. 무슨 일이 나려면 누군가 옛 쿠키를 들고 와야 한다.
3. 파드가 실제로 새로 떴는가
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 가 없거나 만료됐으면 전원이 로그인 화면을 본다.
로그가 무슨 일이 났는지 말한다. 먼저 최근 로그를 통째로 본다.
kubectl -n keycloak-lab logs -l app=oauth2-proxy --since=3m --prefix
--prefix 는 각 줄 앞에 파드 이름을 붙여 준다. replica 가 둘이므로 이것이 없으면 누가 무엇을 했는지 못 가린다. --since=3m 은 최근 3분만 보므로 브라우저로 접속한 뒤 3분을 넘겨 치면 아무 줄도 안 나온다. 그때 나온 빈 결과는 「로그가 없다」가 아니라 창을 놓쳤다는 뜻이니, 브라우저를 한 번 더 열고 곧바로 친다. 그다음 좁힌다.
kubectl -n keycloak-lab logs -l app=oauth2-proxy --since=3m | grep -i stored_session
실측은 이렇다(observed, 03-rotation.txt).
[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).
[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 를 본다.
kubectl -n keycloak-lab exec deploy/redis -- \
redis-cli --scan --pattern '_oauth2_proxy-*'
실측은 이렇다(observed, 03-rotation.txt).
=== Redis 세션 수 (옛 세션이 남아 있는가) ===
_oauth2_proxy-978dfaefbdadccb96c7be1625dba5616
_oauth2_proxy-b26111fbd1fdab3ae2182e287001b02a
총: 2 개
키가 둘이다. 뒤엣것(b26111f…)은 회전 전의 세션이고 앞엣것은 방금 새로 생겼다. 사용자는 하나인데 서버 세션이 둘이다. 옛 것은 아무도 쓸 수 없고 프록시도 지우지 못한다.
못 지우는 까닭은 티켓과 키의 관계에 있다. Redis 세션 저장소를 쓰면 쿠키에는 티켓만 담기고, 티켓은 두 부분이다.
티켓 = <세션 ID>.<암호화 키>
│ └─ 값을 복호화할 키
└─ Redis 키 이름을 만든다 → _oauth2_proxy-<ID>
티켓 전체가 cookie secret 으로 봉인되어 있다. secret 을 바꾸면 티켓을 열 수 없고, 그러면 세션 ID 조차 못 읽는다. 프록시는 「이 세션은 못 쓴다」까지는 알지만 그 세션이 Redis 어디에 있는지를 모른다. 그래서 removing session 을 시도하고 실패한다.
secret 교체
└─ 옛 티켓을 못 푼다
├─ 사용자는 재로그인 (SSO 가 있으면 조용히)
└─ ★ 서버 세션은 TTL 만료까지 고아로 남는다
로그인한 사용자 수만큼 고아가 생긴다. 이 절차는 여기서 멈춘다. 정말 사라지는지, 운영자는 지울 수 있는지, 어느 것이 고아인지는 B-7a 가 이어서 잰다.
덤으로, 로그를 파드별로 갈라 보면 BFF 와 정반대인 성질이 보인다.
kubectl -n keycloak-lab logs -l app=oauth2-proxy --since=10m --prefix \
| grep -E 'Initiating login|AuthSuccess'
실측은 해설 문서에 이 모양으로 남아 있다(observed).
--- 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 로 되돌린다
목적 — 실험 전 상태로 돌린다.
행동 — 시각을 남기고 되돌린다.
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 세션도 담고 있다.
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 에 떠 두므로 그쪽을 본다.
wc -l ~/grafana-ingress-backup.yaml
grep -c 'app2.hyeonworks.com' ~/grafana-ingress-backup.yaml
kubectl -n keycloak-lab delete ingress oauth2-proxy
kubectl apply -f ~/grafana-ingress-backup.yaml
kubectl -n observability get ingress grafana
curl -sI --resolve app2.hyeonworks.com:443:192.168.122.10 https://app2.hyeonworks.com/ | head -3
원 가이드는 첫 줄을 kc-lab-1 에서 치라고 적었는데 그 기계에는 kubeconfig 가 없어 kubectl 이 막힌다(2026-09-17, observed). 두 줄 다 lab host 에서 친다 — curl 도 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 에 기반 가이드로 실험대를 새로 세우고 이 편을 어디까지 밟고 멈췄는지 적는다. 못 밟은 것을 밟은 것처럼 적지 않으려고 남긴다.
- 끝까지 밟았다(2026-09-17). TLS 를 세우고 위 클라이언트를 만든 뒤 전 구간을 밟았다. 결과는 문서 그대로다 — 로그인 뒤 쿠키가
_oauth2_proxy176자짜리 티켓이고 세션은 Redis 에_oauth2_proxy-<32자>로 들어갔으며, 업스트림이x-forwarded-user·x-forwarded-email·x-forwarded-preferred-username을 받았다. secret 을 A 에서 B 로 바꾸자 회전 전 200 이던 같은 쿠키가 302 로 바뀌어 로그인 시작점으로 되돌아갔다. 겹침 구간이 없다는 이 편의 결론이 그대로 성립한다. - 그리고 Redis 의 세션 키는 회전 뒤에도 그대로 남았다(observed). 가리키던 쿠키가 못 쓰게 됐을 뿐 값은 살아 있다 — B-7a 가 다루는 고아가 여기서 생긴다.
- 막는 것 —
https://auth.hyeonworks.com이 서지 않는다. 남은 것은 인증서 하나다(2026-09-17 기준). 같이 적어 두었던 다른 둘은 그날 해결됐다 — 밖에서 닿는 길은 호스트의 libvirtguest_input구멍과 유닛의ExecStartPost가 들어가면서 열렸고(http가 밖에서200), 클러스터 안에서 그 이름이 엣지를 안 가리키던 것은 기반 가이드 03 의 CoreDNS 한 단계로 놓았다. 인증서는 Cloudflare API 토큰이 필요한 DNS-01 로만 받을 수 있다 — 이름 셋이 tailnet 주소로 풀려 HTTP-01 은 성립하지 않는다. - 그때까지 이 편의 실측 가운데
(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 로 옮기는 쪽만 쟀다.