365

Microsoft 365 cross-tenant migration

Hello! Long time no… 🍻😊🍻

I thought I would share some PowerShell I used during a recent cross-tenant migration. Firstly, the Microsoft documentation is really good and got the journey off to a good start:

https://docs.microsoft.com/en-us/microsoft-365/enterprise/cross-tenant-mailbox-migration?view=o365-worldwide

NOTE: Following is some fairly raw PowerShell. You can’t just press Go! You’ll need to understand and update the bits required and run accordingly! Hopefully everything that requires updating is in bold-italics. If not please punish me via comment! =)

That said… let’s continue 😃:

Firstly, let’s create a group in the source tenant called MigUsers@sourcedomain.com and add all of the mailboxes you will migrate (the migration endpoint is scoped to a group, anyone not in the group will fail to migrate).

Now load PowerShell ISE and paste all of the code bits in, then save it for later. First, we need our commands to be able to switch quickly between tenants:

# Source tenant
Connect-ExchangeOnline -UserPrincipalName migadmin@sourcedomain.com
# Target tenant
Connect-ExchangeOnline -UserPrincipalName migadmin@targetdomain.com
# Disconnect from tenant
Disconnect-ExchangeOnline -Confirm:$false

Now we can connect to the target tenant and create the Org Relationship and Migration Endpoint – you’ll need the ‘sourcedomain‘, app ID and secret to paste in here where the bold italics are (follow the MS article above to set up the App Registration and Enterprise App, easy as bro!):

# connect to target tenant here
# Enable customization if tenant is dehydrated
  $dehy = Get-OrganizationConfig | fl isdehydrated
  if ($dehy -eq $true) {Enable-OrganizationCustomization}

# Create Migration Endpoint in target tenant
$AppId = "paste the app id here"
$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $AppId, (ConvertTo-SecureString -String "paste the app secret here" -AsPlainText -Force)
New-MigrationEndpoint -RemoteServer outlook.office.com -RemoteTenant "sourcedomain.onmicrosoft.com" -Credentials $Credential -ExchangeRemoteMove:$true -Name "SourceDomainMigEndpoint" -ApplicationId $AppId

# Create Org Relationship in target tenant
$sourceTenantId="paste source tenant id here"
$orgrels=Get-OrganizationRelationship
$existingOrgRel = $orgrels | ?{$_.DomainNames -like $sourceTenantId}
If ($null -ne $existingOrgRel)
{
    Set-OrganizationRelationship $existingOrgRel.Name -Enabled:$true -MailboxMoveEnabled:$true -MailboxMoveCapability Inbound
}
If ($null -eq $existingOrgRel)
{
    New-OrganizationRelationship "SourceDomainOrgRel" -Enabled:$true -MailboxMoveEnabled:$true -MailboxMoveCapability Inbound -DomainNames $sourceTenantId
}

Let’s use our disconnect / connect commands above to disconnect from the target tenant and connect to the source tenant to set up the other side:

# connect to source tenant here

# Configure OrgRel in source tenant
$targetTenantId="insert the target tenant ID here"
$appId="insert the app id here"
$scope="MigUsers@sourcedomain.com"
$orgrels=Get-OrganizationRelationship
$existingOrgRel = $orgrels | ?{$_.DomainNames -like $targetTenantId}
If ($null -ne $existingOrgRel)
{
    Set-OrganizationRelationship $existingOrgRel.Name -Enabled:$true -MailboxMoveEnabled:$true -MailboxMoveCapability RemoteOutbound -OAuthApplicationId $appId -MailboxMovePublishedScopes $scope
}
If ($null -eq $existingOrgRel)
{
    New-OrganizationRelationship "targetdomainOrgRel" -Enabled:$true -MailboxMoveEnabled:$true -MailboxMoveCapability RemoteOutbound -DomainNames $targetTenantId -OAuthApplicationId $appId -MailboxMovePublishedScopes $scope
}

Hopefully you got to this point without issue – if not let me know in the comments section and I’ll try to help! Next, we should be getting a successful test of what we have set up. Run this command since you are still connected to the source tenant:

# Confirm OrgRel in Source tenant
Get-OrganizationRelationship | fl name, DomainNames, MailboxMoveEnabled, MailboxMoveCapability

Now, disconnect and connect to the target tenant again, and run this:


# Confirm OrgRel and MigEndpoint in Target tenant
Get-MigrationEndpoint
Get-OrganizationRelationship | fl name, DomainNames, MailboxMoveEnabled, MailboxMoveCapability
Test-MigrationServerAvailability -Endpoint "SourceDomainMigEndpoint"

Awesome, green lights!? The tenants are configured. Now from the source tenant, we’ll need to get the following details out into a CSV file so we can create the MailUser objects in the target tenant:

# get source mailbox details
Get-Mailbox | select DisplayName, UserPrincipalName, PrimarySMTPAddress, ExchangeGUID, ArchiveGUID, LegacyExchangeDN | Export-Csv C:\temp\sourceusers.csv -NoTypeInformation

We want to test one user first, so I used these commands (replace bold italic with values for one user from the CSV output):

# create MailUsers in target tenant - ONE AT A TIME HERE OR BELOW FOR BATCHES
$originalalias = 'sales'
$newalias = 'sourcedomain.sales'
$userdisplayname = 'sourcedomain Sales Department'
$exchguid = '80ddefd4-26cb-7621-c497-g6c044b04dd9'
$archguid = '70edegc5-36f8-8639-b287-f6g035408ec2'
$x500address = 'x500:/o=ExchangeLabs/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=ce04185c5ff841f885d660e46e6c8bc5-sourcedomain SAL'
$usersourceaddress = $originalalias + "@sourcedomain.onmicrosoft.com"
$usertargetaddress = $newalias + "@targetdomain.com"
$usertargettenantaddress = $newalias + "@targetdomain.onmicrosoft.com"
New-MailUser -Name $newalias -DisplayName $userdisplayname -ExternalEmailAddress $usersourceaddress -MicrosoftOnlineServicesID $usertargetaddress -Password (ConvertTo-SecureString -String 'Hellosourcedomain2099' -AsPlainText -Force)
Set-MailUser $usertargetaddress -ExchangeGuid $exchguid -ArchiveGuid $archguid -PrimarySmtpAddress $usertargetaddress
Set-MailUser $usertargetaddress -EmailAddresses @{Add="$x500address","$usertargettenantaddress"}
Set-MailUser $usertargetaddress -EmailAddresses @{Remove="smtp:$usersourceaddress"}

Then we can migrate the user:

New-MigrationBatch -Name testbatch -SourceEndpoint 'sourcedomainMigEndpoint' -UserIds $usertargetaddress -Autostart -TargetDeliveryDomain 'targetdomain.onmicrosoft.com'

Once this is working and we are confident all is well, copy then open the CSV output and add columns ‘originalalias’ (the bit before the UPN), ‘newalias’ (set as needed for the target tenant) and ‘newupn’ (new UPN in the target tenant):

Assuming the file is called batch1.csv, create another CSV called batch1-users.csv. This is the input for the migration batch command (very simple and worked well for me), just copy the target UPNs into the file and populate the other columns as below:

Now you can use this code to create the MailUser objects with the first CSV file:

# TO DO BATCHES
# UPDATE THESE CORRECTLY - set variables and import csv
$batchname = 'batch1'
$csv = Import-Csv C:\temp\sourcedomain\batch1.csv

foreach ( $line in $csv ) {

$originalalias = $line.OriginalAlias
$newalias = $line.NewAlias
$userdisplayname = $line.DisplayName
$exchguid = $line.ExchangeGuid
$archguid = $line.ArchiveGuid
$x500address = "X500:" + $line.LegacyExchangeDN
$usersourceaddress = $originalalias + "@sourcedomain.onmicrosoft.com"
$usertargetaddress = $newalias + "@targetdomain.com"
$usertargettenantaddress = $newalias + "@targetdomain.onmicrosoft.com"

Write-Host
Write-Host -ForegroundColor Green "Processing $usertargetaddress..."
Write-Host

New-MailUser -Name $newalias -DisplayName $userdisplayname -PrimarySmtpAddress $usertargetaddress -ExternalEmailAddress $usersourceaddress -MicrosoftOnlineServicesID $usertargetaddress -Password (ConvertTo-SecureString -String 'Hellosourcedomain2099' -AsPlainText -Force)
Set-MailUser $usertargetaddress -ExchangeGuid $exchguid -ArchiveGuid $archguid -PrimarySmtpAddress $usertargetaddress
Set-MailUser $usertargetaddress -EmailAddresses @{Add="$x500address","$usertargettenantaddress"}
Set-MailUser $usertargetaddress -EmailAddresses @{Remove="smtp:$usersourceaddress"}
Get-MailUser $usertargetaddress | fl DisplayName,PrimarySMTPAddress,ExchangeGuid,ArchiveGuid
}

Now we’ve created the MailUser objects for our batch, we can migrate them using the second file:

# create migrationbatch using csv
$batchfile = 'C:\temp\sourcedomain\batch1-users.csv'

New-MigrationBatch -Name $batchname -SourceEndpoint 'sourcedomainMigEndpoint' -CSVData ([System.IO.File]::ReadAllBytes("$batchfile")) -Autostart -TargetDeliveryDomain 'targetdomain.onmicrosoft.com'

This migration went really smoothly… once the user was migrated, the forwarding was set correctly in the source tenant, and permissions were intact for access to Shared Mailboxes etc. Now we are planning to tidy up and move additional email aliases, the accepted domains and MX records across to the new tenant… a far less perilous journey I hope, but really I’m impressed with how relatively easy this was!

Until next time… ‘chur to the chur’ from New Zealand! Hei kone ra 🍻😜

Please do NOT disable Security Defaults!

If you aren’t licensed for and using Conditional Access policies, please do not disable the security defaults feature just because something isn’t working (e.g. scan to email). Microsoft introduced the defaults for a very good reason – they realised that tenants without Azure AD Premium P1 licensing and correctly configured CA policies were wide open to Phishing and Password Spray attacks, via connections to Exchange Online using basic authentication protocols such as POP, IMAP and SMTP.

Connections using basic authentication do not support and therefore bypass MFA. If you disable this setting you are effectively turning off many security features.

Let’s find a solution to these problems and leave our tenant protected ‘by default’.

Here’s how to allow certain things (e.g. Teams meeting room devices and printers) while leaving our tenant secure:

1. Add any external IPs of company locations to Trusted IPs under MFA settings. In most cases you would do this for all company owned office locations. 

https://account.activedirectory.windowsazure.com/UserManagement/MfaSettings.aspx

2. Set Password Reset Registration to No so that new users are not prompted to register.

https://portal.azure.com/#blade/Microsoft_AAD_IAM/PasswordResetMenuBlade/Registration

3. If you need to send SMTP email through Exchange Online (e.g. from a printer), create an account with exchange license to use for sending.

https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/Overview

4. Load Cloud Shell from top of the Azure Portal. Connect to Exchange:

Connect-EXOPSSession

5. Create an EOL Authentication Policy:

New-AuthenticationPolicy -Name “Allow Basic Auth SMTP” -AllowBasicAuthSmtp


6. Assign the policy to the user:

Get-User user@domain.com | Set-User -AuthenticationPolicy “Allow Basic Auth SMTP”


7. Optionally, force the policy to apply within 30 minutes:

Set-User user@domain.com -STSRefreshTokensValidFrom $([System.DateTime]::UtcNow)

Now your users and devices will be able to connect without MFA requirement from trusted offices, and you can set up Scan to Email functions to use the account you created.

Righto – time have a cup of tea and reward yourself for not chopping off a leg to fix an itch! 🤣🤣

Azure Runbook – Licensing Alert

I created this script for a client that wanted to know when they had no available licenses for any SKU.  I’m sure they will add this to the portal soon (?)

The goal is simple – if my consumed no. of licenses = available licenses for any given SKU, send an email to me and my CSP so I can replenish before it becomes a problem.  Easily modified to alert at any number of remaining available licenses. e.g. to alert when there are 5 available licenses change ($_.ConsumedUnits -eq $_.ActiveUnits) to ($_.ConsumedUnits -eq $_.ActiveUnits-5).

The script is written to run as an Azure PowerShell Runbook, which allows use of a credential stored in the automation account, as well as using output to have some nice text show up in the portal logs.  I’m assuming you have set this stuff up already (if you haven’t, google it and get it sorted =). I’ll do a post soon on how to do it but it is not too difficult.

Azure blocks outbound connections on port 25, so no going there!  But aha, they do allow secure port 587.  So I use a ‘soon to be’ deprecated command called send-mailmessage to send the email using a free SendGrid account (no cost for 100 emails per month) which is plenty enough for this solution.

Disclaimer ## as I was testing the script, I noticed Azure now has a ‘SendGrid solution’ where you can sign up to SendGrid free from within the Azure portal – awesome!  Shame I missed it… if I update to using that method I will update this post =).  My understanding is that you could sign up for that, then use it by calling a ‘playbook’ from the automation script.

Here is the script (replace $smtppswd with your sendgrid API key SG.xxxxxx, replace $runbookcredentialname with your runbook cred name, replace $mailfrom and $mailto).  Also, since there may be 0 available licenses or 1000 freely available licenses, by default I’m only considering available license values >1 <500.  Change to suit your needs!

If you have any problems or made the script cooler (like sending the info in an HTML table) please add a comment below! 🙂

# use TLS 1.2
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

# check for any licenses out of stock and send a notification - Simon Burbery - August 2021

# create credential for sending email via SendGrid
$smtpuser = 'apikey'
$smtppswd = ConvertTo-SecureString -String 'SG.xxxxxxx' -AsPlainText -Force
$CredSMTP = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $SMTPuser, $SMTPpswd

# set variables
$runbookcredentialname = 'svc_runbookaccount'
$mailfrom = 'Azure License Notifcation <sendmail@place.co.nz>'
$mailto = @("<admin@place.co.nz>", "<azurealerts@place.co.nz>")
$mailsubject = 'Warning - out of licenses!'
$mailbody = 'Availability of one or more of your license SKUs has reached zero:'
$mailserver = 'smtp.sendgrid.net'
$mailport = '587'
$mailcredential = $CredSMTP

# get credential for msol connection
Try { 
    $CredAzure = Get-AutomationPSCredential -Name $runbookcredentialname
}
        Catch {
            Write-Error "Failed to get credential!"
            Exit
        }   
Write-Output "Get automation credential - Success"

# connect msol
Try {
    Connect-MsolService -Credential $CredAzure
}
        Catch {    
            Write-Error "Failed to connect to MSOnline!"
            Exit
        }
Write-Output "Connect to MSOL - Success"

# license check
$skucheck = Get-MsolAccountSku  | Where-Object { ($_.ActiveUnits -gt 0) -and ($_.ActiveUnits -lt 500) -and ($_.ConsumedUnits -eq $_.ActiveUnits) }

# email body format
$mailbodyfinal = $mailbody,$skucheck | Out-String -Width 500

# send notification
If ( $skucheck -ne $null ) {
    $MailParameters = @{
        From = $mailfrom
        To = $mailto
        Subject = $mailsubject
        Body = $mailbodyfinal
        SmtpServer = $mailserver
        Port = $mailport
        Credential = $CredSMTP
        UseSsl = $true
        }
        Send-MailMessage @MailParameters
            If  ($? -ne $true) { 
    Write-Error "Failed to send email notification!" 
    }
                Else {
                Write-Output "Send email notification - Success"
            }       
}
    Else {
        Write-Output "No licensing issues detected"
    }

# end