B-7 stopped at "could not delete the server-side session". The reason it gave was right: the ticket carries the session id, the ticket is encrypted with the cookie secret, so after a rotation the proxy cannot work out which Redis key to remove. But that is a limitation of the proxy, not of Redis. Measured across two rotations: - The orphan does expire. TTL falls one second per second and is not refreshed by requests (the startup log says refresh:disabled, and --cookie-refresh is unset), so it dies exactly one hour after creation. - An operator can delete it. `redis-cli del` returned 1, dbsize went 2 to 1, and the live session answered /oauth2/userinfo with 200 immediately after. - But nothing in Redis says which key is the orphan. Same name prefix, same type, the same 3510 bytes, and the values are encrypted. - TTL is the only signal, and because it is never refreshed it is an exact function of creation time. Anything created before the rotation is an orphan. Derived creation time 11:30:26 against the AuthSuccess log line at 11:30:27 — one second out. The rule was then run and removed the orphan while leaving the live session. - They accumulate: the session that survived the first rotation became the orphan of the second. The caveat is recorded too: turning on --cookie-refresh breaks the derivation, and at that point flushing and forcing everyone to re-authenticate is the more honest option. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
148 lines
6.8 KiB
Plaintext
148 lines
6.8 KiB
Plaintext
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: <nil>, 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` 로 전부 지우고 모두 재인증시키는 편이 오히려 정직하다.
|