LabHub

기업 인증 연동 · LDAP 과 조직도 · 이론

LDAP 트리와 검색 필터, 그리고 조직도의 함정

LabHub 에서 이어서 보기

한 줄 요약

LDAP 은 트리이고, 이 트리에서 그룹이 사람을 가리키는지 사람이 그룹을 가리키는지가 조회 방향을 정한다 — 이 방향을 잘못 잡으면 로그인이 사용자 수에 비례해 느려진다.

왜 이게 필요한가

"사내 계정 붙여 주세요" 는 결국 LDAP 조회로 끝난다. 그런데 필터를 대충 쓰면 비활성 계정과 서비스 계정까지 사용자 목록에 들어오고, 반환 속성을 지정하지 않으면 10 만 명 디렉터리에서 전 속성을 끌어와 서버를 괴롭힌다.

더 조용한 문제는 방향이다. 이 디렉터리는 그룹이 member 로 사람을 가리키므로, 사용자 엔트리를 아무리 봐도 소속 그룹은 나오지 않는다. 이걸 모르고 로그인마다 전체 그룹을 훑는 코드가 실제로 만들어진다. 사용자 100 명일 땐 아무도 모르다가 1 만 명이 되면 로그인이 3 초가 된다.

DIT — 디렉터리는 트리다

LDAP 의 데이터는 DIT(Directory Information Tree) 라는 트리에 담긴다.
각 노드(엔트리)는 DN(Distinguished Name) 으로 유일하게 지목된다.

dc=labhub,dc=co,dc=kr                         ← 루트 (Base DN)├── ou=People│    ├── uid=hong,ou=People,dc=labhub,dc=co,dc=kr│    └── uid=kim,ou=People,dc=labhub,dc=co,dc=kr├── ou=Groups│    ├── cn=dev-team,ou=Groups,...│    └── cn=role-admin,ou=Groups,...└── ou=Disabled                               ← 퇴사자 격리 구역

DN 은 아래에서 위로 읽는다. uid=hong,ou=People,dc=labhub,dc=co,dc=kr
"labhub.co.kr 조직의 People 부서의 hong" 이다. 파일 경로와 반대 방향이라
처음에 헷갈린다.

주요 속성 약어는 이 정도만 알면 시작할 수 있다.

| 약어 | 뜻 | 예 |
| --- | --- | --- |
| dc | domainComponent | dc=labhub,dc=co,dc=kr |
| ou | organizationalUnit | ou=People |
| cn | commonName | cn=홍길동, cn=dev-team |
| uid | userid | uid=hong |
| sn / givenName | 성 / 이름 | sn=홍, givenName=길동 |
| mail | 메일 | hong@labhub.co.kr |
| title | 직급/직책 | title=과장 |
| member | 그룹 구성원 (DN) | member=uid=hong,ou=People,... |

검색 필터 — RFC 4515 의 전위 표기법

LDAP 필터는 연산자가 앞에 오는 괄호 표기다. 처음엔 낯설지만 규칙은 단순하다.

(uid=hong)                          단순 일치(cn=홍*)                            앞부분 일치 (와일드카드)(&(A)(B))                           A 그리고 B(|(A)(B))                           A 또는 B(!(A))                              A 가 아님(objectClass=*)                     해당 속성이 존재

조합하면 이렇게 된다.

(&(objectClass=inetOrgPerson)(ou=개발팀)(title=과장))(&(objectClass=inetOrgPerson)(|(title=부장)(title=차장))(!(ou=총무팀)))

ldapsearch 로는 이렇게 쓴다.

ldapsearch -x -H ldap://127.0.0.1:1389 \  -D "cn=admin,dc=labhub,dc=co,dc=kr" -w labhub123 \  -b "dc=labhub,dc=co,dc=kr" \  "(&(objectClass=inetOrgPerson)(title=과장))" uid cn ou

검색 범위(-s)도 알아 두면 좋다: base(자기만), one(직계 자식만),
sub(하위 전체, 기본값). 큰 디렉터리에서 sub 를 남발하면 느려진다.

그룹 — 두 가지 방향이 있다

멤버십을 표현하는 방법이 두 개다. 그리고 이 차이가 성능과 동기화를 좌우한다.

1. 그룹이 사람을 가리킨다 (groupOfNamesmember 속성)

실무 함정: member 는 있는데 memberOf 가 없는 디렉터리에서
로그인마다 전체 그룹을 스캔하는 코드가 만들어진다. 사용자 100명일 땐 모르다가
1만 명이 되면 로그인이 3초가 된다.

AD 와 OpenLDAP 의 실무적 차이

| 항목 | Active Directory | OpenLDAP |
| --- | --- | --- |
| 로그인 ID 속성 | sAMAccountName (또는 userPrincipalName) | 보통 uid |
| 불변 식별자 | objectGUID | entryUUID |
| 그룹 역참조 | memberOf 기본 제공 | memberof 오버레이 필요 |
| 비밀번호 속성 | unicodePwd (평문 LDAP 로 변경 불가) | userPassword |
| 계정 상태 | userAccountControl 비트 | 별도 규약 |

불변 식별자가 왜 중요한가. 사람은 결혼해서 이름이 바뀌고, 부서를 옮겨 DN 이 바뀐다.
uid 나 DN 을 키로 동기화하면 같은 사람이 신규 사용자로 두 번 생긴다.
반드시 objectGUID / entryUUID 같은 불변 값을 연결 키로 쓴다.
이 설계를 잘못하면 계정 중복 생성 사고가 나고, 그건 조용히 몇 달간 쌓인다.

userAccountControl 은 비트 플래그다. AD 를 다룬다면 이 셋은 외워 두면 좋다.

0x0002  ACCOUNTDISABLE        계정 비활성0x0010  LOCKOUT               잠김0x10000 DONT_EXPIRE_PASSWD    비밀번호 만료 없음512   = 정상 계정514   = 정상 + 비활성 (512 + 2)66048 = 정상 + 비밀번호 만료 없음 (512 + 65536)

그래서 "활성 사용자만" 뽑는 AD 필터는 이렇게 생겼다.

(&(objectCategory=person)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))

가운데 OID 는 "비트 AND" 매칭 규칙이다. 처음 보면 암호 같지만
"userAccountControl 에 2 비트가 켜진 것을 제외"라는 뜻이다.
이 필터를 안 쓰면 비활성 계정과 컴퓨터 계정까지 사용자 목록에 들어온다.

동기화의 두 가지 함정

디렉터리에서 우리 시스템(또는 IdP)으로 사용자를 주기 동기화할 때 반드시 만나는 문제다.

1. 변경분 동기화는 삭제를 감지하지 못한다.
"최근 변경된 엔트리"를 가져오는 방식은, 사라진 엔트리를 볼 방법이 없다.
그래서 퇴사자 계정이 우리 쪽에 살아남는다. 이것이 유령 계정의 단골 원인이다.
→ 변경분 동기화는 자주(1시간), 전체 동기화는 주기적으로(주 1회) 함께 돌린다.
2. 변경 시각은 디렉터리 서버의 시계다.
NTP 가 어긋난 서버 한 대가 있으면, 그 구간의 변경이 조용히 누락된다.
증상은 "가끔 한 명씩 반영이 안 돼요"이고, 원인 파악이 매우 어렵다.

평문 389 은 감사 지적 대상이다

ldap:// 평문 연결로 bind 하면 비밀번호가 네트워크를 평문으로 지나간다.
운영 환경에서는 ldaps://(636) 또는 StartTLS 를 써야 하고,
평문 구성은 보안 감사에서 즉시 지적된다.

(이 실습 환경은 capability 제약으로 1024 미만 포트를 못 열어 1389 평문을 쓴다.
학습용이라 그렇게 한 것이지, 운영 구성이 아니라는 점을 기억하자.)