LDAP 서비스 구축

LDAP 기본 용어 및 데이터 구조

용어 정리

  • DN (Distinguished Name): 엔트리의 전체 절대 경로 (예: cn=kim,ou=dev,dc=example,dc=com)
  • RDN (Relative DN): 상대 경로, 트리 상위 노드와 구분되는 이름 (예: cn=kim)
  • dc (Domain Component): 도메인 요소 (example.com -> dc=example,dc=com)
  • ou (Organizational Unit): 그룹/부서 단위
  • cn (Common Name): 개별 개체 이름 (사용자명, 서버명 등)
  • sn (Surname): 성(Family Name/Last Name), sn=Hong
dc=example,dc=com (도메인)
 ├── ou=dev (부서: Organizational Unit)
 │    ├── cn=kim (사용자: Common Name)
 │    └── cn=lee
 └── ou=hr
      └── cn=park
  • upn : User Principal Name ex)호스트명@도메인명
  • entry : 디렉터리에 보관된 정보의 기본 단위로, Entry가 나타내는 객체(사용자)에 대한 정보를 가지고 있는 속성 집합으로 구성. Entry의 집합은 DN(Distinguished Name)을 기반으로 DIT(Directory Information Tree)라는 계층적인 트리 구조로 구성됨.
  • ldif : LDAP Data Interchange Format
  • l : LocalityName (도시, 특정지역 단위명)
  • c : Country Name
  • dit : Directory Information Tree
  • ldif : LDAP Data Interchange Format
  • o : Organization Name
  • uid : User ID
  • givenName : 이름
  • Objectclass

모든 엔트리 들은 하나하나의 Objectclass 를 갖는다. Objectclass 란 동일한 설정을 갖고 있는 일종의 그룹이라고 이해하면 됨. 새로운 엔트리가 생성될 때, 특정한 Objectclass에 속하는 엔트리로 생성을 하게 되면 해당 Objectclass의 속성을 그대로 상속받게 되는 원리. https://docs.oracle.com/cd/E29542_01/reference.1111/e10035/schema_objclass.htm ☞쿼리문자열 샘플 (오른쪽에서 왼쪽으로 읽는다) “cn=dev-korea,ou=distribution groups,dc=gp,dc=gl,dc=google,dc=com”

LDAP 개념 정리

  • LDAP DIT(Directory Informaation Tree)
                   +--------+
                   |   dc   | dc=com
                   +---+----+
                       |
                   +---+----+
                   |   dc   | dc=example
                 +-+--------+--+
                 |             |
             +--------+    +--------+
     ou=tech |   ou   |    |   ou   | ou=sales
        +----+---+----+    +---+----+----------+
        |        |             |               |
  +-------+--+  +--------------+ +----------+ +--+-----------+
  | cn=staff |  | uid=teseter1 | | cn=staff | | uid=teseter2 |
  +----------+  +--------------+ +----------+ +--------------+
  ex) "uid=user01"의 DN은 "uid=user01,ou=tech,dc=example,dc=com"
      "cn=user01", "ou=tech" 등 단일 항목은 RDN
  • ObjectClass Entry에서 꼭 필요하거나 가질 수 있는 Attribute 타입을 정의한다. Entry를 만들 때 ObjectClass를 통해 데이터에 필수적으로 들어가야 하는 정보를 담을 수 있게 정의할 수 있으며 ObjectClass는 다른 ObjectClass를 상속해 구현하며 개념을 확장할 수 있다.
  • Schema Schema는 ObjectClass와 Attribute에 대해 정의하는 규칙으로 보면 된다. ObjectClass에 어떤 Attribute가 들어갈지, Attribute의 값에 대한 제약 및 조건 등 관련된 규칙들을 정의할 수 있다. 스키마 정의를 통해 여러 응용 프로그램에서 디렉토리 서비스를 읽고 사용할 때 상호운용성을 보호해주는 역할을 한다.
  • LDAP 구조 LDAP은 아래 4가지 모델로 나눌 수 있다.
    1. Information 모델 데이터의 형태와 데이터를 통해 디렉토리 구조로 정보를 저장하는 방식에 관한 것으로 Entry와 각 Entry마다 Attribute가 존재하여 타입이나 변수 값을 저장할 수 있다. “OU=People”, "CN=Gerald Carter"등은 해당 계층을 나타내는 고유한 주소 Attribute에 해당한다.
    2. Naming 모델 트리 구조에서 각 Entry마다 존재하는 RDN 값들을 통해 원하는 경로에 있는 Entry 정보를 찾을 수 있다. 또한 경로 내 RDN값들을 이어 붙여 생성된 고유한 문자를 DN(Distinguished Name)이라고 부르며, LDAP의 DIT 형태에서 가장 위에 존재하는 Entry는 DIT의 시작점, 데이터 트리의 루트로 보며 하나의 데이터셋으로 이해할 수 있다.
    3. Functional 모델 LDAP 디렉토리에서 작업하는 명령을 의미한다. 8가지의 작업 명령으로 나누게 되고 작업 명령의 기능에 따라 3가지로 구분한다.
      • 질문 작업
        • Search: 주어진 조건에 맞는 Entry 도출
        • Compare: 특정 Entry의 Attribute 값 비교
      • 갱신 작업
        • Add: 디렉토리에 신규 Entry 추가
        • Delete: 디렉토리에 기존 Entry 삭제
        • Modify: 디렉토리에 기존 Entry 수정 및 Entry DN값 변경
      • 인증 및 제어 작업
        • Bind: 디렉토리 서버 연결 시 사용자 인증
        • Unbind: 디렉토리 서버와의 연결 해제
        • Abandon: 이전 요청 명령을 취소
    4. Security 모델 디렉토리에 접근하는 사용자 인증과 데이터 접근 권한을 통해 서비스를 보호하는 방식을 의미한다. LDAP v3 버전에서는 기존의 보안 방식뿐만 아니라 외부의 인증 방법을 제공할 수 있는 SASL 방식도 제공한다.

OpenLDAP 설치 및 설정

OpenLDAP 설치

apt -y install slapd
apt -y install ldap-utils
apt -y install ldap-account-manager

OpenLDAP 기본 설정

dpkg-reconfigure slapd
  Omit OpenLDAP server configuration? no
  DNS domain name: example.com
  Organization name My Company
  Administrator password: XXXXXXX
  Do you want the database to be removed when slapd is purged? no
  Move old database? yes
systemctl status slapd
systemctl enable slapd

LDAP 조회

ldapsearch -x -H ldap://10.10.10.1 -D "cn=admin,dc=example,dc=com" -w 'Pass@234' -b "dc=example,dc=com"

LAM을 위한 PHP LDAP 패키지 설치

ls -l /etc/php 로 php 버전 확인 후
apt -y install php8.4-ldap
apt -y install php8.4-mbstring
apt -y install php8.4-xml
apt -y install php8.4-curl
apt -y install php8.4-gd
apt -y install php8.4-zip

OpenLDAP(slapd) 데이터 초기화

만일 데이터를 완전히 초기화하고 다시 입력하려면 아래를 입력한다.

systemctl stop slapd
rm -rf /var/lib/ldap
rm -rf /etc/ldap/slapd.d

dpkg-reconfigure slapd
rm -rf /var/backups/*.ldapdb
systemctl restart slapd

LDAP 기본 OU 생성

vi /root/scripts/ldap/base.ldif

dn: ou=People,dc=example,dc=com
objectClass: organizationalUnit
ou: People

dn: ou=Groups,dc=example,dc=com
objectClass: organizationalUnit
ou: Groups

dn: ou=Policies,dc=example,dc=com
objectClass: organizationalUnit
ou: Policies
  • 등록
ldapadd -x -H ldap://10.10.10.1 -D "cn=admin,dc=example,dc=com" -w 'Pass@234' -f /root/scripts/ldap/base.ldif
  • 확인
ldapsearch -x -H ldap://10.10.10.1 -b "dc=example,dc=com" "(objectClass=organizationalUnit)"

LDAP 기본 그룹 생성

vi /root/scripts/ldap/groups.ldif

dn: cn=employees,ou=Groups,dc=example,dc=com
objectClass: top
objectClass: posixGroup
cn: employees
gidNumber: 10000
  • 등록
ldapadd -x -H ldap://10.10.10.1 -D "cn=admin,dc=example,dc=com" -w 'Pass@234' -f /root/scripts/ldap/groups.ldif

LDAP 사용자 추가 스크립트 생성

CSV 형식의 사용자 데이터 생성

username,cn,sn,email,password
user01,user01,user01,user01@example.com,"Abc!1234#$user01"
user02,user02,user02,user02@example.com,"T3st!@#$%^&*02"
user03,user03,user03,user03@example.com,"P@ss,w0rd!03"
...

create_ldap_users.sh 작성

#!/bin/bash

set -euo pipefail

#-----------------------------------------------------------
# OpenLDAP CSV bulk user creation script
#-----------------------------------------------------------

LDAP_URI="ldap://10.10.10.1"

BASE_DN="dc=example,dc=com"
USER_BASE_DN="ou=employees,${BASE_DN}"
ADMIN_DN="cn=admin,${BASE_DN}"

CSV_FILE="${1:-users.csv}"

START_UID=10000
GID_NUMBER=10000

WORK_DIR="/root/ldap-user-import"
LDIF_FILE="${WORK_DIR}/users.ldif"

#-----------------------------------------------------------
# Functions
#-----------------------------------------------------------

cleanup()
{
    unset ADMIN_PASSWORD
}

trap cleanup EXIT

die()
{
    echo "ERROR: $*" >&2
    exit 1
}

#-----------------------------------------------------------
# Pre-check
#-----------------------------------------------------------

if [[ ! -f "${CSV_FILE}" ]]; then
    die "CSV file not found: ${CSV_FILE}"
fi

command -v ldapadd >/dev/null 2>&1 ||
    die "ldapadd command not found"

command -v ldapsearch >/dev/null 2>&1 ||
    die "ldapsearch command not found"

command -v python3 >/dev/null 2>&1 ||
    die "python3 command not found"

mkdir -p "${WORK_DIR}"
chmod 700 "${WORK_DIR}"

: > "${LDIF_FILE}"
chmod 600 "${LDIF_FILE}"

#-----------------------------------------------------------
# LDAP administrator password
#-----------------------------------------------------------

read -r -s -p "LDAP administrator password: " ADMIN_PASSWORD
echo

#-----------------------------------------------------------
# Test LDAP connection
#-----------------------------------------------------------

echo
echo "[1/5] Testing LDAP administrator authentication..."

if ! ldapwhoami \
    -x \
    -H "${LDAP_URI}" \
    -D "${ADMIN_DN}" \
    -w "${ADMIN_PASSWORD}" \
    >/dev/null 2>&1
then
    die "LDAP authentication failed"
fi

echo "LDAP authentication: OK"

#-----------------------------------------------------------
# Check employees OU
#-----------------------------------------------------------

echo
echo "[2/5] Checking employee OU..."

if ! ldapsearch \
    -x \
    -H "${LDAP_URI}" \
    -D "${ADMIN_DN}" \
    -w "${ADMIN_PASSWORD}" \
    -b "${USER_BASE_DN}" \
    -s base \
    "(objectClass=organizationalUnit)" \
    dn \
    >/dev/null 2>&1
then

    echo "OU does not exist."
    echo "Creating: ${USER_BASE_DN}"

    OU_LDIF="${WORK_DIR}/employees-ou.ldif"

    cat > "${OU_LDIF}" <<EOF
dn: ${USER_BASE_DN}
objectClass: top
objectClass: organizationalUnit
ou: employees
EOF

    ldapadd \
        -x \
        -H "${LDAP_URI}" \
        -D "${ADMIN_DN}" \
        -w "${ADMIN_PASSWORD}" \
        -f "${OU_LDIF}"
else
    echo "OU already exists: ${USER_BASE_DN}"
fi

#-----------------------------------------------------------
# CSV -> LDIF
#-----------------------------------------------------------

echo
echo "[3/5] Converting CSV to LDIF..."

python3 - \
    "${CSV_FILE}" \
    "${LDIF_FILE}" \
    "${USER_BASE_DN}" \
    "${START_UID}" \
    "${GID_NUMBER}" <<'PYTHON'
import base64
import csv
import hashlib
import os
import sys

csv_file = sys.argv[1]
ldif_file = sys.argv[2]
user_base_dn = sys.argv[3]
start_uid = int(sys.argv[4])
gid_number = int(sys.argv[5])

def ssha(password: str) -> str:
    """
    Generate OpenLDAP-compatible {SSHA} password hash.
    """

    password_bytes = password.encode("utf-8")

    salt = os.urandom(8)

    digest = hashlib.sha1(
        password_bytes + salt
    ).digest()

    result = digest + salt

    encoded = base64.b64encode(result).decode("ascii")

    return "{SSHA}" + encoded

def ldif_value(value: str) -> str:
    """
    Encode every attribute value as LDIF Base64.

    This safely handles:
      - Korean characters
      - leading/trailing spaces
      - colon
      - hash
      - quotes
      - backslashes
      - special characters
    """

    encoded = base64.b64encode(
        value.encode("utf-8")
    ).decode("ascii")

    return encoded

required_fields = {
    "username",
    "cn",
    "sn",
    "email",
    "password",
}

with open(
    csv_file,
    "r",
    encoding="utf-8-sig",
    newline=""
) as f:

    reader = csv.DictReader(f)

    if reader.fieldnames is None:
        raise SystemExit("CSV header not found")

    missing = required_fields - set(reader.fieldnames)

    if missing:
        raise SystemExit(
            "Missing CSV fields: "
            + ", ".join(sorted(missing))
        )

    rows = list(reader)

if not rows:
    raise SystemExit("CSV contains no users")

usernames = set()

with open(
    ldif_file,
    "w",
    encoding="utf-8",
    newline="\n"
) as output:

    for index, row in enumerate(rows):

        username = row["username"].strip()
        cn = row["cn"].strip()
        sn = row["sn"].strip()
        email = row["email"].strip()

        # Password must NOT be stripped.
        #
        # Leading/trailing spaces may intentionally be part
        # of the password.
        password = row["password"]

        if not username:
            raise SystemExit(
                f"Empty username at CSV line {index + 2}"
            )

        if username in usernames:
            raise SystemExit(
                f"Duplicate username: {username}"
            )

        usernames.add(username)

        if not cn:
            raise SystemExit(
                f"Empty cn for user: {username}"
            )

        if not sn:
            raise SystemExit(
                f"Empty sn for user: {username}"
            )

        if not email:
            raise SystemExit(
                f"Empty email for user: {username}"
            )

        if not password:
            raise SystemExit(
                f"Empty password for user: {username}"
            )

        uid_number = start_uid + index

        password_hash = ssha(password)

        dn = (
            f"uid={username},"
            f"{user_base_dn}"
        )

        home_directory = (
            f"/home/{username}"
        )

        #
        # LDIF attributes are Base64 encoded.
        #
        # Syntax:
        #
        # attribute:: BASE64
        #

        output.write(
            f"dn:: {ldif_value(dn)}\n"
        )

        output.write(
            "objectClass: top\n"
        )

        output.write(
            "objectClass: inetOrgPerson\n"
        )

        output.write(
            "objectClass: posixAccount\n"
        )

        output.write(
            "objectClass: shadowAccount\n"
        )

        output.write(
            f"uid:: {ldif_value(username)}\n"
        )

        output.write(
            f"cn:: {ldif_value(cn)}\n"
        )

        output.write(
            f"sn:: {ldif_value(sn)}\n"
        )

        output.write(
            f"mail:: {ldif_value(email)}\n"
        )

        output.write(
            f"uidNumber: {uid_number}\n"
        )

        output.write(
            f"gidNumber: {gid_number}\n"
        )

        output.write(
            f"homeDirectory:: "
            f"{ldif_value(home_directory)}\n"
        )

        output.write(
            "loginShell: /bin/bash\n"
        )

        output.write(
            f"userPassword:: "
            f"{ldif_value(password_hash)}\n"
        )

        output.write("\n")

        print(
            f"{username}: "
            f"uidNumber={uid_number}"
        )

print()
print(
    f"Generated {len(rows)} LDAP users"
)
PYTHON

#-----------------------------------------------------------
# Check duplicate LDAP users
#-----------------------------------------------------------

echo
echo "[4/5] Checking existing LDAP users..."

EXISTING_USER=0

while IFS= read -r USERNAME
do
    [[ -z "${USERNAME}" ]] && continue

    RESULT=$(
        ldapsearch \
            -x \
            -LLL \
            -H "${LDAP_URI}" \
            -D "${ADMIN_DN}" \
            -w "${ADMIN_PASSWORD}" \
            -b "${USER_BASE_DN}" \
            "(uid=${USERNAME})" \
            dn \
            2>/dev/null || true
    )

    if [[ -n "${RESULT}" ]]; then
        echo "Already exists: ${USERNAME}"
        EXISTING_USER=1
    fi

done < <(
    python3 - "${CSV_FILE}" <<'PYTHON'
import csv
import sys

with open(
    sys.argv[1],
    "r",
    encoding="utf-8-sig",
    newline=""
) as f:
    reader = csv.DictReader(f)

    for row in reader:
        print(row["username"].strip())
PYTHON
)

if [[ "${EXISTING_USER}" -ne 0 ]]; then
    die "Existing LDAP users detected. Import cancelled."
fi

#-----------------------------------------------------------
# LDAP import
#-----------------------------------------------------------

echo
echo "[5/5] Adding users to LDAP..."

ldapadd \
    -x \
    -H "${LDAP_URI}" \
    -D "${ADMIN_DN}" \
    -w "${ADMIN_PASSWORD}" \
    -f "${LDIF_FILE}"

echo
echo "=================================================="
echo "LDAP user import completed"
echo "=================================================="
echo
echo "CSV:"
echo "  ${CSV_FILE}"
echo
echo "Base DN:"
echo "  ${USER_BASE_DN}"
echo
echo "UID range:"
echo "  ${START_UID} - $((START_UID + 11))"
echo
echo "Generated LDIF:"
echo "  ${LDIF_FILE}"
echo

create_ldap_users.sh 실행

./create_ldap_users.sh /root/scripts/ldap/ldap_users.csv
LDAP administrator password:
...
Generated LDIF:
  /root/ldap-user-import/users.ldif

계정 정상 등록 확인

ldapsearch -x -LLL -H ldaps://ldap.example.com:636 -D "cn=admin,dc=example,dc=com" -w 'Pass@234' -b "ou=employees,dc=example,dc=com" "(objectClass=posixAccount)" dn uid

수동 추가 (오류 발생 시)

ldapadd -x -H ldaps://ldap.example.com:636 -D "cn=admin,dc=example,dc=com" -w 'Pass@234' -v -f /root/ldap-user-import/users.ldif

OpenLDAP(slapd) 인증서 적용

OpenLDAP(slapd) 인증서 생성

mkdir /etc/ssl/ldap

CA (인증 기관) 인증서 생성

  1. CA 개인키 생성
openssl genrsa -out /etc/ssl/ldap/ca.key 4096
  1. CA 인증서 생성 (유효기간 10년)
openssl req -new -x509 -days 3650 -key /etc/ssl/ldap/ca.key -out /etc/ssl/ldap/ca.crt -subj "/C=KR/ST=Seoul/L=Seoul/O=example/OU=IT/CN=exampleCA"

SAN (Subject Alternative Name) 확장 작성

cat > /etc/ssl/ldap/san.ext << EOF
subjectAltName = @alt_names
[alt_names]
DNS.1 = ldap.example.com
IP.1 = 10.10.10.1
EOF

LDAP 서버 인증서 생성

openssl genrsa -out /etc/ssl/ldap/ldap.example.com.key 4096
openssl req -new -key /etc/ssl/ldap/ldap.example.com.key -out /etc/ssl/ldap/ldap.example.com.csr -subj "/C=KR/ST=Seoul/L=Seoul/O=example/OU=IT/CN=ldap.example.com"

CA로 서버 인증서 서명

openssl x509 -req -in /etc/ssl/ldap/ldap.example.com.csr -CA /etc/ssl/ldap/ca.crt -CAkey /etc/ssl/ldap/ca.key -CAcreateserial -out /etc/ssl/ldap/ldap.example.com.crt -days 3650 -sha256 -extfile /etc/ssl/ldap/san.ext

LDAP 서버 인증서 생성

openssl verify -CAfile /etc/ssl/ldap/ca.crt /etc/ssl/ldap/ldap.example.com.crt
/etc/ssl/ldap/ldap.example.com.crt: OK

LDAP 인증서 적용

chown root:openldap /etc/ssl/ldap/ldap.example.com.key
chmod 640 /etc/ssl/ldap/ldap.example.com.key
vi /root/scripts/ldap/ldap-tls.ldif
dn: cn=config
changetype: modify
replace: olcTLSCertificateFile
olcTLSCertificateFile: /etc/ssl/ldap/ldap.example.com.crt
-
replace: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: /etc/ssl/ldap/ldap.example.com.key
-
replace: olcTLSCACertificateFile
olcTLSCACertificateFile: /etc/ssl/ldap/ca.crt
ldapmodify -Y EXTERNAL -H ldapi:/// -f /root/scripts/ldap/ldap-tls.ldif

LDAP 인증서 검사 비활성화 (사설 인증서인 경우에 적용)

vi /etc/ldap/ldap.conf
TLS_CACERT /etc/ssl/ldap/ca.crt
TLS_REQCERT demand
#TLS_REQCERT never

추가

LDAP 인증서 활성화

vi /etc/default/slapd
아래를 수정한다.
SLAPD_SERVICES="ldap:/// ldapi:///" →
SLAPD_SERVICES="ldap:/// ldapi:/// ldaps:///"
systemctl restart slapd
# 확인
ss -lntp | grep slapd
LISTEN ... :636
LISTEN ... :389

TLS 확인

openssl s_client -connect 127.0.0.1:636 -servername ldap.example.com

LDAP 확인

ldapsearch -x -H ldaps://ldap.example.com:636 -b "dc=example,dc=com"

Password Policy 설정

Password Policy 모듈 추가

vi /root/scripts/ldap/ppolicy-module.ldif
dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: ppolicy.la

ldapmodify -Y EXTERNAL -H ldapi:/// -f /root/scripts/ldap/ppolicy-module.ldif

Password Policy 모듈 확인

ldapsearch -Y EXTERNAL -H ldapi:/// -b cn=config ‘(objectClass=olcModuleList)’ olcModuleLoad olcModuleLoad: {1}ppolicy.la

Password Policy entry 생성

최소 길이 10 이전 PW 재사용 최근 2개 금지 실패 허용 5회 잠금 시간 15분 PW 만료 없음 사용자 PW 변경 허용

vi /root/scripts/ldap/password-policy.ldif

dn: cn=default,ou=Policies,dc=example,dc=com
cn: default
objectClass: pwdPolicy
objectClass: namedPolicy
objectClass: top
pwdAllowUserChange: TRUE
pwdAttribute: userPassword
pwdCheckQuality: 1
pwdExpireWarning: 0
pwdFailureCountInterval: 900
pwdGraceAuthNLimit: 0
pwdInHistory: 2
pwdLockout: TRUE
pwdLockoutDuration: 900
pwdMaxAge: 0
pwdMaxFailure: 5
pwdMinAge: 0
pwdMinLength: 10
pwdMustChange: FALSE
pwdSafeModify: TRUE
ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/ldap/schema/namedobject.ldif
ldapadd -x -H ldaps://ldap.example.com:636 -D "cn=admin,dc=example,dc=com" -w 'Pass@234' -f /root/scripts/ldap/password-policy.ldif

ppolicy overlay 적용

ppolicy overlay는 디렉토리 내부에서 직접 서버 측 비밀번호 정책을 관리할 수 있게 해주는 핵심 모듈. 이는 LDAP 클라이언트나 별도의 애플리케이션에 의존하지 않고, OpenLDAP 서버 자체에서 비밀번호의 복잡성, 만료, 잠금 등의 규칙을 적용하고 관리.

  • OpenLDAP database 번호 찾기 ldapsearch -Y EXTERNAL -H ldapi:/// -b cn=config ‘(olcSuffix=dc=example,dc=com)’ dn olcSuffix “dn: olcDatabase={1}mdb,cn=config” 존재를 확인한다.

  • 등록

vi /root/scripts/ldap/ppolicy-overlay.ldif
dn: olcOverlay=ppolicy,olcDatabase={1}mdb,cn=config
objectClass: olcOverlayConfig
objectClass: olcPPolicyConfig
olcOverlay: ppolicy
olcPPolicyDefault: cn=default,ou=Policies,dc=example,dc=com
olcPPolicyHashCleartext: TRUE
ldapadd -Y EXTERNAL -H ldapi:/// -f /root/scripts/ldap/ppolicy-overlay.ldif
  • 확인
ldapsearch -Y EXTERNAL -H ldapi:/// -b cn=config '(olcOverlay=ppolicy)'
ppolicy_hash_cleartext를 사용하면 LDAP 클라이언트가 password modify 요청을 보낼 때 서버가 서버 측 password hashing 정책에 따라 처리할 수 있다. 이 경우 전송 구간을 반드시 TLS로 보호해야 한다.

LAM(LDAP Account Manager) Community

LDAP Account Manager 설치

cd /opt
wget https://github.com/LDAPAccountManager/lam/releases/download/9.6/ldap-account-manager-9.6.tar.bz2
tar -xjf ldap-account-manager-9.6.tar.bz2
rm -f ldap-account-manager-9.6.tar.bz2
ln -s ldap-account-manager-9.6 ldap-account-manager

LDAP Account Manager 퍼미션 조정

cd /opt/ldap-account-manager
cp config/config.cfg.sample config/config.cfg
chown -R root:root /opt/ldap-account-manager
chown -R www-data:www-data \
  /opt/ldap-account-manager/sess \
  /opt/ldap-account-manager/tmp \
  /opt/ldap-account-manager/config
find /opt/ldap-account-manager/sess \
     /opt/ldap-account-manager/tmp \
     /opt/ldap-account-manager/config \
     -type d -exec chmod 750 {} \;
find /opt/ldap-account-manager/sess \
     /opt/ldap-account-manager/tmp \
     /opt/ldap-account-manager/config \
     -type f -exec chmod 640 {} \;
chmod 750 /opt/ldap-account-manager/lib/lamdaemon.pl

LDAP Tool Box Self Service Password 설치 준비

설치

apt install -y \
 php8.4 \
 php8.4-cli \
 php8.4-common \
 php8.4-curl \
 php8.4-gd \
 php8.4-ldap \
 php8.4-mbstring \
 php8.4-xml \
 smarty4 \
 curl \
 ca-certificates

LDAP Tool Box Self Service Password 설치

cd /opt
wget https://ltb-project.org/archives/ltb-project-self-service-password-1.8.1.tar.gz
tar -xzf ltb-project-self-service-password-1.8.1.tar.gz
rm -f ltb-project-self-service-password-1.8.1.tar.gz
chown -R root:root ltb-project-self-service-password-1.8.1
ln -s ltb-project-self-service-password-1.8.1 self-service-password

LDAP Tool Box Self Service Password 퍼미션 조정

cd /opt/self-service-password
mkdir -p /opt/self-service-password/cache
mkdir -p /opt/self-service-password/templates_c
chown -R root:root /opt/self-service-password
chown www-data:www-data \
  /opt/self-service-password/cache \
  /opt/self-service-password/templates_c
chmod 750 \
  /opt/self-service-password/cache \
  /opt/self-service-password/templates_c

LDAP Tool Box Self Service Password 설정 파일 수정

cp /opt/self-service-password/conf/config.inc.php /opt/self-service-password/conf/config.inc.php.orig
vi /opt/self-service-password/conf/config.inc.local.php
<?php
/*
 * ============================================================
 * Debug
 * ============================================================
 */
$debug = false;

/*
 * ============================================================
 * LDAP
 * ============================================================
 */
$ldap_type = "openldap";
$ldap_url = "ldaps://ldap.example.com:636";
$ldap_starttls = false;
$ldap_base = "ou=employees,dc=example,dc=com";
$ldap_login_attribute = "uid";
$ldap_fullname_attribute = "cn";
$ldap_scope = "sub";
$ldap_filter = "(&(objectClass=inetOrgPerson)(objectClass=posixAccount)(uid={login}))";
$ldap_network_timeout = 10;

/*
 * ============================================================
 * LDAP manager account
 * ============================================================
 */
$ldap_binddn = "cn=admin,dc=example,dc=com";
$ldap_bindpw = "LDAP 패스워드";

/*
 * ============================================================
 * Password change
 * ============================================================
 */
$who_change_password = "user";
$ldap_use_exop_passwd = true;
$ldap_use_ppolicy_control = true;

/*
 * ============================================================
 * Password policy
 * ============================================================
 */
$pwd_min_length = 10;
$pwd_max_length = 0;
$pwd_min_lower = 1;
$pwd_min_upper = 1;
$pwd_min_digit = 1;
$pwd_min_special = 1;
$pwd_special_chars = "^a-zA-Z0-9";
$pwd_diff_login = true;
$pwd_forbidden_ldap_fields = array(
    "uid",
    "cn",
    "sn",
    "mail"
);
$pwd_complexity = 0;
$pwd_show_policy = "always";
$pwd_show_policy_pos = "above";
$pwd_display_entropy = true;
$pwd_check_entropy = false;

/*
 * ============================================================
 * Features
 * ============================================================
 */
$use_change = true;
$use_questions = false;
$use_tokens = false;
$use_sms = false;
$use_restapi = false;
$use_attributes = false;
# openssl rand -base64 48
$keyphrase = "YMYG7xwwElJir52QrXjfuWZwcSLVNIlQGE1ySSoI20ppPIn4MZxNoBid9M36oXyY";

/*
 * ============================================================
 * General
 * ============================================================
 */
$reset_url = "https://www.example.com/ssp/";
$lang = "ko";
$allowed_lang = array(
    "ko",
    "en"
);

$date_timezone = "Asia/Seoul";
$show_menu = true;
$show_help = true;
$default_action = "change";
$use_captcha = false;

/*
 * ============================================================
 * Production
 * ============================================================
 */
$show_extended_error = false;
$smarty_debug = false;

/*
 * ============================================================
 * Smarty
 * ============================================================
 */
if (!defined("SMARTY")) {
    define("SMARTY", "/usr/share/php/smarty4/Smarty.class.php");
}
chown -R root:www-data /opt/self-service-password/conf
chmod 750 /opt/self-service-password/conf
chmod 640 /opt/self-service-password/conf/*

계정 존재 여부 검사

ldapsearch -x -LLL -H ldaps://ldap.example.com:636 -D "cn=admin,dc=example,dc=com" -w 'Pass@234' -b "ou=employees,dc=example,dc=com" "(objectClass=posixAccount)" dn uid

사용자가 현재 비밀번호로 Bind 가능한지 확인

ldapwhoami -x -H ldaps://ldap.example.com:636 -D "uid=user01,ou=employees,dc=example,dc=com" -W
dn:uid=user01,ou=employees,dc=example,dc=com

자기 비밀번호 변경이 가능한지 확인

ldappasswd -x -H ldaps://ldap.example.com:636 -D "uid=user01,ou=employees,dc=example,dc=com" -w 'Pass@234' -S

apache 설정

mkdir -p /etc/apache2/includes
vi /etc/apache2/includes/example-ldap-tools.conf
# ============================================================
# LDAP Web Management Tools
#
# Include this file INSIDE the existing www.example.com:443
# VirtualHost.
#
# LAM:
#   https://www.example.com/lam/
#
# Self Service Password:
#   https://www.example.com/ssp/
# ============================================================

# ------------------------------------------------------------
# LDAP Account Manager
# ------------------------------------------------------------
RedirectMatch 301 ^/lam$ /lam/
Alias /lam/ /opt/ldap-account-manager/

<Directory /opt/ldap-account-manager>
  Options -Indexes
  AllowOverride None
  Require all granted
  DirectoryIndex index.html index.php

  <FilesMatch "(^\.|\.bak$|\.old$|\.orig$|~$)">
    Require all denied
  </FilesMatch>
</Directory>

# LAM runtime/configuration data must never be downloaded directly.
<Directory /opt/ldap-account-manager/config>
  Require all denied
</Directory>

<Directory /opt/ldap-account-manager/sess>
  Require all denied
</Directory>

<Directory /opt/ldap-account-manager/tmp>
  Require all denied
</Directory>

# ------------------------------------------------------------
# Self Service Password
# ------------------------------------------------------------
RedirectMatch 301 ^/ssp$ /ssp/
Alias /ssp/ /opt/self-service-password/htdocs/
<Directory /opt/self-service-password/htdocs>
  Options -Indexes
  AllowOverride None
  Require all granted
  DirectoryIndex index.php
  AddDefaultCharset UTF-8

  <FilesMatch "(^\.|\.bak$|\.old$|\.orig$|~$)">
    Require all denied
  </FilesMatch>
</Directory>

# ------------------------------------------------------------
# SSP REST API
# ------------------------------------------------------------
# SSP documentation recommends denying REST by default.
# The current deployment does not use REST.
Alias /ssp/rest/ /opt/self-service-password/rest/
<Directory /opt/self-service-password/rest>
  Options -Indexes
  AllowOverride None
  Require all denied
</Directory>

apache 활성화

  • https://www.example.com/lam
  • https://www.example.com/ssp

사전 LDAP 트리 검색 (데이터 확인)

ldapsearch -LLL -x -H ldaps://ldap.example.com:636 -D "cn=admin,dc=example,dc=com" -w 'Pass@234' -b "dc=example,dc=com" dn
dn: dc=example,dc=com
dn: ou=People,dc=example,dc=com
dn: ou=Groups,dc=example,dc=com
dn: ou=Policies,dc=example,dc=com
dn: cn=employees,ou=Groups,dc=example,dc=com
dn: cn=default,ou=Policies,dc=example,dc=com
dn: ou=employees,dc=example,dc=com

ldapsearch -LLL -x -H ldaps://ldap.example.com:636 -D "cn=admin,dc=example,dc=com" -w 'Pass@234' -b "dc=example,dc=com" -s one "(objectClass=organizationalUnit)" dn
dn: ou=People,dc=example,dc=com
dn: ou=Groups,dc=example,dc=com
dn: ou=Policies,dc=example,dc=com
dn: ou=employees,dc=example,dc=com

ldapsearch -LLL -x -H ldaps://ldap.example.com:636 -D "cn=admin,dc=example,dc=com" -w 'Pass@234' -b "ou=employees,dc=example,dc=com" "(uid=user01)" objectClass

LAM Community 기본 설정

Server Profile 설정

  • LAM configuration > Edit general settings

  • Configuration storage

    • Database type: Local file system
  • Security settings

    • Hide LDAP details on failed login: ☑
    • Allowed hosts:
      • 121.170.221.42
      • 119.196.53.221
      • 59.15.104.62
      • 10.10.10.*
      • 127.0.0.1
  • Password policy

    • Minimum password length: 10 …

Server Profile 추가

  • LAM configuration > Edit server profiles > Manage server profiles
  • Add profile
    • Profile name: example
    • Profile password / Reenter password 입력
    • Template: unix
    • [Add]

Server Profile 설정

  • LAM configuration > profile name: example > Edit server profiles

  • General settings

    • Server settings
      • Server address: ldaps://ldap.example.com:636
      • Activate TLS: no
      • Login method: Fixed list
      • List of valid users: cn=admin,dc=example,dc=com
      • Advanced options+
        • Display name: example
    • Language settings
      • Default language: English (USA)
      • Time zone: Asia/Seoul
    • Tool settings
      • Tree suffix: dc=example,dc=com
  • Account types

    • Active account types
      • Users
        • LDAP suffix: ou=employees,dc=example,dc=com
        • List attributes: #uid;#cn;#sn;#mail;#uidNumber;#gidNumber
      • Groups
        • LDAP suffix: ou=groups,dc=example,dc=com
        • List attributes: #cn;#gidNumber;#memberUID;#description
  • Module settings

    • Unix
      • Options
        • Password hash type: SSHA
        • Login shells: /usr/sbin/nologin 추가

OpenLDAP 패스워드 변경

  • MDB database의 olcRootPW를 변경

비밀번호 해쉬 생성

slappasswd
New password: XXXXXXXXXXXX
Re-enter new password: XXXXXXXXXXXX
{SSHA}...

실제 LDAP database DN 확인

ldapsearch -Y EXTERNAL -H ldapi:/// -LLL -b cn=config '(olcSuffix=dc=example,dc=com)' dn olcRootDN olcRootPW
dn: olcDatabase={1}mdb,cn=config 확인

olcRootPW 변경

cat > /root/scripts/ldap/change-ldap-admin-password.ldif <<'EOF'
dn: olcDatabase={1}mdb,cn=config
changetype: modify
replace: olcRootPW
olcRootPW: {SSHA}...을 입력
EOF

적용

ldapmodify -Y EXTERNAL -H ldapi:/// -f /root/scripts/ldap/change-ldap-admin-password.ldif

자주 사용하는 LDAP 관리 커맨드


일반 사용자의 패스워드 변경

ldappasswd -x -H ldaps://ldap.example.com:636 -D "uid=user01,ou=employees,dc=example,dc=com" -w 'Pass@234' -S

OpenLDAP 사용자 추가

비밀번호 해쉬 생성

slappasswd
New password: XXXXXXXXXXXX
Re-enter new password: XXXXXXXXXXXX
{SSHA}...

uidNumber 중복 검사

  • ⚠중요: uidNumber: 10020는 절대 중복되면 안되며, 아래의 커맨드로 미리 검사한다. (아무것도 안나오면 성공)
ldapsearch -x -LLL -H ldap://10.10.10.1 -D "cn=admin,dc=example,dc=com" -w 'Pass@234' -b "ou=employees,dc=example,dc=com" "(uidNumber=10020)" dn

모든 사용자 목록 출력

ldapsearch -x -LLL -H ldap://10.10.10.1 -b "ou=employees,dc=example,dc=com" uidNumber

LDIF 생성

vi /root/testuser.ldif
dn: uid=testuser,ou=employees,dc=example,dc=com
objectClass: top
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: testuser
cn: 홍길동
sn: testuser
mail: testuser@example.com
uidNumber: 10020
gidNumber: 10000
homeDirectory: /home/testuser
loginShell: /bin/bash
userPassword: {SSHA}xxxxxxxxxxxxxxxxxxxxxxxxxxxx

생성

ldapadd -x -H ldap://10.10.10.1 -D "cn=admin,dc=example,dc=com" -w 'Pass@234' -f /root/testuser.ldif

생성 확인

ldapsearch -x -LLL -H ldap://10.10.10.1 -D "cn=admin,dc=example,dc=com" -w 'Pass@234' -b "ou=employees,dc=example,dc=com" "(uid=testuser)"

OpenLDAP 사용자 삭제

ldapdelete -H ldap://10.10.10.1 -D "cn=admin,dc=example,dc=com" -w 'Pass@234' "uid=user01,ou=employees,dc=example,dc=com"

OpenLDAP 연결 테스트

export LDAPTLS_REQCERT=never
ldapsearch -LLL -x -H ldaps://10.10.10.1:636 -D "cn=admin,dc=example,dc=com" -w 'Pass@234' -b "dc=example,dc=com" dn
위로 스크롤