중소기업 OpenLDAP 통합 인증 구축 – 05. 사용자·그룹 관리와 Samba 연동: NT 해시 생성 문제까지

2026.10.19

·

사용자 엔트리 생성

기본 구조

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) 해시를 씁니다.

Sponsored

# 컨테이너 내부에서 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 설정이 핵심입니다.


MORE POSTS

다른 글 보기

테크 랩

PMBOK 완독기 – 00. 오리엔테이션: 왜 정석을 완독하나 + 6판·7판 지도

PMBOK을 목차대로 완독하며 IT 현장에 번역하는 시리즈 시작. 6판(10지식영역)·7판(원칙·테일러링) 지도와 15편 로드맵.
2026.10.22
guniq 인사이트 썸네일 — 중소기업 OpenLDAP 통합 인증 구축 – 07. 백업·로그·운영 자동화: 장애 없는 LDAP 서버 유지하기
테크 랩

중소기업 OpenLDAP 통합 인증 구축 – 07. 백업·로그·운영 자동화: 장애 없는 LDAP 서버 유지하기

LDAP 서버는 설치 후가 더 중요합니다. 데이터 유실을 막는 백업 전략(daily/weekly/monthly), slapd 로그 관리와 logrotate 설정, 헬스체크 자동화까지 — 실제 운영 중인 스크립트를 바탕으로 LDAP 서버를 안정적으로 유지하는 방법을 정리합니다.
2026.10.21
guniq 인사이트 썸네일 — 중소기업 OpenLDAP 통합 인증 구축 – 06. Synology NAS LDAP 연동: DSM 설정과 그룹 권한 매핑
테크 랩

중소기업 OpenLDAP 통합 인증 구축 – 06. Synology NAS LDAP 연동: DSM 설정과 그룹 권한 매핑

Synology DSM의 LDAP 클라이언트를 커스텀 프로파일로 설정하는 방법을 정리합니다. groupPosixAux 필터, memberUid 매핑, LDAPS 인증서 처리까지 — 표준을 지킨 LDAP 구조가 NAS 연동을 얼마나 단순하게 만드는지 확인합니다.
2026.10.20

프로젝트 문의 환영합니다

기획부터 개발, 운영까지 함께 만들어 드립니다.

무료 3분 자가진단

우리 회사, 자체 클라우드가 답일까?

AWS vs 자체 인프라 · 11개 항목 3분 체크