B-7a — cookie secret 회전이 남기는 고아 세션의 수명과 정리 ============================================================= 수집: 2026-09-04 11:29 ~ 11:34 UTC · Redis + oauth2-proxy 로그 + Playwright B-7 이 남긴 것 -------------- | 관찰 | | |---|---| | 옛 쿠키 | 검증 실패 — session ticket cookie failed validation | | 사용자 경험 | Keycloak SSO 가 살아 있어 조용히 재로그인 | | **서버 쪽 세션** | **★ 지우지 못했다** | `Error removing session: error decoding ticket to clear session` → **티켓을 못 푸니 Redis 키를 계산할 수 없고, 그래서 지울 수도 없다.** B-7 은 여기서 멈췄다. 남은 물음 셋을 잰다. (1) 고아의 TTL 은 실제로 줄어드는가 — 정말 사라지긴 하는가 (2) 운영자가 직접 지울 수 있는가 · 지우면 산 세션이 다치는가 (3) ★ 어느 키가 고아인지 구분할 수 있는가 [기준선] 회전 전 — 11:29:42 UTC ------------------------------------ secret = COOKIE_SECRET_A _oauth2_proxy-f6a9201fd534a047998278452001ccbf type=string ttl=3568초 크기=3510바이트 dbsize=1 설정: --cookie-expire=1h --session-store-type=redis 기동 로그: Cookie settings: name:_oauth2_proxy secure(https):true httponly:true expiry:1h0m0s ... refresh:disabled [주입] 1차 회전 A → B — 11:29:56 UTC -------------------------------------- kubectl patch deployment oauth2-proxy ... COOKIE_SECRET_B 회전 직후 Redis: 키 그대로 1개 (회전만으로는 아무 일도 안 일어난다) 브라우저가 접근한 순간(11:30:27) 로그: [stored_session.go:94] Error loading cookied session: session ticket cookie failed validation: , removing session [stored_session.go:97] Error removing session: error decoding ticket to clear session: session ticket cookie failed validation [oauthproxy.go:1024] No valid authentication in request. Initiating login. [AuthSuccess] Authenticated via OAuth2: Session{email:labuser@example.com ...} → B-7 의 관찰 그대로 재현. Keycloak SSO 가 살아 있어 로그인 화면 없이 통과했다. Redis: _oauth2_proxy-87faa1c94db3bd72c11c4e100c3ca593 ttl=3588 ← 새 세션 _oauth2_proxy-f6a9201fd534a047998278452001ccbf ttl=3511 ← ★ 고아 dbsize=2 [측정 1] ★ Redis 만 보고는 구분할 수 없다 ------------------------------------------- 키 type strlen ttl _oauth2_proxy-87faa1c9…(새) string 3510 3558 _oauth2_proxy-f6a9201f…(고아) string 3510 3480 · 이름 접두사가 같다 (_oauth2_proxy-) · 뒤는 불투명한 32자 hex — 사용자·시각·상태 어느 것도 안 담긴다 · 타입이 같다, 크기가 **바이트 단위로 같다** (3510) · 값은 암호화되어 있다 새 "\xcb\xb3h\xfa\x98\xedc\xe4@<\x9b\x83\xce\xc1\x18<…" 고아 "N\xf5\x0e=\xe1N\xfc|\xa2qE\xde\x1b\x82k\x88\x05…" md5 f9ad43cc6bbb2db4 / 9b31f7c4138e6472 (다르지만 뜻을 읽을 수 없다) → **다른 것은 TTL 뿐이다.** [측정 2] TTL 은 정직하게 줄어든다 — 그리고 갱신되지 않는다 ----------------------------------------------------------- 30초 간격 3회: t+00초 새=3557 고아=3479 t+30초 새=3526 고아=3448 t+60초 새=3494 고아=3417 1초에 1초씩. 고아는 **생성 후 정확히 1시간에 사라진다.** 요청을 보내도 늘지 않는다 (11:32:26, 11:32:49 두 번 요청 후): 살아있는 세션 ttl=3464 ← 계속 줄어든다 기동 로그의 `refresh:disabled` 와 일치한다. `--cookie-refresh` 가 없기 때문이다. ★ 이것이 다음 측정의 열쇠가 된다 — TTL 이 갱신되지 않으므로 TTL 은 **생성 시각의 정확한 함수**다. [측정 3] 운영자는 지울 수 있다 — 산 세션은 다치지 않는다 ---------------------------------------------------------- redis-cli del _oauth2_proxy-f6a9201f… → 반환 1 dbsize 2 → 1 남은 키: _oauth2_proxy-87faa1c9… 삭제 직후 브라우저 요청 (11:32:49): app2.hyeonworks.com GET - "/oauth2/userinfo" ... labuser@example.com 200 108 → **200. 산 세션은 영향이 없다.** oauth2-proxy 는 못 지우지만 **운영자는 지울 수 있다.** [측정 4] ★ 누적한다 — 회전할 때마다 ------------------------------------- 2차 회전 B → A — 11:33:27 UTC. 브라우저 재접근 후: 키 TTL 생성시각(추정) 판정 _oauth2_proxy-dad9c9fb… 3581 11:33:54 살아있음 _oauth2_proxy-87faa1c9… 3373 11:30:26 ★ 고아 dbsize=2 **1차 회전에서 살아남았던 세션이 2차 회전에서 고아가 됐다.** 회전 1회 = 그 시점 로그인 사용자 수만큼의 고아. [측정 5] ★ 그래서 정리 규칙을 유도할 수 있다 ---------------------------------------------- TTL 이 갱신되지 않으므로(측정 2): 생성시각 = 지금 - (cookie-expire - TTL) 이 값이 **회전 시각보다 이르면 그 키는 고아다.** 회전 이후에 만들어진 세션은 새 secret 으로 만들어졌으므로 반드시 유효하기 때문이다. 검증 — 추정 생성시각 11:30:26 vs 로그의 AuthSuccess 11:30:27. **1초 오차.** 추정이 아니라 사실상 정확하다. 실행: NOW=$(date -u +%s); ROT=<회전 시각 epoch> redis-cli --scan --pattern '_oauth2_proxy-*' | while read K; do T=$(redis-cli ttl "$K") C=$(( NOW - (3600 - T) )) [ $C -lt $ROT ] && redis-cli del "$K" done 실제 실행 결과: `삭제: _oauth2_proxy-87faa1c9…` · 남은 dbsize=1 산 세션은 남고 고아만 사라졌다. ════ 결론 ════ 1. **"지울 수 없다"는 oauth2-proxy 의 한계이지 Redis 의 한계가 아니다.** 프록시는 티켓을 못 풀어 키를 계산할 수 없다. 운영자는 키를 직접 안다. 2. **고아는 반드시 사라진다 — 생성 후 1시간.** TTL 이 갱신되지 않기 때문에 "쓰고 있으면 안 지워진다" 같은 일이 없다. 다만 그 1시간 동안은 남는다. 3. **어느 것이 고아인지는 Redis 값으로 알 수 없다.** 이름·타입·크기가 같고 값은 암호화되어 있다. **TTL 만이 신호다.** 4. **그 TTL 로 정리 규칙이 유도된다.** 회전 시각 이전에 생성된 키는 전부 고아다. 1초 오차로 정확히 골라낼 수 있고, 실제로 골라내 지웠다. 5. **전제가 하나 있다 — `--cookie-refresh` 를 켜면 이 규칙이 깨진다.** TTL 이 갱신되면 생성 시각을 역산할 수 없기 때문이다. 그때는 회전 후 `FLUSHDB` 로 전부 지우고 모두 재인증시키는 편이 오히려 정직하다.