Инструкция предназначена для переноса пользователей и групп из существующего OpenLDAP-сервера в FreeIPA.
Для тестового примера используется домен tx0.ru
, сервер ipa-test.tx0.ru
и временный контейнер OpenLDAP.
sudo su -
dnf update -y && dnf upgrade -y
dnf install -y dnf-plugins-core curl net-tools traceroute mc git wget nano python3 firewalld ipa-server ipa-healthcheck
В этом примере используется домен
tx0.ru
и имя сервераipa-test.tx0.ru
, не задьте добавить запись для вашего сервера в DNS.
hostnamectl set-hostname ipa-test.tx0.ru
echo "$(curl -s ifconfig.me) $(hostname -f) $(hostname -s)" | tee -a /etc/hosts
sed -i '/^pool 2.cloudlinux.pool.ntp.org iburst/d' /etc/chrony.conf
echo -e "server 0.pool.ntp.org iburst\nserver 1.pool.ntp.org iburst\nserver 2.pool.ntp.org iburst" | tee -a /etc/chrony.conf
timedatectl set-timezone Europe/Moscow
systemctl restart chronyd
ipa-server-install \
--realm=TX0.RU \
--domain=tx0.ru \
--hostname=ipa-test.tx0.ru \
--ip-address=XXX.XXX.XXX.XXX \
--ds-password=YOUR-DS-PASSWORD \
--admin-password=YOUR-ADMIN-PASSWORD \
--http-pin=YOUR-PIN \
--dirsrv-pin=YOUR-DS-PIN \
--mkhomedir \
--no-host-dns \
--unattended
systemctl enable --now firewalld
firewall-cmd --permanent --add-port={80,443,389,636,88,464,8080,8443}/tcp
firewall-cmd --reload
mkdir -p ~/openldap/tmp && cd ~/openldap
docker-compose.yml
:Внутри задается
LDAP_DOMAIN=tx0.ru
— используется как базовый DN:dc=tx0,dc=ru
mkdir openldap && cd openldap && mkdir -p tmp
cat << 'EOF' > docker-compose.yml
version: '3.7'
services:
openldap:
image: osixia/openldap:1.5.0
container_name: openldap
environment:
LDAP_ORGANISATION: "tx0"
LDAP_DOMAIN: "tx0.ru"
LDAP_ADMIN_PASSWORD: "YOU-LDAP-PASSWORD"
ports:
- "389:389"
- "636:636"
restart: unless-stopped
EOF
tmp/full-structure.ldif
:cat << 'EOF' > tmp/full-structure.ldif
dn: ou=accounts,dc=tx0,dc=ru
objectClass: top
objectClass: organizationalUnit
ou: accounts
dn: ou=users,ou=accounts,dc=tx0,dc=ru
objectClass: top
objectClass: organizationalUnit
ou: users
dn: ou=groups,ou=accounts,dc=tx0,dc=ru
objectClass: top
objectClass: organizationalUnit
ou: groups
EOF
tmp/export-full.ldif
generate-file-to-ldap.py
Скрипт преобразует пользователей и группы из
export-full.ldif
в формат для загрузки в OpenLDAP-контейнер. Пример DN в скрипте:dc=tx0,dc=ru
Он создаёт:
tmp/users.ldif
tmp/groups.ldif
tmp/group-members-modify.ldif
⚠️ Не забудьте скорректировать диапазон UID/GID (next_uid_gid_number
) под вывод команды ipa idrange-find
, добавив пару к диапозону.
cat << 'EOF' > ./generate-file-to-ldap.py
#!/usr/bin/env python3
import re
import os
# Файлы
input_file = 'tmp/export-full.ldif'
users_output = 'tmp/users.ldif'
groups_output = 'tmp/groups.ldif'
members_output = 'tmp/group-members-modify.ldif'
# DN-базисы
base_dn = 'dc=tx0,dc=ru'
users_base = f'ou=users,ou=accounts,{base_dn}'
groups_base = f'ou=groups,ou=accounts,{base_dn}'
# UID/GID начало
next_uid_gid_number = 7894560100
next_group_gid_number = 7894560100
old_user_dn_suffix = f'ou=People,{base_dn}'
new_user_dn_suffix = users_base
def parse_entries(text):
entries = []
for raw in text.strip().split('\n\n'):
lines = raw.strip().split('\n')
if not lines:
continue
dn_line = next((l for l in lines if l.lower().startswith('dn: ')), None)
if not dn_line:
continue
dn = dn_line[4:].strip()
entries.append((dn, lines))
return entries
def fix_user_entry(dn, lines, number):
if not re.search(r'uid=', dn, re.IGNORECASE):
return None
uid = re.search(r"uid=([^,]+)", dn, re.IGNORECASE).group(1)
new_dn = f'uid={uid},{users_base}'
output = [f'dn: {new_dn}']
attrs = {
'objectClass': ['inetOrgPerson', 'posixAccount', 'top'],
'uidNumber': str(number),
'gidNumber': str(number),
'loginShell': '/bin/bash',
'homeDirectory': f'/home/{uid}',
}
seen = set()
for line in lines:
if ':' not in line or line.lower().startswith('dn:'):
continue
key, val = map(str.strip, line.split(':', 1))
lkey = key.lower()
# Переносим только нужные атрибуты
if lkey in ['uid', 'cn', 'sn', 'mail', 'givenname', 'displayname', 'userpassword']:
if key not in seen:
output.append(f'{key}: {val}')
seen.add(key)
output.append(f'uidNumber: {attrs["uidNumber"]}')
output.append(f'gidNumber: {attrs["gidNumber"]}')
output.append(f'homeDirectory: {attrs["homeDirectory"]}')
output.append(f'loginShell: {attrs["loginShell"]}')
output += [f'objectClass: {cls}' for cls in attrs['objectClass']]
return '\n'.join(output)
def fix_group_entry(dn, lines, gid_number):
if not re.search(r'cn=', dn, re.IGNORECASE):
return None, None, None
cn = re.search(r'cn=([^,]+)', dn, re.IGNORECASE).group(1)
new_dn = f'cn={cn},{groups_base}'
members = []
for line in lines:
if line.lower().startswith('memberuid:'):
uid = line.split(':', 1)[1].strip()
members.append(f'uid={uid},{users_base}')
elif line.lower().startswith('member:'):
member_dn = line.split(':', 1)[1].strip()
if old_user_dn_suffix in member_dn:
member_dn = member_dn.replace(old_user_dn_suffix, new_user_dn_suffix)
members.append(member_dn)
if not members:
members = [f'cn=dummy,{base_dn}']
output = [
f'dn: {new_dn}',
'objectClass: top',
'objectClass: groupOfNames',
f'cn: {cn}',
f'member: {members[0]}',
]
return '\n'.join(output), new_dn.lower(), members[1:] # Остальных добавим отдельно
def main():
global next_uid_gid_number, next_group_gid_number
with open(input_file) as f:
content = f.read()
entries = parse_entries(content)
users = []
groups = []
members = {}
for dn, lines in entries:
if 'organizationalUnit' in ''.join(lines):
continue
if 'uid=' in dn.lower():
user = fix_user_entry(dn, lines, next_uid_gid_number)
if user:
users.append(user)
next_uid_gid_number += 1
elif 'cn=' in dn.lower():
group, group_dn, group_members = fix_group_entry(dn, lines, next_group_gid_number)
if group:
groups.append(group)
members[group_dn] = group_members
next_group_gid_number += 1
os.makedirs('tmp', exist_ok=True)
with open(users_output, 'w') as f:
f.write('version: 1\n\n' + '\n\n'.join(users) + '\n')
with open(groups_output, 'w') as f:
f.write('version: 1\n\n' + '\n\n'.join(groups) + '\n')
with open(members_output, 'w') as f:
f.write('version: 1\n\n')
for dn, memlist in members.items():
if not memlist:
continue
f.write(f'dn: {dn}\nchangetype: modify\nadd: member\n')
for m in memlist:
f.write(f'member: {m}\n')
f.write('\n')
print("✅ users.ldif, groups.ldif, group-members-modify.ldif созданы.")
if __name__ == '__main__':
main()
EOF
python3 generate-file-to-ldap.py
upload-ldif-to-openldap.sh
#!/bin/bash
LDAP_CONTAINER="openldap"
LDAP_BIND_DN="cn=admin,dc=tx0,dc=ru"
LDAP_PASSWORD="YOUR-LDAP-PASSWORD"
echo "📂 Загрузка структуры"
docker cp tmp/full-structure.ldif $LDAP_CONTAINER:/tmp/
docker exec -i $LDAP_CONTAINER ldapadd -x -D "$LDAP_BIND_DN" -w "$LDAP_PASSWORD" -f /tmp/full-structure.ldif || true
echo "👥 Загрузка групп"
docker cp tmp/groups.ldif $LDAP_CONTAINER:/tmp/
docker exec -i $LDAP_CONTAINER ldapadd -x -D "$LDAP_BIND_DN" -w "$LDAP_PASSWORD" -f /tmp/groups.ldif || true
echo "👤 Загрузка пользователей"
docker cp tmp/users.ldif $LDAP_CONTAINER:/tmp/
docker exec -i $LDAP_CONTAINER ldapadd -x -D "$LDAP_BIND_DN" -w "$LDAP_PASSWORD" -f /tmp/users.ldif || true
echo "🔗 Назначение членов групп"
docker cp tmp/group-members-modify.ldif $LDAP_CONTAINER:/tmp/
docker exec -i $LDAP_CONTAINER ldapmodify -x -D "$LDAP_BIND_DN" -w "$LDAP_PASSWORD" -f /tmp/group-members-modify.ldif
echo "✅ Импорт завершен"
sh upload-ldif-to-openldap.sh
kinit admin
ipa config-mod --enable-migration=TRUE
tx0.ru
):ipa migrate-ds ldap://<OpenLDAP-IP>:389 \
--user-container="ou=users,ou=accounts,dc=tx0,dc=ru" \
--group-container="ou=groups,ou=accounts,dc=tx0,dc=ru" \
--with-compat \
--bind-dn="cn=admin,dc=tx0,dc=ru" \
--continue
ipa config-mod --enable-migration=FALSE
После успешной миграции данных, ваши пользователи завершают процесс миграции самостоятельно переходя по адресу https://ipa-test.tx0.ru/ipa/migration/ с действующими логинами и паролями для получения новых ключей Kerberos.
ldapmodify -x -D "cn=Directory Manager" -W -h localhost -p 389
dn: cn=config
changetype: modify
replace: nsslapd-allow-anonymous-access
nsslapd-allow-anonymous-access: off
Нажмите Ctrl+D
для завершения ввода.