📌 개요
LDAP이란?
* 참고자료: https://www.okta.com/identity-101/what-is-ldap/
LDAP(Lightweight Directory Access Protocol)이란, 디렉터리 서비스를 위한 프로토콜이다.
일반적으로 회사에서 부서 및 사용자를 관리하기 위해 사용하며, 사용자/그룹/권한 등의 정보를 저장 및 조회 할 수 있다.
dc=example,dc=org (회사)
├── ou=Development (개발부서)
│ ├── cn=developer1
│ └── cn=developer2
├── ou=Sales (영업부서)
│ ├── cn=sales1
│ └── cn=sales2
└── ou=HR (인사부서)
├── cn=hr1
└── cn=hr2
dc, ou, cn 등 각 객체들에 대해 간단히 설명하자면 아래와 같다.
- dc (Domain Component): 도메인 구성요소
- ou (Organizational Unit): 조직 단위/부서
- cn (Common Name): 일반적인 이름
- uid (User ID): 사용자 식별자
- gid (Group ID): 그룹 식별자
설치 후 사용자를 생성할때 각 객체를 만나게 될 예정이니 약간은 익숙해지도록 하자!
LDAP과 Keycloak의 Federate (연동)
LDAP은 그룹 및 사용자 정보를 저장하고 관리하는 시스템이다.
LDAP의 사용자/그룹을 ketcloak하고 연동(federate)해서 여러 어플리케이션에 SSO(single-sing-on)할 수 있도록 구성할 수 있다.
- LDAP: 사용자 및 그룹의 중앙 저장소 역할
- Keycloak: 인증/인가 처리 및 SSO 제공 역할
바로 테스트 해보자 😁
📌 테스트 환경 구성
1. minikube 실행
helm으로 쉽게 설치할 수 있기때문에, 로컬에 minikube를 설치한다.
minikube는 각 환경마다 설치 매뉴얼을 아주 자세히 쉽고 간단하게 설명해 놓았으니 링크를 따라서 설치하면 된다.
minikube start
kubectl 명령어가 없다면 여기 링크를 따라서 설치한다.
2. Keycloak 실행
가장 많이 사용하는 bitnami keycloak helm chart를 이용한다.
아래 명령어로 설치한다.
helm install keycloak oci://registry-1.docker.io/bitnamicharts/keycloak \
--set auth.adminUser=admin \
--set auth.adminPassword=admin \
--set postgresql.auth.username=bn_keycloak \
--set postgresql.auth.password=password \
--set postgresql.auth.database=keycloak \
--set postgresql.auth.postgresPassword=password
설치가 완료 되었다면 포트 포워딩을 통해 로컬 브라우저에서 접근 가능하도록 설정한다.
# Keycloak 서비스로 포트포워딩
kubectl port-forward svc/keycloak 8082:80 &
브라우저로 localhost:8082 접속해서 로그인 한다.
3. OpenLDAP 실행
* 참고링크: https://artifacthub.io/packages/helm/helm-openldap/openldap
LDAP을 제공하는 여러 솔루션이 있는데, 그 중 대표적인게 Windows Active Directory이고,
오픈소스로는 OpenLDAP이 있다. 아래 명렁어 대로 간단하게 OpenLDAP을 설치해보자.
git clone https://github.com/jp-gouin/helm-openldap.git
cd helm-openldap
helm install openldap .
설치가 완료 되었다면 포트 포워딩을 통해 로컬 브라우저에서 접근 가능하도록 설정한다.
openLDAP webadmin에 접속하려면 비밀번호를 알아야 하기떄문에 비밀번호 확인 명령어로 비밀번호를 확인한다.
# openldap admin으로 포트 포워딩
kubectl port-forward svc/openldap-phpldapadmin 8083:80 &
# 비밀번호 확인 명령어
kubectl get secret --namespace default openldap -o jsonpath="{.data.LDAP_ADMIN_PASSWORD}" | base64 --decode; echo
kubectl get secret --namespace default openldap -o jsonpath="{.data.LDAP_CONFIG_ADMIN_PASSWORD}" | base64 --decode; echo
브라우저로 localhost:8083 접속해서 로그인 한다.
📌 LDAP 간단하게 설정하기
위 개요에서 언급한것과 같이 LDAP은 조직 (사용자/그룹) 중앙 저장소 역할이다.
OpenLDAP에 접속하여 기본적인 객체들을 생성해보자.
1. Group 생성
그룹(Groups)은 사용자를 묶는 사용자를 묶는 단위이며, 권한과 정책을 관리하는데 사용한다.
admin, read-only처럼 권한을 분리하고 정책을 적용하는 단위가 된다.
ou=groups에 공통으로 사용할 그룹을 3개 생성한다.
- administrator
- editor
- readonlyuser
2. OU 생성 (Sales/Marketing 조직)
ou는 트리 형태로 만들 수 있다. 일반적으로는 조직의 부서/팀 형태로 많이 구성한다.
# OU구성 예시
dc=example,dc=org
├── ou=headquarters
│ ├── ou=development
│ │ ├── ou=web-team
│ │ └── ou=mobile-team
│ ├── ou=marketing
│ │ ├── ou=digital
│ │ └── ou=branding
│ └── ou=sales
│ │ ├── ou=domestic
│ │ └── ou=international
├── ou=branch-busan-office
│ └── ou=sales
│ │ ├── ou=retail
│ │ └── ou=wholesale
└── ou=groups
├── ou=admin-groups
└── ou=user-groups
우리는 테스트이므로 간단하게 2개의 ou를 생성한다.
- sales
- marketing
3. User 생성
이제 조직에 속할 사용자를 만들어보자. sales, marketing ou에 사용자를 만들고, 위에서 생성한 Group에 한명씩 매핑한다.
각 OU에 3명씩 총 6명의 사용자가 생성 완료 되었다면, 다음단계로 넘어가보자!
📌 Keycloak LDAP Federate
Realm 단위로 LDAP 연동이 가능하다.
먼저, my-LDAP이라는 Realm을 생성하자.
1. LDAP Realm 생성
2. Keycloak LDAP 연동
생성한 my-LDAP realm에서 LDAP연동을 진행한다.
왼쪽 하단 [User Federation] -> [Add Ldap providers]를 클릭한다.
이제 각 항목을 살펴보면서 설정해보자. 설정해야할 부분은 빨간 네모로 표시해두었고,
설명은 각 캡쳐 하위에 달아놓았다.
- Vendor: Other (openldap 이므로 other 선택)
- Connection URL: ldap://openldap.default:389 (LDAP 서버 주소)
- Bind DN(Distinguished Name): cn=admin,dc=example,dc=org (관리자 계정)
- Bind Credential: (관리자 비밀번호)
여기서 말하는 DN은 아래와 같은 형태로 사용된다.
cn=lee.leader,ou=marketing,ou=headquarters,dc=example,dc=org
└─────────┴──────────────┴───────────────┴─────────┴────────
│ │ │ │ │
│ │ │ │ └─ 최상위 도메인
│ │ │ └─ 도메인
│ │ └─ 본사
│ └─ 마케팅 부서
└─ 사용자 이름
- Edit mode (WRITABLE): LDAP 서버에 대한 접근 모드 설정 (WRITABLE/READ_ONLY/UNSYNCED)
- Users DN: 사용자들이 저장된 기본 DN 위치
- Username LDAP attribute: 사용자 이름으로 사용될 LDAP 속성 (cn (Common Name)이 일반적으로 사용됨)
- RDN LDAP attribute: 엔트리의 고유 식별자로 사용할 속성
- UUID LDAP attribute: 사용자의 고유 식별자로 사용되는 속성 (objectGUID는 전역적으로 고유한 식별자)
- User object classes: LDAP 사용자 객체의 클래스 정의 (person, organizationalPerson, user는 표준 사용자 객체 클래스
- User LDAP filter: 사용자 검색 시 적용할 필터 (특정 조건으로 사용자를 필터링할 때 사용)
- Search scope (Subtree): LDAP 검색 범위 설정 (Subtree: 하위 모든 레벨 검색/OneLevel: 직계 하위 레벨만 검색)
- Read timeout: LDAP 검색 작업의 제한 시간 설정
- Pagination: 대량의 검색 결과를 페이지 단위로 가져올지 설정 (메모리 사용 효율화를 위해 ON 권장)
- Referral: LDAP 리퍼럴(다른 LDAP 서버로의 참조) 처리 방식 설정
- Synchronization settings (동기화 설정)
- Import users: LDAP의 사용자를 Keycloak으로 가져올지 여부를 설정 (ON/OFF)
- Sync Registrations: Keycloak에서 생성된 사용자를 LDAP에 동기화할지 여부를 설정 (ON/OFF)
- Batch size: 한 번에 동기화할 사용자의 수를 지정 (성능과 메모리 사용량 조절)
- Periodic full sync: 전체 사용자 데이터를 주기적으로 동기화할지 설정 (ON/OFF)
- Periodic changed users sync: 변경된 사용자만 주기적으로 동기화할지 설정 (ON/OFF)
- Kerberos integration (Kerberos 통합)
- Allow Kerberos authentication: Kerberos 인증 허용 여부 설정 (ON/OFF)
- Use Kerberos for password authentication: 비밀번호 인증에 Kerberos 사용할지 여부 설정 (ON/OFF)
* 참고: Kerberos는 네트워크 인증 프로토콜로, 싱글 사인온(SSO)을 구현하는 데 사용되는 보안 시스템이다.
- Cache settings (캐시 설정)
- Cache policy: LDAP 조회 결과를 캐시하는 정책 설정 (DEFAULT/EVICT_DAILY/EVICT_WEEKLY/MAX_LIFESPAN/NO_CACHE)
- Advanced settings (고급 설정)
- Enable the LDAPv3 password modify extended operation:
LDAPv3의 확장된 비밀번호 수정 작업 활성화 여부 (ON/OFF) - Validate password policy: LDAP 서버의 비밀번호 정책 검증 활성화 여부 (ON/OFF)
- Trust Email: LDAP에서 가져온 이메일 주소를 신뢰할지 여부 설정 (ON이면 이메일 검증 절차 생략) (ON/OFF)
- Connection trace: LDAP 연결 디버깅을 위한 상세 로그 활성화 여부 (ON/OFF)
- Query Supported Extensions: LDAP 서버가 지원하는 확장 기능 조회 버튼
- Enable the LDAPv3 password modify extended operation:
이제 LDAP 연동이 완료 되었다.
Users 탭으로 이동해서 사용자가 보이는지 점검해보자.
사용자가 잘 보인다.
여기까지만 하면 실제 환경에서 어떻게 쓰이는지 이해하기 쉽지 않으니, 샘플 어플리케이션으로 테스트를 해보자.
📌 어플리케이션 추가 (Ldap 사용자로 Keycloak SSO로 Grafana 로그인하기)
1. Grafana 설치
* 설치링크: https://grafana.com/docs/grafana/latest/setup-grafana/installation/helm/
널리 사용하는 Grafana를 설치하고, 사용자 로그인까지 테스트 해보자.
# helm 저장소 등록 및 업데이트
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update
# helm 다운받고 압축풀기
helm fetch grafana/grafana
tar -zxvf grafana-8.10.1.tgz
# 설치하기
cd grafana
helm install grafana .
설치했다면 역시 포트 포워딩을 통해 로컬 브라우저에서 접속 가능하도록 하자.
# Grafana 서비스로 포트포워딩
kubectl port-forward svc/grafana 8084:80 &
# Grafana admin 비밀번호 확인
kubectl get secret --namespace default grafana -o jsonpath="{.data.admin-password}" | base64 --decode ; echo
브라우저로 localhost:8084 접속해서 로그인 한다.
2. Keycloak LDAP Mapper 설정
위 Keycloak 단계에서 사용자 정보를 가져오는것은 확인했지만, 잘 보면 사용자에 그룹정보가 빠져있다.
LDAP의 Group정보를 가져오기위해서는 추가적인 Mapper를 설정해주어야 한다.
- 그룹 정보 동기화 (group-ldap-mapper)
먼저 LDAP의 그룹을 가져와 보자.
LDAP연동한 곳에서 새로운 매퍼를 추가한다.
이 매퍼는 그룹을 동기화 시키기 위해 설정한다.
LDAP 기본설정이아닌 openLDAP 그룹(posixGroup)설정에 맞춰져 있으니 AD등 다른 LDAP서버를 사용한다면,
설정되어있는 LDAP 정보를 잘 보고 사용해야 한다.
- Name: group-ldap-mapper
- Mapper type: group-ldap-mapper
- Group Name LDAP Attribute: cn
- Group Object Class: posixGroup
- Preserve Group Ingeritance: off
- Ignore Missing Group: off
- Membership LDAP Attribute: gidNumber
- Membership Attribute Type: UID
- Membership User LDAP Attribute
- LDAP FIlter: (빈값)
- Mode: READ_ONLY
- User Groups Retrieve Strategy: LODA_GROUPS_BY_MEMBER_ATTRIBUTE
이렇게 설정하고나서 동기화를 해준다.
OpenLDAP에서 설정했던것과 같이 3개의 그룹이 보인다.
다만, 안에 들어가면 유저가 하나도 없는데 그 이유는 아직 사용자의 gidNumber를 가져오지 않았기 때문이다.
- 사용자의 그룹 정보 동기화 (user-attribute-ldap-mapper)
이제 사용자의 gidNumber를 가져오기 위해 매퍼를 하나 더 생성한다.
LDAP에서 사용자와 그룹 정보는 gidMember 값으로 매핑되기때문에,
keycloak에서도 이 정보를 가지고 있어야 사용자를 그룹에 올바르게 매핑할 수 있다.
이제 Sync all users를 클릭하면 사용자 정보를 업데이트 하는것을 확인할 수 있다.
이제 그룹에 사용자가 할당되어있다!
3. Keycloak Client 생성
Keycloak으로 Grafana 인증/인가를 처리할 예정이니, Client를 생성한다.
물론 위에서 만든 my-LDAP Realm에서 만들어야 한다.
- Client type: OpenID Connect
- ClientID: grafana
- Name: grafana
- Client authentication: on
- Root URL: http://127.0.0.1:8084
- Home URL: http://127.0.0.1:8084
- Valid redirect URIs: http://127.0.0.1:8084/login/generic_oauth
- Web origins: http://127.0.0.1:8084
4. LDAP Group Keycloak Role 매핑
이제 앞에서 설정한 LDAP Group과 Client의 Role을 연결해주어야 한다.
우선 Client Role탭에서 Role을 생성한다.
- GrafanaAdmin
- GrafanaEditor
- GrafanaViewer
이제 Group으로 가서 각 그룹과 매칭되는 Client Role을 매핑한다.
- administrartor(LDAP Group) --[mapping]-- GrafanaAdmin(ClientRole)
- editor(LDAP Group) --[mapping]-- GrafanaEditor(ClientRole)
- readonlyuser(LDAP Group) --[mapping]-- GrafanaViewer(ClientRole)
이렇게 하면 LDAP Group설정과 Client Role설정이 매핑된다.
매핑이 잘 되었는지 확인하려면 [사용자 -> Role mapping탭]에서 Hide inherited roles를 해제하고 보면 된다.
4. Client Role을 Protocal Mapper를 통해 Claim에 추가
이제 Role이 사용자 정보에 포함되어있다.
이제 Protocal Mapper를 통해 Claim에 추가해주어야 한다. ("어떤 정보를" "어떤 형식으로" 토큰에 넣을지 지정)
이제 정말 끝났다!!
마지막으로 client secret을 저장한다.
5. Grafana oauth 설정 (grafana.ini)
* Grafana Keycloak SSO 문서: https://grafana.com/docs/grafana/latest/setup-grafana/configure-security/configure-authentication/keycloak/
이제 위에서 받아놓은 grafana helm charts를 수정해주어야 한다.
grafana 디렉토리 내 values.yaml을 찾아 아래 내용을 추가한다. 주의해야할점은 기존 정보를 건드리지 않는것이다!
server:
# Grafana의 외부 접근 URL 설정
root_url: http://127.0.0.1:8084
auth.generic_oauth:
# OAuth 인증 활성화 여부
enabled: true
# OAuth 제공자 이름 설정
name: Keycloak-OAuth
# 새로운 사용자의 자동 회원가입 허용
allow_sign_up: true
# OAuth 클라이언트 ID
client_id: grafana
# OAuth 클라이언트 시크릿 키
client_secret: "Qi3lPg5eU9HA8Hc3LHyseWuhAH936qYU"
# 요청할 OAuth 스코프 (권한 범위)
scopes: "openid email profile offline_access roles"
# 로그인에 사용할 사용자 속성 경로
login_attribute_path: "username"
# 사용자 이름으로 사용할 속성 경로
name_attribute_path: "name"
# 이메일로 사용할 속성 경로
email_attribute_path: "email"
# OAuth 인증 엔드포인트 URL (사용자인증 경로)
auth_url: "http://127.0.0.1:8082/realms/my-LDAP/protocol/openid-connect/auth"
# OAuth 토큰 발급 엔드포인트 URL
token_url: http://keycloak.default.svc.cluster.local/realms/my-LDAP/protocol/openid-connect/token
# 사용자 정보를 가져올 API URL
api_url: http://keycloak.default.svc.cluster.local/realms/my-LDAP/protocol/openid-connect/userinfo
# 사용자 역할 매핑 규칙 (GrafanaAdmin -> Admin, GrafanaViewer -> Editor, 기타 -> Viewer)
role_attribute_path: "contains(roles[*], 'GrafanaAdmin') && 'Admin' || contains(roles[*], 'GrafanaEditor') && 'Editor' || contains(roles[*], 'GrafanaViewer') && 'Viewer'"
# Grafana 관리자 권한 할당 허용
allow_assign_grafana_admin: true
# Grafana 조직 역할 동기화 건너뛰기 비활성화
skip_org_role_sync: false
# 엄격한 역할 속성 매칭 사용
role_attribute_strict: true
이렇게 수정했다면 이제 helm을 업데이트 한다.
helm upgrade grafana . -f values.yaml --set assertNoLeakedSecrets=false
헬름 업데이트 후 터널링이 끊겨있을텐데 한번 더 연결해주자!
kubectl port-forward svc/grafana 8084:80
6. Grafana 에 LDAP 계정으로 로그인하기
이제 드디어 접속해보자
Sign in with Keycloak-OAuth을 클릭하면 Keycloak 로그인 페이지로 redirect 된다.
LDAP 계정에 이메일 설정이 안되어있기 때문에, 이메일 정보를 추가하고 Submit하면 Grafana에 로그인 되며,
사용자 Profile을 확인하면 admin에 매핑되어있는걸 확인할 수 있다.
7. 추가) 트러블슈팅 "IdP did not return a role attribute, please contact your administrator"
# Grafana logs
logger=context userId=0 orgId=0 uname= t=2025-02-22T20:43:15.60593592Z level=info msg="Request Completed" method=GET path=/login/generic_oauth status=302 remote_addr=127.0.0.1 time_ms=0 duration=683.167µs size=309 referer=http://127.0.0.1:8084/login handler=/login/:name status_source=server
logger=oauth.generic_oauth t=2025-02-22T20:43:15.709432879Z level=warn msg="Failed to extract role" err="[oauth.role_attribute_strict_violation] idP did not return a role attribute, but role_attribute_strict is set"
logger=authn.service t=2025-02-22T20:43:15.709525962Z level=error msg="Failed to authenticate request" client=auth.client.generic_oauth error="[auth.oauth.userinfo.error] failed to get user info: [oauth.role_attribute_strict_violation] could not evaluate any valid roles using IdP provided data"
logger=context userId=0 orgId=0 uname= t=2025-02-22T20:43:15.712088504Z level=info msg="Request Completed" method=GET path=/login/generic_oauth status=302 remote_addr=127.0.0.1 time_ms=28 duration=28.130417ms size=29 referer= handler=/login/:name status_source=server
만약 IdP did not return a role attribute, please contact your administrator 오류를 만났다면,
아래 명령어를 통해 Claim으로 roles 추가가 잘 된건지 한번 더 점검해보자.
curl -X POST "http://localhost:8082/realms/my-LDAP/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=grafana" \
-d "client_secret=Qi3lPg5eU9HA8Hc3LHyseWuhAH936qYU" \
-d "username=sadministrator" \
-d "password=1234" \
-d "grant_type=password" | jq -r '.access_token' | jwt decode -
만약 이부분이 정상이라면 helm charts values의 role_attribute_path가 잘못되었을 가능성이 크다.
Roles와 매핑이 잘 되는지 한번 더 점검해보자.