📘 Инструкция по миграции данных из OpenLDAP в FreeIPA через контейнер OpenLDAP

Инструкция предназначена для переноса пользователей и групп из существующего OpenLDAP-сервера в FreeIPA. Для тестового примера используется домен tx0.ru, сервер ipa-test.tx0.ru и временный контейнер OpenLDAP.


🔹 Этап 1: Установка и базовая настройка FreeIPA

1. Подключитесь к серверу и установите зависимости:

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

2. Установите имя хоста:

В этом примере используется домен 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

3. Настройка времени и NTP:

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

4. Установка FreeIPA:

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

5. Открытие необходимых портов:

systemctl enable --now firewalld
firewall-cmd --permanent --add-port={80,443,389,636,88,464,8080,8443}/tcp
firewall-cmd --reload

🔹 Этап 2: Запуск промежуточного OpenLDAP в Docker

1. Подготовка структуры:

mkdir -p ~/openldap/tmp && cd ~/openldap

2. Создание 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

3. Базовая структура каталогов 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

🔹 Этап 3: Подготовка данных для импорта

1. Разместите экспорт действующего LDAP в файл:

tmp/export-full.ldif

2. Используйте Python-скрипт 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

3. Запуск скрипта генерации:

python3 generate-file-to-ldap.py

🔹 Этап 4: Импорт в OpenLDAP-контейнер

1. Скрипт 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 "✅ Импорт завершен"

2. Запуск импорта:

sh upload-ldif-to-openldap.sh

🔹 Этап 5: Импорт в FreeIPA

1. Аутентификация администратора:

kinit admin

2. Включение режима миграции:

ipa config-mod --enable-migration=TRUE

3. Миграция из OpenLDAP (пример с доменом 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

4. Отключение режима миграции:

ipa config-mod --enable-migration=FALSE

После успешной миграции данных, ваши пользователи завершают процесс миграции самостоятельно переходя по адресу https://ipa-test.tx0.ru/ipa/migration/ с действующими логинами и паролями для получения новых ключей Kerberos.

🔹 Этап 6: (Опционально) Отключение анонимного доступа к OpenLDAP

1. Запустите ldapmodify:

ldapmodify -x -D "cn=Directory Manager" -W -h localhost -p 389

2. Вставьте LDIF:

dn: cn=config
changetype: modify
replace: nsslapd-allow-anonymous-access
nsslapd-allow-anonymous-access: off

Нажмите Ctrl+D для завершения ввода.