사용자 엔트리 생성
기본 구조
LDAP 사용자는 LDIF(LDAP Data Interchange Format) 파일로 정의합니다.
앞서 설계한 커스텀 스키마와 표준 objectClass를 모두 포함합니다.
dn: uid=john,ou=people,dc=ldap,dc=example,dc=com
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: customHRPerson
objectClass: sambaSamAccount
uid: john
cn: John Doe
sn: Doe
givenName: John
displayName: John Doe
mail: john@example.com
userPassword: {SSHA}hashedpassword
# posixAccount
uidNumber: 10001
gidNumber: 4013
homeDirectory: /home/john
loginShell: /bin/bash
# HR 속성
ou: Development
departmentNumber: DEPT013
hrEmploymentStatus: active
hrEmploymentType: regular
# Samba
sambaSID: S-1-5-21-XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXXX-21003
sambaNTPassword: UPPERCASE_HEX_NT_HASH
sambaAcctFlags: [U ]
sambaPwdMustChange: 2147483647
sambaPrimaryGroupSID: S-1-5-21-XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXXX-513
uidNumber 설계
uidNumber는 시스템 전체에서 유일해야 합니다.
- Linux 시스템 계정은 일반적으로 1000번대를 씁니다
- LDAP 사용자는 충돌을 피하기 위해 높은 번호대(9999번~)를 씁니다
- 신규 사용자: 기존 최대 uidNumber + 1
# 현재 최대 uidNumber 조회
docker exec openldap ldapsearch -x -H ldap://localhost:389 \
-D "cn=admin,dc=ldap,dc=example,dc=com" -w 'your_password' \
-b "ou=people,dc=ldap,dc=example,dc=com" \
"(objectClass=posixAccount)" uidNumber \
| grep uidNumber | sort -t: -k2 -n | tail -1
비밀번호 해시 생성
userPassword는 평문 대신 SSHA(Salted SHA) 해시를 씁니다.
# 컨테이너 내부에서 SSHA 해시 생성
docker exec openldap slappasswd -s 'user_password'
# {SSHA}xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
사용자 추가
# LDIF 파일로 추가
docker exec -i openldap ldapadd -x -H ldap://localhost:389 \
-D "cn=admin,dc=ldap,dc=example,dc=com" \
-w 'your_admin_password' < add-user.ldif
# 또는 파일을 컨테이너로 복사 후 실행
docker cp add-user.ldif openldap:/tmp/
docker exec openldap ldapadd -x -H ldap://localhost:389 \
-D "cn=admin,dc=ldap,dc=example,dc=com" \
-w 'your_admin_password' -f /tmp/add-user.ldif
그룹 엔트리 생성
dn: cn=dept-dev,ou=Groups,dc=ldap,dc=example,dc=com
objectClass: top
objectClass: groupOfNames
objectClass: groupPosixAux
cn: dept-dev
gidNumber: 4013
description: Development Department
member: uid=john,ou=people,dc=ldap,dc=example,dc=com
memberUid: john
그룹에 멤버 추가/제거
# 멤버 추가
cat << 'EOF' | docker exec -i openldap ldapmodify -x -H ldap://localhost:389 \
-D "cn=admin,dc=ldap,dc=example,dc=com" -w 'your_admin_password'
dn: cn=dept-dev,ou=Groups,dc=ldap,dc=example,dc=com
changetype: modify
add: member
member: uid=jane,ou=people,dc=ldap,dc=example,dc=com
-
add: memberUid
memberUid: jane
EOF
# 멤버 제거
cat << 'EOF' | docker exec -i openldap ldapmodify -x -H ldap://localhost:389 \
-D "cn=admin,dc=ldap,dc=example,dc=com" -w 'your_admin_password'
dn: cn=dept-dev,ou=Groups,dc=ldap,dc=example,dc=com
changetype: modify
delete: member
member: uid=john,ou=people,dc=ldap,dc=example,dc=com
-
delete: memberUid
memberUid: john
EOF
자주 쓰는 관리 명령어
조회
ADM="cn=admin,dc=ldap,dc=example,dc=com"
PW="your_admin_password"
BASE="dc=ldap,dc=example,dc=com"
# 사용자 수 확인
docker exec openldap ldapsearch -x -H ldap://localhost:389 \
-D "$ADM" -w "$PW" \
-b "ou=people,$BASE" "(objectClass=posixAccount)" dn \
| grep -c "^dn:"
# 특정 사용자 전체 속성 조회
docker exec openldap ldapsearch -x -H ldap://localhost:389 \
-D "$ADM" -w "$PW" \
-b "ou=people,$BASE" "(uid=john)"
# 특정 그룹의 멤버 목록
docker exec openldap ldapsearch -x -H ldap://localhost:389 \
-D "$ADM" -w "$PW" \
-b "ou=Groups,$BASE" "(cn=dept-dev)" member memberUid
수정 및 삭제
# 비밀번호 변경
docker exec openldap ldappasswd -x -H ldap://localhost:389 \
-D "$ADM" -w "$PW" \
-s 'new_password' "uid=john,ou=people,$BASE"
# 사용자 삭제 (퇴직 처리)
docker exec openldap ldapdelete -x -H ldap://localhost:389 \
-D "$ADM" -w "$PW" \
"uid=john,ou=people,$BASE"
Samba 연동: sambaSamAccount 설정
Samba SMB 파일 서버가 LDAP을 사용자 저장소로 쓰려면 각 사용자 엔트리에 sambaSamAccount objectClass와 관련 속성이 필요합니다.
Domain SID
Samba 도메인은 고유한 SID(Security Identifier)로 식별됩니다.
# 신규 도메인 SID 생성
docker exec openldap samba-tool domain info 127.0.0.1 2>/dev/null | grep "Domain SID" || \
python3 -c "
import uuid, hashlib
u = str(uuid.uuid4()).replace('-','')
parts = [int(u[i:i+8], 16) for i in range(0, 24, 8)]
print(f'S-1-5-21-{parts[0]}-{parts[1]}-{parts[2]}')
"
결과 예시: S-1-5-21-3731106688-3267238735-397953648
이 SID를 기록해 두고 모든 사용자의 sambaSID 계산에 사용합니다.
sambaSID 계산
각 사용자의 sambaSID는 도메인 SID + RID(Relative ID)로 구성됩니다.
sambaSID = {DOMAIN_SID}-{RID}
RID = uidNumber * 2 + 1000
예시: uidNumber가 10001이면 RID = 20002 + 1000 = 21002
sambaSID: S-1-5-21-3731106688-3267238735-397953648-21002
RID를 uidNumber 기반으로 계산하면 uidNumber가 고유한 한 SID도 고유합니다.
사용자 엔트리의 Samba 속성
# Samba 관련 속성만 발췌
sambaSID: S-1-5-21-XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXXX-21003
sambaNTPassword: 8846F7EAEE8FB117AD06BDD830B7586C # NT 해시 (대문자 hex)
sambaAcctFlags: [U ] # U = 일반 사용자
sambaPwdMustChange: 2147483647 # 만료 없음
sambaPrimaryGroupSID: S-1-5-21-XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXXX-513
# 513 = Domain Users (표준 RID)
NT 해시(sambaNTPassword) 생성
sambaNTPassword는 MD4(UTF-16LE(password))의 대문자 hex 값입니다.
이것이 Samba 인증의 핵심입니다.
Python으로 NT 해시 생성
import hashlib
def nt_hash(password: str) -> str:
return hashlib.new('md4', password.encode('utf-16-le')).hexdigest().upper()
print(nt_hash('user_password'))
# 예: 8846F7EAEE8FB117AD06BDD830B7586C
문제: Node.js 20에서 MD4가 비활성화됨
그룹웨어나 내부 서비스를 Node.js로 개발할 때 사용자 비밀번호 변경 시 NT 해시를 동시에 업데이트해야 합니다.
Node.js 17부터 OpenSSL 3을 사용하는데, OpenSSL 3은 레거시 알고리즘으로 분류된 MD4를 기본적으로 비활성화했습니다.
// Node.js 20에서 오류 발생
const crypto = require('crypto');
const hash = crypto.createHash('md4'); // Error: unknown digest
해결책: hash-wasm 패키지
hash-wasm은 WebAssembly로 구현된 해시 라이브러리로, Node.js의 OpenSSL 설정에 의존하지 않습니다.
npm install hash-wasm
const { md4 } = require('hash-wasm');
async function ntHash(password) {
const utf16leBuffer = Buffer.from(password, 'utf16le');
const hash = await md4(utf16leBuffer);
return hash.toUpperCase();
}
// 사용 예
const nt = await ntHash('user_password');
// → '8846F7EAEE8FB117AD06BDD830B7586C'
전체 사용자 NT 해시 일괄 재생성 (Python 스크립트)
이미 등록된 사용자들의 userPassword를 읽어 sambaNTPassword를 일괄 업데이트하는 스크립트입니다.
컨테이너 내부에서 실행합니다.
#!/usr/bin/env python3
import subprocess, base64, hashlib
LDAP_HOST = "ldap://localhost:389"
LDAP_DN = "cn=admin,dc=ldap,dc=example,dc=com"
LDAP_PW = "your_admin_password"
BASE_DN = "ou=people,dc=ldap,dc=example,dc=com"
# 사용자 목록 조회 (sambaSamAccount + userPassword)
result = subprocess.run([
"ldapsearch", "-x", "-H", LDAP_HOST,
"-D", LDAP_DN, "-w", LDAP_PW,
"-b", BASE_DN,
"(&(objectClass=sambaSamAccount))",
"uid", "userPassword"
], capture_output=True, text=True)
# 파싱
entries = {}
current = {}
for line in result.stdout.splitlines():
if line.startswith("dn:"):
if current.get("uid") and current.get("pw"):
entries[current["uid"]] = current["pw"]
current = {}
elif line.startswith("uid: "):
current["uid"] = line.split(": ", 1)[1]
elif line.startswith("userPassword:: "):
try:
current["pw"] = base64.b64decode(line.split(":: ", 1)[1]).decode("utf-8")
except:
pass
elif line.startswith("userPassword: "):
current["pw"] = line.split(": ", 1)[1]
if current.get("uid") and current.get("pw"):
entries[current["uid"]] = current["pw"]
# NT 해시 업데이트
ok = fail = 0
for uid, pw in entries.items():
try:
nt = hashlib.new("md4", pw.encode("utf-16-le")).hexdigest().upper()
ldif = f"""dn: uid={uid},{BASE_DN}
changetype: modify
replace: sambaNTPassword
sambaNTPassword: {nt}
"""
r = subprocess.run([
"ldapmodify", "-x", "-H", LDAP_HOST,
"-D", LDAP_DN, "-w", LDAP_PW
], input=ldif, capture_output=True, text=True)
if r.returncode == 0:
print(f"OK {uid}")
ok += 1
else:
print(f"ERR {uid}: {r.stderr.strip()}")
fail += 1
except Exception as e:
print(f"ERR {uid}: {e}")
fail += 1
print(f"\n완료: {ok}명 성공, {fail}명 실패")
# 컨테이너 내부에서 실행
docker cp gen-nt-hash.py openldap:/tmp/
docker exec openldap python3 /tmp/gen-nt-hash.py
Samba 도메인 엔트리
Samba가 LDAP을 백엔드로 쓰려면 LDAP 트리에 도메인 정보 엔트리도 있어야 합니다.
dn: sambaDomainName=ldap,dc=ldap,dc=example,dc=com
objectClass: sambaDomain
sambaDomainName: ldap
sambaSID: S-1-5-21-XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXXX
sambaNextUserRid: 1000
sambaNextGroupRid: 1001
sambaAlgorithmicRidBase: 1000
퇴직자 처리 절차
계정을 즉시 삭제하지 않고 비활성화하고 싶다면:
# 재직상태 변경 + 로그인 차단
dn: uid=john,ou=people,dc=ldap,dc=example,dc=com
changetype: modify
replace: hrEmploymentStatus
hrEmploymentStatus: resigned
-
replace: sambaAcctFlags
sambaAcctFlags: [D ]
sambaAcctFlags: [D ]는 Disabled 상태로, Samba 로그인이 차단됩니다.userPassword를 잠그려면 앞에 !를 붙인 해시로 교체합니다.
다음 편에서는 이 LDAP 구조를 Synology NAS DSM의 LDAP 클라이언트에 연동하는 설정을 다룹니다.objectClass=groupPosixAux 필터와 memberUid 설정이 핵심입니다.