들어가며
기업 환경에서 Keycloak을 도입할 때 가장 먼저 부딪히는 과제는 "이미 존재하는 사용자 디렉토리를 어떻게 할 것인가"입니다. 대부분의 조직은 수년에서 수십 년간 운영해 온 Active Directory(AD)나 OpenLDAP 같은 디렉토리 서비스를 보유하고 있고, 수천에서 수십만 명의 사용자 계정과 그룹, 비밀번호 정책이 그 안에 살아 있습니다.
2026년 현재 Keycloak은 26.6.x 버전(2026년 5월 기준 26.6.2)까지 발전하면서 passkeys 로그인 폼 통합, FAPI 2.0 Security Profile Final 지원, Workflows 기반 realm 관리 자동화 같은 현대적 기능을 갖추었지만, 엔터프라이즈 도입의 성패는 여전히 레거시 디렉토리와의 연동 품질에서 갈립니다. Zero Trust와 identity-first 보안이 기본값이 된 시대에도, 그 출발점은 "신뢰할 수 있는 단일 사용자 저장소"이기 때문입니다.
이 글에서는 Keycloak User Federation의 아키텍처부터 LDAP provider의 세부 설정, AD 특화 옵션, attribute/group 매핑, 대규모 디렉토리 성능 튜닝, 동기화 장애 트러블슈팅, 그리고 장기적으로 LDAP을 걷어내고 Keycloak 내장 스토리지로 이전하는 마이그레이션 전략까지 실무 관점에서 다룹니다.
User Federation 아키텍처 이해
기본 구조
Keycloak의 User Federation은 외부 사용자 저장소를 Keycloak의 사용자 모델에 "연결"하는 계층입니다. 핵심은 Keycloak이 외부 저장소를 대체하는 것이 아니라, User Storage SPI를 통해 위임(delegation)과 캐싱을 수행한다는 점입니다.
+-------------------+ +----------------------------+
| Application | | Keycloak |
| (OIDC / SAML) +------->+ +----------------------+ |
+-------------------+ | | Authentication Flow | |
| +----------+-----------+ |
| | |
| +----------v-----------+ |
| | User Cache (L1) | |
| +----------+-----------+ |
| | |
| +----------v-----------+ |
| | User Storage SPI | |
| +---+-------------+----+ |
| | | |
+------+-------------+-------+
| |
+---------v---+ +-----v--------+
| Local DB | | LDAP / AD |
| (federated | | Provider |
| link data) | +-----+--------+
+-------------+ |
+-----v--------+
| Directory |
| (AD/LDAP) |
+--------------+
동작 순서를 정리하면 다음과 같습니다.
1. 사용자가 로그인을 시도하면 Keycloak은 먼저 내부 캐시와 로컬 DB에서 사용자를 찾습니다.
2. 없으면 등록된 User Storage provider(LDAP provider)에 순서대로 질의합니다.
3. LDAP에서 사용자를 찾으면 Keycloak 로컬 DB에 "federated user" 엔트리를 생성합니다. 이 엔트리는 LDAP 엔트리에 대한 링크(원본 DN, provider ID)를 보관합니다.
4. 비밀번호 검증은 설정에 따라 LDAP bind로 위임되거나, Keycloak 내부에서 처리됩니다.
여기서 중요한 것은 **federated user는 LDAP의 미러가 아니라 링크**라는 점입니다. 어떤 속성을 로컬에 복사할지, 쓰기 작업을 어디로 보낼지는 모두 edit mode와 mapper 설정이 결정합니다.
import 모드와 no-import 모드
LDAP provider 설정의 `importEnabled` 옵션은 운영 특성을 크게 바꿉니다.
| 모드 | 동작 | 장점 | 단점 |
| --- | --- | --- | --- |
| import on (기본) | 사용자를 로컬 DB에 복사 후 링크 유지 | 검색/조회 빠름, 오프라인 토큰 안정적 | 동기화 필요, DB 용량 증가 |
| no-import | 매 요청마다 LDAP에서 직접 조회 | 동기화 불필요, 항상 최신 | LDAP 부하 증가, 일부 기능 제약 |
대규모 조직(5만 명 이상)에서는 import 모드 + 주기적 동기화 조합이 일반적입니다. no-import는 디렉토리가 빠르고 가까이 있으며, 사용자 수가 적고, "LDAP이 항상 진실의 원천"이어야 하는 경우에 적합합니다.
LDAP Provider 설정 상세
기본 연결 설정
Admin Console에서 User Federation 메뉴를 통해 설정할 수도 있지만, 재현 가능한 인프라를 위해 `kcadm.sh` CLI나 Terraform(keycloak provider)으로 코드화하는 것을 권장합니다.
LDAP provider 생성 (kcadm.sh)
./kcadm.sh create components -r myrealm \
-s name=corp-ldap \
-s providerId=ldap \
-s providerType=org.keycloak.storage.UserStorageProvider \
-s 'config.enabled=["true"]' \
-s 'config.priority=["0"]' \
-s 'config.editMode=["READ_ONLY"]' \
-s 'config.vendor=["ad"]' \
-s 'config.connectionUrl=["ldaps://ad01.corp.example.com:636"]' \
-s 'config.usersDn=["OU=Employees,DC=corp,DC=example,DC=com"]' \
-s 'config.bindDn=["CN=svc-keycloak,OU=ServiceAccounts,DC=corp,DC=example,DC=com"]' \
-s 'config.bindCredential=["CHANGE_ME"]' \
-s 'config.usernameLDAPAttribute=["sAMAccountName"]' \
-s 'config.rdnLDAPAttribute=["cn"]' \
-s 'config.uuidLDAPAttribute=["objectGUID"]' \
-s 'config.userObjectClasses=["person, organizationalPerson, user"]' \
-s 'config.searchScope=["2"]' \
-s 'config.useTruststoreSpi=["always"]' \
-s 'config.connectionPooling=["true"]' \
-s 'config.pagination=["true"]'
연결 관련 핵심 포인트는 다음과 같습니다.
- **반드시 LDAPS(636) 또는 StartTLS를 사용**합니다. 평문 LDAP(389)으로 bind 비밀번호가 네트워크를 오가는 구성은 감사에서 즉시 지적 대상입니다.
- `bindDn`에 사용하는 서비스 계정은 최소 권한(읽기 전용, 필요한 OU 한정)으로 만들고, 비밀번호는 Vault 같은 시크릿 매니저에서 주입합니다.
- AD의 `uuidLDAPAttribute`는 `objectGUID`, OpenLDAP은 `entryUUID`를 사용합니다. 이 값이 federated link의 불변 키가 되므로, 잘못 설정하면 사용자 중복 생성 사고로 이어집니다.
Edit Mode — READ_ONLY, WRITABLE, UNSYNCED
edit mode는 "쓰기 작업이 어디로 가는가"를 결정하는 가장 중요한 설정입니다.
| Edit Mode | 사용자 속성 수정 | 비밀번호 변경 | 적합한 시나리오 |
| --- | --- | --- | --- |
| READ_ONLY | 불가 (예외 발생) | 불가 | AD가 유일한 진실의 원천, HR 시스템이 AD를 관리 |
| WRITABLE | LDAP에 직접 기록 | LDAP에 기록 | Keycloak을 셀프서비스 포털로 사용 |
| UNSYNCED | Keycloak 로컬 DB에만 기록 | 로컬에만 기록 | LDAP은 읽기만 하고 점진적으로 독립 |
각 모드의 운영상 함의를 짚어보겠습니다.
**READ_ONLY**는 가장 안전한 기본값입니다. 사용자가 Keycloak Account Console에서 프로필을 수정하려 하면 에러가 발생하므로, Account Console의 해당 기능을 비활성화하거나 required action을 조정해야 합니다. 비밀번호 변경 요청은 거부되므로, "비밀번호 변경은 사내 포털에서 하세요" 같은 안내가 필요합니다.
**WRITABLE**은 Keycloak이 LDAP의 쓰기 클라이언트가 됩니다. 이때 bind 서비스 계정에 쓰기 권한이 필요해지고, AD라면 비밀번호 변경을 위해 LDAPS가 필수입니다(AD는 평문 채널로 unicodePwd 수정을 거부합니다). 또한 Keycloak의 required action(예: 최초 로그인 시 비밀번호 변경)이 실제 AD 비밀번호를 바꾸게 되므로, AD 쪽 비밀번호 정책과의 충돌을 반드시 검토해야 합니다.
**UNSYNCED**는 흥미로운 중간 지대입니다. LDAP에서 사용자를 읽어오되, 이후 변경 사항은 Keycloak 로컬에만 저장합니다. 사실상 "LDAP을 시드 데이터로 쓰는 점진적 마이그레이션 모드"이며, 뒤에서 다룰 마이그레이션 전략의 핵심 도구입니다. 단, 이 모드에서는 LDAP과 Keycloak의 데이터가 시간이 지날수록 분기(diverge)하므로, 어느 쪽이 진실인지에 대한 운영 정책을 명확히 해야 합니다.
동기화 전략 — Full Sync와 Changed Users Sync
임포트(import) 모드에서는 LDAP의 변경 사항을 Keycloak 로컬 DB에 반영하는 동기화가 필요합니다.
전체 동기화 트리거 (component ID는 생성 시 반환된 값)
./kcadm.sh create user-storage/COMPONENT_ID/sync?action=triggerFullSync -r myrealm
변경분 동기화 트리거
./kcadm.sh create user-storage/COMPONENT_ID/sync?action=triggerChangedUsersSync -r myrealm
주기 설정은 provider config에서 합니다.
./kcadm.sh update components/COMPONENT_ID -r myrealm \
-s 'config.fullSyncPeriod=["604800"]' \
-s 'config.changedSyncPeriod=["3600"]'
| 전략 | 주기 권장값 | 동작 | 주의점 |
| --- | --- | --- | --- |
| Full sync | 주 1회 (604800초) | 전체 사용자 재조회 및 갱신 | 대규모 디렉토리에서 수십 분 소요 가능 |
| Changed users sync | 1시간 (3600초) | 수정 타임스탬프 기반 증분 조회 | 삭제 감지 불가 |
changed sync는 LDAP의 `modifyTimestamp`(AD는 `whenChanged`) 속성을 기준으로 변경된 엔트리만 가져옵니다. 여기서 흔히 놓치는 함정이 두 가지 있습니다.
1. **삭제는 감지하지 못합니다.** LDAP에서 삭제된 사용자는 changed sync로는 사라지지 않습니다. full sync 시 "Periodic full sync should remove non-existent users" 동작이 적용되거나, 별도 정리 작업이 필요합니다. 퇴사자 계정이 Keycloak에 살아남는 보안 사고의 단골 원인이므로, full sync 주기와 함께 비활성화 정책(AD에서 계정 비활성화 시 Keycloak도 비활성)을 반드시 설계하세요.
2. **타임스탬프는 디렉토리 서버 기준**입니다. Keycloak 서버와 디렉토리 서버 간 시계가 어긋나면 변경분을 놓칠 수 있습니다. NTP 동기화를 확인하세요.
Active Directory 특화 설정
MSAD User Account Control Mapper
AD는 표준 LDAP과 달리 계정 상태를 `userAccountControl`이라는 비트 플래그 속성으로 관리합니다. Keycloak의 MSAD user account control mapper는 이 플래그를 해석해 Keycloak 사용자 상태와 연동합니다.
userAccountControl 주요 비트 플래그
---------------------------------------------
0x0002 ACCOUNTDISABLE 계정 비활성화
0x0010 LOCKOUT 계정 잠금
0x0020 PASSWD_NOTREQD 비밀번호 불필요
0x10000 DONT_EXPIRE_PASSWD 비밀번호 만료 없음
0x800000 PASSWORD_EXPIRED 비밀번호 만료됨
예) 512 (0x200) = 정상 계정
514 (0x202) = 비활성화된 정상 계정
66048 = 정상 + 비밀번호 만료 없음
이 mapper가 활성화되면 다음이 가능해집니다.
- AD에서 비활성화된 계정(ACCOUNTDISABLE)은 Keycloak에서도 로그인 불가 처리됩니다.
- AD의 PASSWORD_EXPIRED 상태가 감지되면 Keycloak이 UPDATE_PASSWORD required action을 트리거합니다(WRITABLE 모드에서).
- `pwdLastSet` 값이 0인 사용자(다음 로그인 시 비밀번호 변경 필요)도 비밀번호 갱신 흐름으로 유도됩니다.
또 하나, AD 연동에서 `userObjectClasses`에 `computer`가 섞이지 않도록 검색 필터를 조정하는 것이 좋습니다. AD의 user objectClass는 컴퓨터 계정에도 적용되기 때문입니다.
추가 사용자 LDAP 필터 예시 (custom user LDAP filter)
(&(objectCategory=person)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))
위 필터는 person 카테고리이면서 비활성화 비트가 켜지지 않은 계정만 가져옵니다. `1.2.840.113556.1.4.803`은 AD의 비트 AND 매칭 룰 OID입니다.
Kerberos / SPNEGO 통합
사내 Windows PC에 도메인 로그인한 사용자가 브라우저에서 Keycloak에 접근할 때 비밀번호 입력 없이 SSO 되게 하려면 Kerberos/SPNEGO 통합을 구성합니다.
먼저 AD에서 Keycloak용 서비스 계정에 SPN(Service Principal Name)을 등록하고 keytab을 발급합니다.
AD 도메인 컨트롤러에서 (관리자 권한)
setspn -A HTTP/sso.corp.example.com svc-keycloak-krb
ktpass -princ HTTP/sso.corp.example.com@CORP.EXAMPLE.COM \
-mapuser CORP\svc-keycloak-krb \
-crypto AES256-SHA1 -ptype KRB5_NT_PRINCIPAL \
-pass SERVICE_ACCOUNT_PASSWORD \
-out keycloak.keytab
Keycloak 서버 측에는 krb5.conf를 배치하고 LDAP provider의 Kerberos 통합 옵션을 켭니다.
/etc/krb5.conf
[libdefaults]
default_realm = CORP.EXAMPLE.COM
dns_lookup_kdc = true
forwardable = true
[realms]
CORP.EXAMPLE.COM = {
kdc = ad01.corp.example.com
admin_server = ad01.corp.example.com
}
./kcadm.sh update components/COMPONENT_ID -r myrealm \
-s 'config.allowKerberosAuthentication=["true"]' \
-s 'config.kerberosRealm=["CORP.EXAMPLE.COM"]' \
-s 'config.serverPrincipal=["HTTP/sso.corp.example.com@CORP.EXAMPLE.COM"]' \
-s 'config.keyTab=["/opt/keycloak/conf/keycloak.keytab"]' \
-s 'config.useKerberosForPasswordAuthentication=["false"]'
마지막으로 브라우저 인증 플로우에서 Kerberos execution을 ALTERNATIVE 또는 REQUIRED로 활성화합니다. 운영 팁 몇 가지를 덧붙입니다.
- SPNEGO 협상이 실패하면 표준 로그인 폼으로 자연스럽게 폴백되도록 ALTERNATIVE로 두는 것이 안전합니다.
- 브라우저 측에서도 Intranet zone 설정(Windows GPO) 또는 Firefox의 negotiate URI 화이트리스트 설정이 필요합니다.
- keytab은 파일 권한 600으로 보호하고, 컨테이너 환경에서는 Secret 마운트로 주입합니다.
- `useKerberosForPasswordAuthentication`을 true로 하면 로그인 폼의 비밀번호 검증도 LDAP bind 대신 Kerberos로 수행합니다. KDC 부하와 잠금 정책 동작이 달라지므로 일반적으로는 false를 권장합니다.
Attribute Mapper 구성
LDAP 속성과 Keycloak 사용자 속성/모델을 잇는 것이 mapper입니다. provider 생성 시 vendor에 맞는 기본 mapper(username, email, first name, last name 등)가 자동 생성되고, 필요에 따라 추가합니다.
| Mapper 유형 | 용도 | 예시 |
| --- | --- | --- |
| user-attribute-ldap-mapper | LDAP 속성을 사용자 속성으로 | mobile, department, employeeNumber |
| full-name-ldap-mapper | cn 또는 displayName을 성+이름으로 분해/결합 | AD displayName |
| hardcoded-attribute-mapper | 고정값 주입 | source=ldap |
| group-ldap-mapper | LDAP 그룹을 Keycloak 그룹으로 | 아래 절 참고 |
| role-ldap-mapper | LDAP 그룹/엔트리를 role로 | 레거시 role 체계 |
| msad-user-account-control-mapper | AD 계정 상태 연동 | 위 절 참고 |
부서 정보를 사용자 속성으로 가져오는 예시입니다.
./kcadm.sh create components -r myrealm \
-s name=department-mapper \
-s providerId=user-attribute-ldap-mapper \
-s providerType=org.keycloak.storage.ldap.mappers.LDAPStorageMapper \
-s parentId=COMPONENT_ID \
-s 'config."user.model.attribute"=["department"]' \
-s 'config."ldap.attribute"=["department"]' \
-s 'config."read.only"=["true"]' \
-s 'config."always.read.value.from.ldap"=["true"]' \
-s 'config."is.mandatory.in.ldap"=["false"]'
이렇게 가져온 속성은 client scope의 protocol mapper를 통해 토큰 클레임으로 노출할 수 있습니다. 즉 "AD의 department 속성 → Keycloak user attribute → JWT claim"이라는 파이프라인이 완성됩니다. 주의할 점은 다음과 같습니다.
- `always.read.value.from.ldap`을 true로 하면 조회 시마다 LDAP을 읽으므로 항상 최신이지만 부하가 늘어납니다. 변경이 잦지 않은 속성은 false + 동기화에 맡기는 편이 낫습니다.
- READ_ONLY edit mode에서는 mapper도 `read.only=true`로 맞춰야 일관성이 유지됩니다.
- 바이너리 속성(objectGUID, 사진 등)은 `is.binary.attribute` 옵션을 켜야 합니다.
그룹과 Role 매핑
group-ldap-mapper
AD 보안 그룹을 Keycloak 그룹 트리로 가져오는 설정입니다.
./kcadm.sh create components -r myrealm \
-s name=ad-groups \
-s providerId=group-ldap-mapper \
-s providerType=org.keycloak.storage.ldap.mappers.LDAPStorageMapper \
-s parentId=COMPONENT_ID \
-s 'config."groups.dn"=["OU=Groups,DC=corp,DC=example,DC=com"]' \
-s 'config."group.name.ldap.attribute"=["cn"]' \
-s 'config."group.object.classes"=["group"]' \
-s 'config."membership.ldap.attribute"=["member"]' \
-s 'config."membership.attribute.type"=["DN"]' \
-s 'config."membership.user.ldap.attribute"=["sAMAccountName"]' \
-s 'config."mode"=["READ_ONLY"]' \
-s 'config."user.roles.retrieve.strategy"=["LOAD_GROUPS_BY_MEMBER_ATTRIBUTE"]' \
-s 'config."preserve.group.inheritance"=["true"]' \
-s 'config."drop.non.existing.groups.during.sync"=["false"]'
핵심 옵션을 해설하면 다음과 같습니다.
- `user.roles.retrieve.strategy`는 멤버십 조회 방식입니다. AD라면 `GET_GROUPS_FROM_USER_MEMBEROF_ATTRIBUTE`(사용자의 memberOf 속성 활용)가 효율적이고, 중첩 그룹까지 펼치려면 `LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY`(AD의 matching rule in chain 활용)를 씁니다. 다만 recursive 전략은 AD에 무거운 쿼리를 던지므로 대규모 환경에서는 성능 검증이 필수입니다.
- `preserve.group.inheritance`를 true로 하면 LDAP의 그룹 계층이 Keycloak 그룹 트리로 재현됩니다. 평탄한(flat) 그룹 구조라면 false로 두는 것이 단순합니다.
- `drop.non.existing.groups.during.sync`는 LDAP에서 사라진 그룹을 동기화 시 제거할지 결정합니다. true로 하면 깔끔하지만, 해당 그룹에 걸어둔 role 매핑이나 권한도 함께 사라지므로 신중해야 합니다.
role 매핑 전략
그룹을 가져온 후 권한 부여는 두 가지 패턴이 있습니다.
1. **그룹에 realm/client role을 부여**: Keycloak 그룹 "AD-App-Admins"에 client role `app-admin`을 매핑. AD 그룹 멤버십이 곧 애플리케이션 권한이 됩니다. 가장 일반적이고 권장되는 패턴입니다.
2. **role-ldap-mapper로 직접 role 생성**: LDAP 그룹을 Keycloak role로 직접 변환. 그룹 트리가 필요 없고 role만 있으면 되는 단순한 경우에 적합하지만, 그룹과 role이 혼재되면 관리가 어려워지므로 한 가지 패턴으로 통일하는 것을 권장합니다.
토큰에 그룹 정보를 넣을 때는 client scope에 group membership protocol mapper를 추가하고, full path 여부(슬래시 구분 계층 경로)를 애플리케이션과 합의해 두어야 파싱 사고를 막을 수 있습니다.
비밀번호 정책 충돌 문제
WRITABLE 모드에서 가장 자주 터지는 문제가 비밀번호 정책 충돌입니다. Keycloak realm에도 password policy가 있고, AD에도 도메인 비밀번호 정책(복잡도, 최소 길이, 이력)이 있기 때문입니다.
사용자 → Keycloak 비밀번호 변경 폼
|
| (1) Keycloak realm policy 검사 — 통과
v
LDAP modify (unicodePwd)
|
| (2) AD 도메인 정책 검사 — 실패!
v
LDAPException: WILL_NOT_PERFORM (error code 53)
Keycloak 정책은 통과했는데 AD가 거부하면 사용자에게는 모호한 에러만 보입니다. 실무 가이드라인은 다음과 같습니다.
- **Keycloak realm policy를 AD 정책의 상위 집합(더 엄격하게)으로 설정**합니다. 예를 들어 AD가 "최소 8자, 복잡도 켜짐, 이력 24개"라면 Keycloak은 "최소 12자, 대소문자/숫자/특수문자 각 1개 이상, 이력 24개"로 맞춥니다. Keycloak에서 먼저 걸러지면 사용자는 명확한 정책 안내 메시지를 받습니다.
- AD의 **비밀번호 최소 사용 기간(minimum password age)** 정책이 있으면, 변경 직후 재변경이 거부됩니다. 헬프데스크 리셋 흐름과 충돌하지 않는지 확인하세요.
- AD 비밀번호 변경은 **LDAPS 필수**입니다. 평문이나 StartTLS가 아닌 LDAPS 636 연결이 아니면 unicodePwd 수정이 거부됩니다.
- error code 53(WILL_NOT_PERFORM)이 로그에 보이면 대부분 정책 위반 또는 비보안 채널 문제입니다. error code 19(CONSTRAINT_VIOLATION)는 이력/최소 기간 위반인 경우가 많습니다.
또한 AD의 계정 잠금(lockout) 정책과 Keycloak의 brute force detection이 이중으로 동작하면 사용자 경험이 혼란스러워집니다. 일반적으로는 "잠금은 AD에 위임, Keycloak brute force는 보조적 알림 용도"로 역할을 나누는 것이 깔끔합니다.
대규모 디렉토리 성능 튜닝
사용자 10만 명 이상의 디렉토리를 연동할 때 고려할 항목입니다.
페이지네이션과 검색 범위
./kcadm.sh update components/COMPONENT_ID -r myrealm \
-s 'config.pagination=["true"]' \
-s 'config.batchSizeForSync=["1000"]'
- `pagination`을 켜면 LDAP Simple Paged Results 컨트롤을 사용해 대량 결과를 나눠 받습니다. AD는 기본적으로 한 번에 1000개(MaxPageSize)까지만 반환하므로, 페이지네이션 없이 full sync를 돌리면 1000명 이후 사용자가 누락되는 사고가 납니다.
- `usersDn`은 가능한 한 좁은 OU로 한정하고, 광범위한 베이스에서 SUBTREE 검색을 해야 한다면 custom LDAP filter로 대상을 줄입니다.
- AD 글로벌 카탈로그(3268/3269 포트)를 사용하면 멀티 도메인 포리스트에서 검색이 빨라지지만, 글로벌 카탈로그에 복제되지 않는 속성은 조회되지 않으므로 mapper 대상 속성이 partial attribute set에 포함되는지 확인해야 합니다.
커넥션 풀과 타임아웃
./kcadm.sh update components/COMPONENT_ID -r myrealm \
-s 'config.connectionPooling=["true"]' \
-s 'config.connectionTimeout=["5000"]' \
-s 'config.readTimeout=["10000"]'
- 커넥션 풀링은 반드시 켭니다. bind마다 TCP+TLS 핸드셰이크를 새로 하면 로그인 지연이 수백 ms 단위로 늘어납니다.
- 타임아웃을 설정하지 않으면 디렉토리 장애 시 Keycloak 워커 스레드가 무한 대기하며 전체 로그인이 마비됩니다. connection 5초, read 10초 정도가 합리적인 출발점입니다.
캐시 정책
User Storage provider에는 캐시 정책을 설정할 수 있습니다.
| 정책 | 동작 | 적합한 경우 |
| --- | --- | --- |
| DEFAULT | 표준 user cache 사용 | 대부분의 경우 |
| EVICT_DAILY | 매일 지정 시각에 캐시 무효화 | 야간 배치로 디렉토리가 갱신되는 환경 |
| EVICT_WEEKLY | 매주 지정 요일/시각에 무효화 | 변경이 드문 환경 |
| MAX_LIFESPAN | 지정 시간(ms) 후 무효화 | 최신성 요구가 명확한 환경 |
| NO_CACHE | 캐시 비사용 | 디버깅, 극단적 최신성 요구 |
캐시 덕분에 LDAP 조회가 줄지만, "AD에서 비활성화했는데 Keycloak에서 아직 로그인된다"는 상황의 원인이 되기도 합니다. 보안 민감 환경이라면 MAX_LIFESPAN을 짧게(예: 5분) 잡고 LDAP 부하를 모니터링하면서 조정하세요. 캐시 무효화는 Admin REST API의 clear-user-cache 엔드포인트로 수동 실행할 수도 있습니다.
동기화 장애 트러블슈팅
운영 중 만나는 대표적인 장애 패턴과 대응을 정리합니다.
진단 첫걸음 — 로그 레벨
Keycloak 기동 옵션에 LDAP 카테고리 디버그 로그 추가
bin/kc.sh start \
--log-level=INFO,org.keycloak.storage.ldap:DEBUG \
--spi-connections-http-client-default-connection-pool-size=128
증상별 체크리스트
| 증상 | 유력 원인 | 확인 방법 |
| --- | --- | --- |
| 로그인 간헐적 실패 | 커넥션 풀 고갈, DC 한 대 장애 | LDAP 서버별 응답 시간, 풀 설정 |
| full sync에서 1000명만 동기화 | 페이지네이션 꺼짐 | pagination 설정, AD MaxPageSize |
| 사용자 중복 생성 | uuidLDAPAttribute 변경/오설정 | objectGUID 매핑 확인 |
| 퇴사자가 로그인 가능 | changed sync만 동작, 캐시 잔존 | full sync 주기, 캐시 정책 |
| 비밀번호 변경 실패 (code 53) | 평문 채널, AD 정책 위반 | LDAPS 여부, 도메인 정책 |
| 동기화가 끝나지 않음 | 거대 OU 전체 스캔, 인덱스 없는 필터 | custom filter, AD 인덱스 속성 |
| TLS 핸드셰이크 실패 | truststore에 사내 CA 미등록 | useTruststoreSpi, truststore 내용 |
ldapsearch로 Keycloak 바깥에서 재현하기
문제가 Keycloak 설정인지 디렉토리 자체인지 가르는 가장 빠른 방법은 동일 조건의 ldapsearch입니다.
Keycloak이 수행하는 것과 동일한 검색을 수동 재현
ldapsearch -H ldaps://ad01.corp.example.com:636 \
-D "CN=svc-keycloak,OU=ServiceAccounts,DC=corp,DC=example,DC=com" \
-W \
-b "OU=Employees,DC=corp,DC=example,DC=com" \
-s sub \
-E pr=1000/noprompt \
"(&(objectCategory=person)(sAMAccountName=jdoe))" \
sAMAccountName mail userAccountControl whenChanged
이 명령이 빠르게 결과를 반환하면 문제는 Keycloak 쪽 설정이고, 여기서부터 느리면 디렉토리/네트워크 문제입니다. AD 쪽 비인덱스 속성으로 필터링하면 전체 스캔이 발생하므로, 필터에 쓰는 속성(sAMAccountName, mail 등)이 인덱싱되어 있는지 AD 관리자와 확인하세요.
고가용성 구성
connectionUrl에는 공백으로 구분해 여러 서버를 지정할 수 있습니다.
ldaps://ad01.corp.example.com:636 ldaps://ad02.corp.example.com:636
다만 단순 페일오버이므로, 실무에서는 DNS 라운드로빈이나 LDAP 프록시(예: 도메인 DNS의 DC 로케이터) 뒤에 두고 헬스체크를 인프라 계층에서 처리하는 편이 견고합니다.
하이브리드 시나리오 — AD 직원 + 소셜 외부 사용자
현실의 서비스는 "직원은 AD로, 외부 파트너나 고객은 소셜/이메일 가입으로"라는 혼합 요구가 많습니다. Keycloak에서는 하나의 realm 안에서 다음을 조합할 수 있습니다.
+---------------------------+
| Realm: company |
| |
Employees --------->| User Federation (LDAP/AD) |
(corp laptop, | |
Kerberos SSO) | Identity Providers |
Partners ---------->| - Google / GitHub |
Customers --------->| - Apple / Kakao |
| |
| Local users (self-reg) |
+---------------------------+
설계 포인트는 다음과 같습니다.
- **계정 충돌 처리**: 직원이 회사 이메일로 Google 로그인을 시도하면, 동일 이메일의 federated user와 충돌합니다. First Broker Login 플로우의 계정 연결(link) 단계를 어떻게 처리할지 — 자동 연결 금지 + 기존 계정 인증 요구 — 를 명시적으로 설계해야 합니다. 자동 연결을 허용하면 이메일 소유 검증이 없는 IdP를 통한 계정 탈취 경로가 생깁니다.
- **realm 분리 고려**: 직원과 고객의 보안 요구(MFA 정책, 세션 수명, 비밀번호 정책)가 크게 다르면 realm을 분리하는 것이 운영상 단순합니다. 단일 앱에서 두 realm을 받으려면 앱이 multi-issuer를 처리하거나 중간에 broker realm을 둡니다.
- **속성 기반 구분**: 단일 realm로 간다면 hardcoded attribute mapper로 federated user에 `source=ldap` 같은 속성을 박아두고, 토큰 클레임으로 노출해 애플리케이션이 사용자 출처를 구분할 수 있게 합니다.
- 2026년 트렌드 관점에서는 직원 쪽에 passkeys(Keycloak 26.6의 로그인 폼 통합 conditional UI)를 점진 도입하면서, AD 비밀번호 의존도를 낮추는 전략이 유효합니다.
마이그레이션 전략 — LDAP에서 Keycloak 내장 스토리지로
LDAP 연동은 종착지가 아니라 과도기인 경우가 많습니다. 디렉토리 의존을 끊고 Keycloak(또는 그 뒤의 DB)을 진실의 원천으로 만드는 마이그레이션 시나리오를 정리합니다.
단계별 전략
Phase 1 Phase 2 Phase 3 Phase 4
READ_ONLY 연동 → UNSYNCED 전환 → 비밀번호 점진 획득 → LDAP 제거
(현상 유지) (쓰기 독립) (로그인 시 로컬 (federation
해시 저장) link 해제)
1. **Phase 1 — READ_ONLY로 안정화**: import 모드 + full/changed sync로 모든 사용자가 Keycloak 로컬 DB에 federated entry로 존재하게 만듭니다. 애플리케이션들의 인증을 모두 Keycloak으로 모읍니다.
2. **Phase 2 — UNSYNCED 전환**: 프로필 변경이 로컬에만 기록되기 시작합니다. 이 시점부터 HR 연동 등 사용자 라이프사이클 이벤트를 Keycloak Admin API(또는 SCIM 브리지)로 직접 받도록 전환합니다.
3. **Phase 3 — 비밀번호 획득**: 가장 까다로운 부분입니다. LDAP 비밀번호 해시는 보통 추출할 수 없으므로(AD는 불가능), 사용자가 로그인할 때 검증에 성공한 비밀번호를 Keycloak이 로컬 해시(기본 argon2)로 저장하게 합니다. UNSYNCED 모드에서 비밀번호 변경이 로컬에 저장되는 동작을 활용하거나, 일정 기간 후 전체 사용자에게 비밀번호 재설정/passkey 등록을 요구하는 방식을 병행합니다. "passkey 등록 캠페인"을 이 단계에 끼워 넣으면 마이그레이션과 passwordless 전환을 한 번에 달성할 수 있습니다.
4. **Phase 4 — 링크 해제**: 모든(또는 임계치 이상의) 사용자가 로컬 크리덴셜을 갖게 되면 LDAP provider를 제거합니다. provider를 제거하면 federated link가 끊기는데, import된 사용자 엔트리는 남습니다. 사전에 스테이징 realm에서 provider 제거 후 사용자 상태(크리덴셜 보유 여부, required action)를 반드시 리허설하세요.
마이그레이션 체크리스트
- 로그인 통계로 **휴면 사용자**를 식별하고, 마이그레이션 대상에서 제외하거나 별도 재활성화 절차를 둡니다.
- 그룹/role 매핑이 LDAP mapper에 의존하고 있다면, 마이그레이션 전에 Keycloak 네이티브 그룹으로 복제하고 권한 매핑을 옮깁니다.
- 애플리케이션이 LDAP 유래 속성(employeeNumber 등)을 클레임으로 쓰고 있다면, 해당 속성의 새 공급 경로(HR API 등)를 먼저 확보합니다.
- 롤백 계획: Phase 2~3 동안 LDAP과 로컬 데이터가 분기되므로, 롤백 시 어떤 데이터를 버릴지 기준을 문서화합니다.
- Keycloak 26.x의 조직(Organizations) 기능과 Workflows를 활용하면 마이그레이션 후의 사용자 라이프사이클 자동화(온보딩/오프보딩)를 Keycloak 안에서 구성할 수 있습니다.
운영 베스트 프랙티스 요약
- LDAPS + truststore 검증을 기본으로, bind 계정은 최소 권한 + 시크릿 매니저 관리.
- edit mode는 의도를 명확히: 진실의 원천이 AD라면 READ_ONLY, 마이그레이션 중이라면 UNSYNCED.
- full sync 주 1회 + changed sync 1시간을 출발점으로, 삭제/비활성화 전파를 반드시 검증.
- 페이지네이션 활성화, 커넥션 풀/타임아웃 설정은 필수.
- 비밀번호 정책은 Keycloak 쪽을 더 엄격하게 맞춰 사용자에게 명확한 에러를 보여줄 것.
- AD 잠금 정책과 Keycloak brute force detection의 역할 분담을 정의할 것.
- 디렉토리 장애 시나리오(타임아웃, 폴백, 캐시 수명)를 카오스 테스트로 검증할 것.
- 마이그레이션은 READ_ONLY → UNSYNCED → 크리덴셜 획득 → 링크 해제의 단계로, 각 단계마다 롤백 기준을 문서화할 것.
마치며
User Federation은 Keycloak 기능 중 가장 "오래된" 축에 속하지만, 엔터프라이즈 현장에서는 여전히 도입 성패를 가르는 핵심입니다. edit mode와 동기화 전략이라는 두 축을 정확히 이해하면 대부분의 설계 결정이 명확해지고, 페이지네이션/캐시/타임아웃이라는 세 가지 운영 설정을 챙기면 대규모 환경에서도 안정적으로 굴러갑니다.
장기적으로는 LDAP 연동을 영구 상태가 아닌 과도기로 보고, passkeys와 현대적 사용자 라이프사이클 관리(SCIM, Workflows)로 나아가는 로드맵을 함께 그리는 것을 권합니다. 다음 글에서는 Keycloak Authorization Services를 활용한 세밀한 권한 제어를 다루겠습니다.
참고 자료
- [Keycloak Server Administration Guide — User Federation](https://www.keycloak.org/docs/latest/server_admin/index.html#_user-storage-federation)
- [Keycloak Documentation](https://www.keycloak.org/documentation)
- [Keycloak 26.6.0 Release Notes](https://www.keycloak.org/docs/latest/release_notes/index.html)
- [Keycloak User Storage SPI — Server Developer Guide](https://www.keycloak.org/docs/latest/server_development/index.html#_user-storage-spi)
- [RFC 4511 — LDAP: The Protocol](https://datatracker.ietf.org/doc/html/rfc4511)
- [RFC 2696 — LDAP Control Extension for Simple Paged Results](https://datatracker.ietf.org/doc/html/rfc2696)
- [RFC 4178 — SPNEGO: The Simple and Protected GSS-API Negotiation Mechanism](https://datatracker.ietf.org/doc/html/rfc4178)
- [Microsoft Learn — UserAccountControl property flags](https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/useraccountcontrol-manipulate-account-properties)
- [Microsoft Learn — Active Directory Schema](https://learn.microsoft.com/en-us/windows/win32/adschema/active-directory-schema)
- [RFC 7644 — SCIM Protocol](https://datatracker.ietf.org/doc/html/rfc7644)
- [W3C WebAuthn Level 3](https://www.w3.org/TR/webauthn-3/)
- [FIDO Alliance — Passkeys](https://fidoalliance.org/passkeys/)
현재 단락 (1/303)
기업 환경에서 Keycloak을 도입할 때 가장 먼저 부딪히는 과제는 "이미 존재하는 사용자 디렉토리를 어떻게 할 것인가"입니다. 대부분의 조직은 수년에서 수십 년간 운영해 온 A...