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가지 모델로 나눌 수 있다.
- Information 모델 데이터의 형태와 데이터를 통해 디렉토리 구조로 정보를 저장하는 방식에 관한 것으로 Entry와 각 Entry마다 Attribute가 존재하여 타입이나 변수 값을 저장할 수 있다. “OU=People”, "CN=Gerald Carter"등은 해당 계층을 나타내는 고유한 주소 Attribute에 해당한다.
- Naming 모델 트리 구조에서 각 Entry마다 존재하는 RDN 값들을 통해 원하는 경로에 있는 Entry 정보를 찾을 수 있다. 또한 경로 내 RDN값들을 이어 붙여 생성된 고유한 문자를 DN(Distinguished Name)이라고 부르며, LDAP의 DIT 형태에서 가장 위에 존재하는 Entry는 DIT의 시작점, 데이터 트리의 루트로 보며 하나의 데이터셋으로 이해할 수 있다.
- Functional 모델
LDAP 디렉토리에서 작업하는 명령을 의미한다. 8가지의 작업 명령으로 나누게 되고 작업 명령의 기능에 따라 3가지로 구분한다.
- 질문 작업
- Search: 주어진 조건에 맞는 Entry 도출
- Compare: 특정 Entry의 Attribute 값 비교
- 갱신 작업
- Add: 디렉토리에 신규 Entry 추가
- Delete: 디렉토리에 기존 Entry 삭제
- Modify: 디렉토리에 기존 Entry 수정 및 Entry DN값 변경
- 인증 및 제어 작업
- Bind: 디렉토리 서버 연결 시 사용자 인증
- Unbind: 디렉토리 서버와의 연결 해제
- Abandon: 이전 요청 명령을 취소
- 질문 작업
- 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 (인증 기관) 인증서 생성
- CA 개인키 생성
openssl genrsa -out /etc/ssl/ldap/ca.key 4096
- 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
- Server settings
-
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
- Users
- Active account types
-
Module settings
- Unix
- Options
- Password hash type: SSHA
- Login shells: /usr/sbin/nologin 추가
- Options
- Unix
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
