# INC: postgres-prod stale-replica 승격 → SSO/Forgejo 전면 장애 RCA (2026-07-24)

<!-- live-verified: 2026-07-24 -->
작성: 세션 5e2b754b (bg, 읽기전용 RCA — 복구 실행은 aef6cec6 + postgres-prod-failover-restore lock 세션들이 claim)

## 0. 최초 증상 (사용자 보고)

mail.keiailab.com → sso.keiailab.com → `…/realms/keiailab/broker/google/endpoint?state=…&code=…&prompt=none` 에서 로그인 정지.
Google 리프는 code 를 정상 반환(구글 인증 성공) — 정지 지점은 **Keycloak 이 code 처리 시 DB 조회 불가**.

## 1. 근본원인 체인 (5-why)

1. mail 로그인 정지 ← Keycloak 이 브로커 콜백 처리 실패
2. ← Keycloak 양 파드 JDBC 전멸: `FATAL: password authentication failed for user "keycloak"` (01:18:26Z~)
   - 실체는 비밀번호 불일치가 아니라 **role 자체가 없는 서버에 접속** (PG 는 미존재 role 도 mock SCRAM 후 같은 에러 반환)
3. ← 01:16Z failover 로 **7일 묵은 stale replica(shard-0-1) 가 primary 승격** — 그 서버엔 keycloak(45h 전 생성)·forgejo(2d23h 전 생성) DB/role 이 아예 없음 (litellm/jari 등 구시대 DB 만 존재)
4. ← 진짜 최신 데이터를 가진 옛 primary(-0)의 PVC 가 **01:04:32Z 삭제·재생성**되고(동일 초에 200Gi PV 47개 생성되는 재생성 폭풍 동반), 새 PVC 는 stale -1 로부터 재시딩(timeline 2). `#220 failback guard` 가 "fenced(known-failed primary) 후보 승격 금지 + unfenced 서빙 멤버 유지" 로직으로 **stale 멤버를 보호하고 fresh 멤버 복귀를 차단**
5. ← 전제 조건 두 가지의 결합:
   - **복제가 2026-07-16 시딩 직후부터 조용히 죽어 있었음** — -1(7d17h)·-2(9d) 는 시딩 시점에 동결. `ShardsReady` 는 primary 만 추적, 복제 단절 알림 0 (silent gray failure)
   - e121 노드 2차 사망→재조인(오늘 ~22:40Z경 재조인, node age 3h) 여파의 자동 조치(펜스/재조정)가 fresh primary 를 "known-failed" 로 오분류

## 2. 영향 표면 (01:16Z ~ 진행 중)

| 서비스 | 상태 |
|---|---|
| sso.keiailab.com (Keycloak) | 로그인 전면 불가 (모든 SSO 연동 서비스 파급: mail/chat/git/…) |
| git.keiailab.com (Forgejo) | 500 — DB 소멸. Flux GitOps 소스도 정지 (기존 배포 상태는 유지) |
| harbor/samnix/worldrealdebt | 7~9일 전 데이터로 롤백된 상태로 서빙 (harbor health 404 — 최근 이미지 manifest 소실 → 신규 pull 실패 위험) |

## 3. 핵심 증거 (실측 인용)

- Keycloak keiailab-0: `01:18:18 … The connection attempt failed.` → `01:18:26 … FATAL: password authentication failed for user "keycloak"`
- 오퍼레이터: `01:16:25 skipping promotion: candidate PVC is fenced (known-failed primary) while an unfenced member is serving (#220 failback guard)` 스팸
- 서빙 primary(-1) 실측: `pg_database = postgres,template1,template0,litellm,jari,worldrealdebt,harbor,samnix` — keycloak/forgejo 부재, `pg_authid` 에 role 부재
- PVC: `data-postgres-prod-shard-0-0` AGE **36m** (01:04:57Z 재생성) vs -1 7d17h / -2 9d
- Released PV 48개 — claim 전부 `data/data-postgres-prod-shard-0-0`, 그중 47개 생성시각 **01:04:32Z 동일 초** (재생성 폭풍), 1개가 원본(2026-07-16 생성)
- 원본 PV: `pvc-667c5952-c4b8-4ba0-96df-3569667eb812` → RBD `replicapool/csi-vol-be58c1ce-6eee-4d4e-90bc-47da94c1970b` — **무결 잔존** (reclaimPolicy=Retain 이 구원)
- -0 재기동 로그: `starting backup recovery … on timeline ID 2` — stale 로부터 재시딩 확증

## 4. 복구 오케스트레이션 (진행 중, 본 세션 아님)

- claim: `2026-07-24T01:44:21Z session=aef6cec6 topic=pg-rescue-old-primary-extraction` — 원본 PV rescue mount + 골드 덤프 추출, "서빙측 쓰기는 dump-4 복원(형제) 완료 후 델타만"
- 로컬 lock: `postgres-prod-failover-restore.lock` (01:39Z) — 서빙측 dump-4 복원 형제 세션
- 라이브 확인: `pg-rescue`(골드 이미지 마운트, RBD watcher 활성)·`pg-dump-helper` 파드 가동, **keycloak·forgejo DB 서빙측 복귀 완료** (realm 7 / user 41 / google 연동 3 — dump-4 = 07-23 19:00Z 기준), 골드 델타(19:00Z→01:04Z) 적용 대기
- 예상 최종 손실창: 골드 덤프 적용 완료 시 **~0** (01:04Z 까지), 미적용 구간은 dump-4 기준 ~6.5h
- 본 세션(5e2b754b) 기여: RCA 확정 + 원본 이미지 보호 스냅샷 추가(`@pre-restore-20260724`, aef6cec6 의 `@pre-rescue-20260724` 에 2중) + 클러스터 변경 전면 양보

## 5. 사후 근본수리 백로그 (복구 완료 후 착수 — 별도 claim 필요)

1. **#220 failback guard 역전 버그**: "unfenced 서빙 멤버" 가 fresh 하다는 가정 제거 — 승격/유지 판단에 **데이터 신선도(LSN/최신 checkpoint 시각) 비교** 추가. fenced 후보가 unfenced 서빙자보다 수 일 앞서면 guard 를 뒤집어야 함 (PR#68, 2026-07-23 작업의 회귀)
2. **복제 단절 silent failure**: replica lag/단절을 상태 조건 + 알림으로 승격 (`pg_stat_replication` 부재 N분 = ShardsReady 강등 + gitops-mail-triage 신호). 7일간 무감지가 이번 사고의 필요조건이었음
3. **PVC 재생성 폭풍**: 01:04:32Z 동일 초 PV 47개 생성 — 재생성 루프에 백오프/가드 부재. 삭제 주체 로그 부재(오퍼레이터 로그에 없음) → 삭제 경로 audit 로깅 추가
4. **e121 하드웨어**: Zen4 무전조 즉사 3차 — 기존 인시던트 트랙 연장
5. **청소**: 고아 Released PV 47개 + RBD 이미지 (200Gi thin × 47) 정리, litellm/jari 구시대 DB 정리, 보호 스냅샷 2종 만료 처리, keycloak `passwordSecretResourceVersion` 캐시로 인한 ALTER 미재적용 여부 점검

## 6. 검증 명령 (재현/확인용)

```bash
kubectl --context keiailab exec -n data postgres-prod-shard-0-1 -c postgres -- psql -U postgres -tAc "SELECT string_agg(datname,',') FROM pg_database"
kubectl --context keiailab get pv --sort-by=.metadata.creationTimestamp | grep shard-0-0
kubectl --context keiailab exec -n rook-ceph deploy/rook-ceph-tools -- rbd snap ls replicapool/csi-vol-be58c1ce-6eee-4d4e-90bc-47da94c1970b
curl -sS -o /dev/null -w '%{http_code}\n' https://sso.keiailab.com/realms/keiailab/.well-known/openid-configuration
```
