17 KiB
Identity Redis Mirror 정책
관련 이슈: #1039
결정
사용자 identity에 대해 Backend DB 복제본이나 claim output을 SSOT 일치 보장 대상으로 취급하지 않습니다.
Baron SSO의 identity 원장은 Ory Kratos입니다. Redis는 Kratos identity를 빠르게 조회하기 위한 mirror/cache 계층이고, Backend DB는 Ory에 저장되지 않거나 Ory API로 필요한 방식의 조회가 불가능한 데이터의 read model로만 사용합니다.
Ory에서 Redis cache로 웜업된 identity/조직 데이터는 frontend나 외부 API가 직접 소비하지 않습니다. Backend가 Redis와 허용된 read model을 조합해 cursor 기반 API로 제공합니다.
역할 분리
| 구성요소 | 역할 | 보장 |
|---|---|---|
Kratos identities |
identity SSOT | 인증 주체, credentials, recovery/verifiable address의 원장 |
| Redis identity mirror | cache/read mirror | 빠른 목록/검색/단건 조회. stale 가능 |
| Backend DB read model | Ory 보완 read model | Ory에 저장되지 않거나 조회 불가능한 업무/운영 데이터 |
Backend DB read model의 visible count를 Kratos identity total로 표시하지 않습니다. 화면과 API에서는 identity mirror count와 허용된 read model count를 분리해서 보여야 합니다.
현재 필드 대조
현재 코드 기준으로 Kratos traits와 backend DB users는 일부 필드를 중복 보관합니다. Redis mirror 전환 이후에는 Kratos traits를 인증/기본 identity 필드 중심으로 줄이고, Baron 업무/조직/연동 정보는 Ory에 저장되지 않거나 조회가 불가능한 경우에만 Backend read model로 유지합니다.
Kratos에 유지할 identity 필드
| 필드 | 현재 저장 위치 | 유지 이유 |
|---|---|---|
id |
Kratos identity ID, backend users.id 참조 |
인증 subject. WORKS externalKey 기준 |
email |
Kratos traits, backend users.email |
로그인 ID, recovery, verification |
phone_number |
Kratos traits, backend users.phone |
SMS 로그인/검증 식별자 |
custom_login_ids |
Kratos traits, backend user_login_ids |
password identifier. backend는 중복/정책 검증용 index |
name |
Kratos traits, backend users.name |
기본 프로필 표시 |
role |
Kratos traits, backend users.role |
세션/profile claim 계산에 필요. 최종 권한은 Keto/ReBAC와 함께 검증 |
state |
Kratos identity state, backend users.status로 일부 투영 |
identity 활성 상태의 원본 |
현재 Kratos에도 있지만 backend-only로 축소해야 하는 필드
| 필드 | 현재 코드 사용 | 전환 방향 |
|---|---|---|
tenant_id |
대표 테넌트, profile, local user sync | Keto relation과 Backend read model 기준으로 이동. Kratos에는 identity 원장 필드로 추가하지 않음 |
department |
사용자 표시, 조직도, WORKS 비교 | backend users.department 또는 tenant membership metadata 기준 |
grade |
직급 표시, role fallback legacy | backend users.grade. role fallback 용도 제거 |
position |
직책 표시 | backend users.position |
jobTitle |
직무 표시 | backend users.job_title |
affiliationType |
내부/외부/게스트 구분 | backend users.affiliation_type |
relying_party_id |
RP admin profile 보조 | backend/RP relation 기준 |
additionalAppointments |
다중 소속 표시/WORKS 연동 | Keto relation과 Backend read model 기준 |
sub_email, aliasEmails, secondary_emails, worksmobileAliasEmails |
WORKS alias 및 보조 이메일 | backend users.metadata 또는 명시 테이블 기준 |
| tenant UUID namespaced metadata | tenant별 custom schema 값 | backend users.metadata 또는 전용 custom-field storage 기준 |
Backend read model로만 허용하는 정보
| 데이터 | 저장 위치 | Ory SSOT와의 관계 |
|---|---|---|
| soft-delete 상태 | users.deleted_at |
Ory Kratos identity 삭제/비활성화와 의미가 다른 Baron 운영 상태 |
| Baron 사용자 상태 세부값 | users.status |
WORKS provision/deprovision, org visible 정책과 결합된 운영 데이터 |
| WORKS mapping/outbox/job 상태 | worksmobile_* 테이블 |
외부 SaaS 연동 상태이며 identity 원장이 아님 |
| Keto outbox 및 relation sync 상태 | keto_outboxes, Keto |
권한/관계 원장은 Keto이고 DB는 처리 상태 read model |
| RP metadata/consent/usage | rp_user_metadata, client_consents, usage tables |
Ory에 저장되지 않거나 client 단위 조회가 불가능한 RP 업무 데이터 |
| tenant tree 표시/검색 metadata | tenants, relation/outbox |
관계 판단은 Keto, 표시/검색/slug 조회는 Backend read model |
| custom field schema 및 tenant별 값 | tenant config, users.metadata, related tables |
Ory에 schema/검색 정책을 저장하거나 조회할 수 없는 tenant별 운영 데이터 |
user_login_ids row metadata |
user_login_ids |
Kratos는 identifier 값 원장, 발급 tenant/field key는 Backend 검증용 read model |
| audit/session activity read model | audit/clickhouse/local tables | 감사/운영 분석 데이터 |
정리하면, Kratos에는 “로그인과 subject 확인에 필요한 최소 identity”만 남깁니다. 조직도/WORKS/RP/Keto/감사/tenant custom schema에 필요한 데이터도 Ory에 저장되거나 조회 가능한 경우에는 Ory를 기준으로 하고, 그렇지 않은 영역만 Backend read model을 허용합니다.
일관성 모델
Redis mirror는 strong consistency 원장이 아닙니다.
다만 Baron backend를 통해 발생한 Kratos write에 대해서는 다음 수준을 목표로 합니다.
- Kratos create/update/delete 성공
- 성공한 identity ID를 기준으로 Kratos
GetIdentity(id)재조회 - Redis
identity:mirror:{id}write-through - Redis index/state 갱신
- Redis 갱신 실패 시 mirror state를
failed또는stale로 표시
Kratos Admin API를 backend 밖에서 직접 수정하는 경로는 운영 정책상 금지합니다. 금지할 수 없는 환경에서는 Redis mirror가 stale해질 수 있음을 인정하고, 주기 refresh와 drift report로만 복구합니다.
Kratos write 경로 감사
2026-06-09 기준으로 admin/identities, CreateUser, UpdateIdentity, DeleteIdentity, UpdateUserPassword, Kratos DB identities 직접 변경 경로를 정적 검색했습니다.
중앙 Kratos client 구현
| 파일 | 역할 | 판정 |
|---|---|---|
backend/internal/service/identity_write_service.go |
Kratos identity 변경의 중앙 write boundary. 성공/실패 후 Redis mirror 상태를 갱신 또는 stale 표시 | 허용. 신규 identity write는 이 서비스를 거쳐야 함 |
backend/internal/service/kratos_admin_service.go |
Kratos Admin API list/get/create/update/delete/password/session client | 허용. 이후 IdentityWriteService의 하위 client로만 사용 |
backend/internal/service/ory_service.go |
legacy IDP provider. create/password/verifiable address 변경 시 Kratos Admin API 호출 | 허용하되 write-through 책임은 상위 IdentityWriteService로 이동 |
정상 backend API 경로
아래 경로는 사용자 요청이 backend를 통과하지만, Redis mirror write-through 구현 시 모두 같은 중앙 write service를 지나야 합니다.
| 경로 | 파일 | Kratos 변경 | 판정 |
|---|---|---|---|
| admin 사용자 단건 생성 | backend/internal/handler/user_handler.go |
OryProvider.CreateUser |
허용. 생성 성공 후 Kratos 재조회와 Redis mirror write-through 필요 |
| admin 사용자 bulk 생성 | backend/internal/handler/user_handler.go |
OryProvider.CreateUser |
허용. 부분 성공/실패별 mirror 갱신 필요 |
| admin 사용자 수정 | backend/internal/handler/user_handler.go |
KratosAdmin.UpdateIdentity, 선택적 OryProvider.UpdateUserPassword |
허용. password 변경도 identity write audit에 포함 |
| admin 사용자 삭제/bulk 삭제 | backend/internal/handler/user_handler.go |
KratosAdmin.DeleteIdentity |
허용. Redis mirror delete 또는 tombstone 갱신 필요 |
| 일반 회원가입 | backend/internal/handler/auth_handler.go |
IdpProvider.CreateUser |
허용이지만 Backend read model sync가 goroutine 기반이라 write-through 기준에서는 약함 |
| 내 프로필 수정 | backend/internal/handler/auth_handler.go |
KratosAdmin.UpdateIdentity |
직접 PUT /admin/identities/{id} 호출 제거 완료. 향후 IdentityWriteService write-through 대상 |
| 비밀번호 재설정/내 비밀번호 변경 | backend/internal/handler/auth_handler.go |
IdpProvider.UpdateUserPassword |
허용. traits mirror와 별도 audit event 필요 |
| 조직 그룹 멤버 추가 | backend/internal/service/user_group_service.go |
Kratos write 없음 | Kratos tenant_id, department write 제거 완료. 조직/부서 정보는 backend DB/Keto/WORKS 기준 |
backend 내부이지만 일반 API가 아닌 경로
| 경로 | 파일 | Kratos 변경 | 판정 |
|---|---|---|---|
| super-admin 보장 CLI | backend/cmd/adminctl/main.go, backend/internal/bootstrap/admin_account.go |
CreateUser, UpdateIdentityPassword |
운영 bootstrap/정비 경로. 실행 후 Redis mirror 갱신 또는 refresh 필수 |
| 초기 admin seed | backend/internal/bootstrap/kratos_seed.go |
IdpProvider.CreateUser |
startup bootstrap 경로. 신규 환경에서만 허용하고 반복 실행 영향 점검 필요 |
| role 보정 CLI | backend/cmd/fix_kratos_roles.go |
ListIdentities 후 UpdateIdentity |
기본 dry-run. 실제 변경은 --dry-run=false --maintenance-window --mark-mirror-stale 없이는 거부 |
| WORKS 기준 Baron 보정 CLI | backend/cmd/adminctl/worksmobile_sync.go |
IdentityWriteService.UpdateIdentity |
중앙 write boundary 강제. 변경 후 Redis mirror stale 표시 |
| RP custom claim traits sync | backend/internal/handler/dev_handler.go |
IdentityWriteService.UpdateIdentity |
중앙 write boundary 강제. RP read model과 Kratos traits 동기화 잔여 경로는 Ory SSOT 전환 대상 |
backend와 Kratos Admin API를 모두 우회하는 경로
| 경로 | 파일/문서 | 변경 대상 | 판정 |
|---|---|---|---|
| orphan tenant membership shell 정리 | scripts/clear_orphan_tenant_memberships.sh |
ory_kratos.identities.traits 직접 UPDATE |
CONFIRM_KRATOS_DB_MAINTENANCE=baron-sso와 MARK_IDENTITY_MIRROR_STALE=true 없이는 거부 |
| tenant maintenance 문서의 직접 SQL 절차 | docs/tenant-maintenance-procedures.md |
Baron DB 및 Kratos traits | 문서화된 우회 절차. Kratos DB 직접 UPDATE 절차는 폐기 또는 강한 경고 필요 |
| backup restore | scripts/backup/restore.sh |
ory_kratos 전체 restore 가능 |
DR 경로로만 허용. restore 이후 Redis mirror full rebuild와 Baron DB/Kratos drift report 필수 |
| Docker network 직접 접근 | compose, docker, mcp 설정 |
http://kratos:4434 접근 가능 컨테이너 |
public publish는 아니지만 같은 network의 정비 컨테이너가 admin API를 칠 수 있음. 접근 주체 제한 필요 |
조치 원칙
- Kratos identity write는
IdentityWriteService하나로 모으고, 성공한 create/update/delete/password 변경이 audit와 Redis mirror write-through를 남기게 합니다. auth_handler.updateKratosIdentity처럼KRATOS_ADMIN_URL을 직접 읽어admin/identities를 호출하는 코드는 금지합니다.backend/cmd/fix_kratos_roles.go와 Kratos DB 직접 UPDATE 스크립트는--dry-run,--maintenance-window,--mark-mirror-stale같은 명시적 가드 없이는 실행하지 못하게 합니다.- shell/SQL로 Kratos DB를 직접 수정한 경우에는 Backend read model이나 Redis mirror를 신뢰하지 않고, Kratos full refresh와 drift report를 먼저 실행합니다.
- CI에 정적 정책 테스트를 추가해
admin/identitieswrite 호출과UPDATE identitiesSQL이 허용 파일 밖에 생기면 실패시킵니다.
Redis 키 설계
초기 구현 기준:
identity:mirror:{identityID}- Kratos identity summary JSON
- 단건 조회 cache
identity:mirror:statestatus:ready,refreshing,stale,failedlastRefreshedAtlastErrorobservedCount
identity:index:active- active identity ID 목록
- 구현은 Redis Set 또는 Sorted Set으로 둡니다.
기존 domain.RedisRepository가 문자열 KV만 제공하므로 목록 index를 제대로 구현하려면 Redis repository 인터페이스를 확장해야 합니다.
API 원칙
사용자 목록 API는 다음 값을 구분해야 합니다.
identityTotal: Redis mirror 기준 Kratos identity 수localUserTotal: PostgreSQLusers기준 Baron 로컬 사용자 수mirrorStatus: Redis mirror 상태items: identity mirror와 허용된 Backend read model을 조합한 응답
Redis cache miss 발생 시:
- 단건 조회는 Kratos
GetIdentity(id)로 fallback합니다. - fallback 성공 시 Redis mirror를 갱신합니다.
- fallback 실패 시 SSOT 조회 실패로 응답합니다.
목록 조회는 Redis mirror가 ready가 아니면 경고 상태를 포함해야 합니다. Backend read model을 대체 SSOT처럼 사용하지 않습니다.
Front 전송과 cursor 보장
front/API로 전달되는 사용자 목록은 Backend가 제공하는 cursor 기반을 원칙으로 합니다. offset은 하위 호환 파라미터로만 유지하고, 신규 화면 또는 대량 조회 화면은 cursor 외 방식을 사용하지 않습니다.
API 계약
GET /api/v1/admin/users 응답은 다음 형태를 유지해야 합니다.
{
"items": [],
"limit": 50,
"cursor": "현재 요청 cursor 또는 빈 값",
"nextCursor": "다음 페이지 cursor 또는 빈 값",
"identityTotal": 2106,
"localUserTotal": 2106,
"mirrorStatus": "ready"
}
전환 기간에는 기존 total을 유지할 수 있지만 의미를 명확히 해야 합니다.
identityTotal: Redis identity mirror 기준 Kratos identity 수localUserTotal: PostgreSQLusers기준 Baron business local record 수total: deprecated. 화면에서 신규 의미로 사용하지 않습니다.
cursor 불변 조건
nextCursor가 있으면 front는 반드시 다음 요청에cursor=nextCursor를 사용합니다.- cursor 요청에서는
offset을 의미 있게 사용하지 않습니다. - backend는 cursor 요청에서 안정 정렬 키를 사용해야 합니다. 기본 정렬은
created_at DESC, id DESC또는 mirror index score +id같이 중복 없는 조합이어야 합니다. - cursor는 필터 조건(
search,tenantSlug, status filter 등)과 묶여야 합니다. 다른 필터에 재사용하면 400으로 거부할 수 있습니다. - 대량 화면도
limit=5000&offset=0단일 호출을 사용하지 않습니다.
현재 front 점검 결과
| 화면/모듈 | 현재 상태 | 조치 |
|---|---|---|
adminfront 사용자 목록 |
useInfiniteQuery로 nextCursor 사용 |
유지 |
adminfront 일부 tenant/user group modal |
fetchUsers(20/100/1000, 0) 단일 호출 |
cursor helper로 전환 |
adminfront bulk upload modal |
fetchUsers(10000, 0) 단일 호출 |
금지. cursor 수집 helper 또는 서버 검증 API로 전환 |
orgfront 조직도 |
Redis orgchart snapshot 기반 | 유지. Backend가 Ory/Redis/read model을 조합해 제공 |
orgfront 조직 picker |
Redis orgchart snapshot 기반으로 전환 | 유지. 인증 picker는 fetchOrgChartSnapshot, public picker는 token 기반 orgchart API 사용 |
orgfront/src/lib/adminApi.ts |
UserListResponse에 nextCursor 없음 |
타입 계약 보완 |
공통 helper 원칙:
fetchUsersPage(params)는 단일 페이지 조회만 담당합니다.fetchAllUsersByCursor(params)는nextCursor가 없어질 때까지 반복합니다.- front에서 arbitrary large limit로 전체 데이터를 가져오는 코드를 금지합니다.
- E2E 또는 unit test에서
fetchAllUsersByCursor가nextCursor를 끝까지 따라가는지 검증합니다.
Refresh 정책
주기 refresh는 Redis mirror를 재구성하는 best-effort 작업입니다.
Kratos list pagination을 끝까지 순회하는 것은 refresh의 필요조건일 뿐입니다. 호출 도중 Kratos identity 변경이 끼어들 수 있으므로 snapshot isolation 보장으로 해석하지 않습니다.
refresh 중 불일치 또는 실패가 발생하면:
- Redis mirror state를
failed또는stale로 표시합니다. - PostgreSQL
users를 자동 삭제하지 않습니다. - drift report를 남겨 운영자가 확인할 수 있게 합니다.
금지 사항
- Kratos partial list를 full snapshot으로 간주하지 않습니다.
- Backend read model을 Kratos identity total의 원장으로 사용하지 않습니다.
- Redis mirror refresh 실패를 숨기고
ready로 표시하지 않습니다. - 외부 도구가 Kratos Admin API를 직접 수정하도록 허용하지 않습니다.
전환 작업
- legacy user sync 명칭과 API를
identity_mirror성격으로 분리합니다. - Redis repository에 Set/Sorted Set 또는 scan 가능한 index 연산을 추가합니다.
- Kratos create/update/delete 성공 직후 Redis write-through 테스트를 추가합니다.
- admin 사용자 목록 응답에서 identity count와 local user count를 분리합니다.
- 기존 Backend DB count를 identity count처럼 사용하는 화면과 WORKS 비교 경로를 점검합니다.
- Kratos identity 변경은
IdentityWriteService경유를 강제하고, 직접KratosAdmin.UpdateIdentity경로를 정책 테스트로 차단합니다.