Как я подключил четвёртый домен и перенёс 70 почтовых ящиков с Яндекс 360 на свой Exchange

У заказчика уже была своя инфраструктура Active Directory и Exchange Server, которая обслуживала почту трёх брендов компании. Когда в структуре появился четвёртый бренд - на 70 ящиков, ранее живших на Yandex 360 - встал закономерный вопрос: платить за отдельную подписку на облачную почту для нового домена или добавить его в уже существующую Exchange-организацию. Решил по двум причинам: подписка на 70 ящиков в Яндексе при живом собственном Exchange-сервере - неоправданный дубль расходов, а вдобавок хотелось, чтобы новый домен управлялся из той же Active Directory, что и остальные три - единая точка администрирования, единые политики, один RBAC. Разворачивать что-то с нуля не пришлось: сервер и так был жив, нужно было аккуратно пристроить к нему ещё один домен.

Инфраструктура на входе

Один сервер Exchange 2019 CU15 (билд 15.2.1748.10, без Security Update), роль Mailbox, Windows Server 2022 Standard. До начала работ - 27 ящиков на трёх доменах компании. Изначально сервер стоял на StandardEvaluation - лицензию ввели заранее, редакция стала Enterprise. Гибрида с Exchange Online нет, весь трафик - через собственный send connector с DNS-маршрутизацией, без smarthost.

Get-Mailbox | Measure-Object
# 27 (до добавления нового домена)

Разграничение адресных политик между тремя существующими доменами держится на структуре OU в Active Directory - контейнеры называются по имени домена, у каждого свой шаблон адреса:

ad.corp.local/brand1.example
ad.corp.local/brand2.example
ad.corp.local/brand3.example

Везде шаблон SMTP:%m@домен - адрес строится из Alias, а не из имени/фамилии, потому что ФИО в карточках AD кириллические. Конвенция Alias - Firstname.Lastname в транслите. Default Policy (без контейнера) собирает адрес @ad.corp.local - для объектов вне трёх брендовых OU.

Внешний IP Exchange уже был занят под PTR одного из трёх существующих доменов (mail.brand1.example) - отдельный PTR под новый, четвёртый домен решил не делать, он и не нужен для приёма почты через уже существующий FQDN коннектора.

Ящики: 68 из 70 с первого раза

Новый домен newbrand.example добавил в организацию Authoritative (а не InternalRelay - на период сосуществования с Яндексом это означало бы, что почта на несуществующие адреса будет отбиваться на нашей стороне, а не улетать транзитом мимо, что и требовалось). Под новый домен - своя база:

New-MailboxDatabase -Name DB-NewBrand -Server EX01 -EdbFilePath "D:\Exchange\DB-NewBrand\DB-NewBrand.edb"
Mount-Database -Identity DB-NewBrand

Ящики создавались пакетно, по CSV-выгрузке из Яндекса:

Import-Csv .\newbrand-users.csv | ForEach-Object {
    New-Mailbox -Name $_.Name -Alias $_.Alias `
        -UserPrincipalName "$($_.Alias)@newbrand.example" `
        -OrganizationalUnit "ad.corp.local/newbrand.example" `
        -Database DB-NewBrand -Password (ConvertTo-SecureString $_.TempPass -AsPlainText -Force)
}

68 из 69 создались с первого раза (70-я запись в выгрузке - личный адрес администратора на Яндексе, не переносили, увели отдельно). Не создался info@newbrand.example - алиас info в организации уже занят ящиком info@brand3.example, а алиасы в Exchange-организации уникальны глобально, а не в рамках домена. Решение: info для нового бренда завёл под другим алиасом, у бренда оставил как есть - переименовывать давно работающий адрес ради нового домена не стали.

Сертификат: SAN, который не покрывал один из доменов

Заодно всплыла отдельная, не связанная напрямую с миграцией находка: у действующего сертификата, выпущенного через win-acme (--source iis, привязка по сайту IIS), в SAN не было имени для brand3.example - только пять остальных имён, включая mail.newbrand.example и autodiscover.newbrand.example, которые нужно было добавить для нового домена. Перевыпустил с полным набором:

wacs.exe --source iis --siteid 1 --force

Здесь отдельный нюанс, который легко упустить: POP и IMAP подхватывают новый сертификат автоматически, потому что настроены на X509CertificateName=mail.brand1.example - имя, а не отпечаток. А вот привязка для SMTP - по отпечатку сертификата, и после перевыпуска её нужно обновлять руками:

Get-ExchangeCertificate | Format-List Thumbprint, Subject, NotAfter
Enable-ExchangeCertificate -Thumbprint <новый_отпечаток> -Services SMTP

Без этого шага SMTP продолжил бы отдавать старый, скоро истекающий сертификат, несмотря на то что новый уже выпущен и виден в панели.

Экспорт из Яндекса: пароли и лимит API

С Яндекс 360 выгрузил ровно 70 записей - конвенция адресов там отличалась от нашей (фамилия в нижнем регистре: ivanova@, petrova@, а не Firstname.Lastname). Массовая смена паролей перед миграцией дала две отдельные проблемы:

Яндекс 360 отклоняет пароли с не-ASCII символами - PATCH-запрос на смену пароля падал с HTTP 400, если в сгенерированном пароле оказывался символ вроде «№». Пришлось перегенерировать пароли по безопасному ASCII-алфавиту отдельно для тех аккаунтов, где скрипт успел подставить такой символ.

Второе - OAuth-токен, которым скрипт стучался в API 360, переставал приниматься прямо посреди прогона (HTTP 401), хотя в начале запуска был рабочим. Эмпирически нашёл причину: у API 360 есть неофициальный лимит на количество операций записи подряд - около 20, после чего сессия токена рвётся. Решение простое - пауза в 1 секунду между запросами и повторный запуск скрипта с того места, где он остановился:

foreach ($user in $users) {
    try {
        Set-360UserPassword -Login $user.Login -Password $user.NewPass
    } catch {
        Write-Warning "Failed: $($user.Login) - $_"
    }
    Start-Sleep -Seconds 1
}

Пароли для Яндекса и для AD/Exchange у одних и тех же 69 пользователей - разные наборы, поэтому для imapsync понадобились два отдельных каталога passfile, а не один общий.

imapsync: перенос 647 ГБ и то, что не перенеслось

Сама перекачка почты - тоже пакетно, по тому же CSV:

imapsync --host1 imap.yandex.ru --user1 "$alias@newbrand.example" --passfile1 "yandex/$alias.pass" \
         --host2 EX01 --user2 "$alias@newbrand.example" --passfile2 "exchange/$alias.pass" \
         --ssl1 --ssl2 --automap --syncinternaldates

Итог по объёму: 69 ящиков, 647 ГБ суммарно, база DB-NewBrand - 648.5 ГБ на диске. Распределение внутри домена оказалось крайне неравномерным: 4 самых крупных ящика (в том числе info@ и general-адреса отделов) - это 90% всего объёма, при медианном размере ящика в 181 МБ. То есть типичный сотрудник почти ничего не весит, а несколько общих ящиков-архивов тянут на себе почти весь трафик переноса.

Около 18 писем imapsync перенести не смог - ошибка BAD Command Argument Error на стороне Exchange при попытке дозаписи. Решение - не гонять imapsync по кругу ради полутора десятков писем, а перенести их вручную (экспорт/импорт через .eml). Повторных массовых прогонов синхронизации не делал.

Права: management scope, RBAC-грабля и то, что не стали выдавать

Администрирование нового домена нужно было делегировать отдельному сотруднику заказчика, не давая ему доступ ко всей организации целиком. Сделал через management scope и кастомную ролевую группу:

New-ManagementScope -Name "NewBrandRecipients" `
    -RecipientRoot "ad.corp.local/newbrand.example" `
    -RecipientRestrictionFilter {RecipientType -eq 'UserMailbox'}

New-RoleGroup -Name "NewBrandAdmins" `
    -Roles "Mail Recipients", "View-Only Configuration" `
    -CustomRecipientWriteScope "NewBrandRecipients"

Add-ADPermission -Identity DB-NewBrand -User "NewBrandAdmins" `
    -ExtendedRights Receive-As

Отдельная неожиданная находка: попытка довесить группе ещё и роль «Reset Password» (чтобы администратор бренда мог сам сбрасывать пароли своим сотрудникам) не сработала обычным New-ManagementRoleAssignment - оказалось, что у родительской роли Organization Management в этой организации самой не назначено делегирующее право на роль Reset Password, а без него делегировать её дальше вниз по иерархии RBAC невозможно. Чинить это ради одной роли не стал и осознанно решил не выдавать сброс паролей новому администратору домена - только просмотр и управление получателями, а сброс паролей остаётся на стороне основной команды.

Квоты на новую базу выставил по образцу уже работающих доменов, но индивидуально понизил для рядовых ящиков и оставил высокими для общих:

Set-MailboxDatabase -Identity DB-NewBrand -ProhibitSendQuota 19GB -IssueWarningQuota 20GB -ProhibitSendReceiveQuota 20GB
Set-Mailbox -Identity "info@newbrand.example" -ProhibitSendReceiveQuota 40GB -IssueWarningQuota 38GB

Итог

Все 69 реальных ящиков перенесены (647 ГБ), четвёртый бренд работает на той же Exchange-организации, что и остальные три, под своей делегированной администратором учётной записью с ограниченными правами. Незапланированный простой Exchange из-за постороннего, давно тлевшего сбоя AD не сорвал сроки - выправил точечно, не дожидаясь основного ремонта контроллера домена.

Выводы

  1. Добавление домена в существующую Exchange-организацию почти всегда дешевле и логичнее, чем отдельная подписка, если инфраструктура для этого уже есть - но требует аккуратности с уникальными на уровне организации сущностями (алиасы, RBAC-scope), которые по ошибке легко зацепить за уже работающие домены.
  2. SMTP-сертификат в Exchange живёт отдельно от POP/IMAP - привязка по отпечатку, а не по имени, и про неё легко забыть после автоматического перевыпуска через win-acme.
  3. API стороннего облачного провайдера может иметь неофициальные лимиты, которые нигде не задокументированы явно - эмпирическая пауза между запросами дешевле, чем попытки понять причину пачки HTTP 401 из документации.
  4. RBAC в Exchange не всегда позволяет делегировать то, что не делегировано выше по цепочке - иногда правильный ответ не «починить это», а сознательно не выдавать конкретное право и оставить его на стороне основной команды.

Похожая задача - подключить новый домен к существующей Exchange-организации, или разобраться с миграцией почты откуда угодно - пишите, разберём.

Оставьте комментарий