1
0
forked from baron/baron-sso
Files
baron-sso/docs/identity-redis-mirror-policy-2026-06-09.md

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에 대해서는 다음 수준을 목표로 합니다.

  1. Kratos create/update/delete 성공
  2. 성공한 identity ID를 기준으로 Kratos GetIdentity(id) 재조회
  3. Redis identity:mirror:{id} write-through
  4. Redis index/state 갱신
  5. 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 ListIdentitiesUpdateIdentity 기본 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-ssoMARK_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/identities write 호출과 UPDATE identities SQL이 허용 파일 밖에 생기면 실패시킵니다.

Redis 키 설계

초기 구현 기준:

  • identity:mirror:{identityID}
    • Kratos identity summary JSON
    • 단건 조회 cache
  • identity:mirror:state
    • status: ready, refreshing, stale, failed
    • lastRefreshedAt
    • lastError
    • observedCount
  • identity:index:active
    • active identity ID 목록
    • 구현은 Redis Set 또는 Sorted Set으로 둡니다.

기존 domain.RedisRepository가 문자열 KV만 제공하므로 목록 index를 제대로 구현하려면 Redis repository 인터페이스를 확장해야 합니다.

API 원칙

사용자 목록 API는 다음 값을 구분해야 합니다.

  • identityTotal: Redis mirror 기준 Kratos identity 수
  • localUserTotal: PostgreSQL users 기준 Baron 로컬 사용자 수
  • mirrorStatus: Redis mirror 상태
  • items: identity mirror와 허용된 Backend read model을 조합한 응답

Redis cache miss 발생 시:

  1. 단건 조회는 Kratos GetIdentity(id)로 fallback합니다.
  2. fallback 성공 시 Redis mirror를 갱신합니다.
  3. 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: PostgreSQL users 기준 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 사용자 목록 useInfiniteQuerynextCursor 사용 유지
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 UserListResponsenextCursor 없음 타입 계약 보완

공통 helper 원칙:

  • fetchUsersPage(params)는 단일 페이지 조회만 담당합니다.
  • fetchAllUsersByCursor(params)nextCursor가 없어질 때까지 반복합니다.
  • front에서 arbitrary large limit로 전체 데이터를 가져오는 코드를 금지합니다.
  • E2E 또는 unit test에서 fetchAllUsersByCursornextCursor를 끝까지 따라가는지 검증합니다.

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를 직접 수정하도록 허용하지 않습니다.

전환 작업

  1. legacy user sync 명칭과 API를 identity_mirror 성격으로 분리합니다.
  2. Redis repository에 Set/Sorted Set 또는 scan 가능한 index 연산을 추가합니다.
  3. Kratos create/update/delete 성공 직후 Redis write-through 테스트를 추가합니다.
  4. admin 사용자 목록 응답에서 identity count와 local user count를 분리합니다.
  5. 기존 Backend DB count를 identity count처럼 사용하는 화면과 WORKS 비교 경로를 점검합니다.
  6. Kratos identity 변경은 IdentityWriteService 경유를 강제하고, 직접 KratosAdmin.UpdateIdentity 경로를 정책 테스트로 차단합니다.