Microsoft

Why aren’t you using Azure DNS yet?

The only answers to this question are:

  • I’m already using AWS Route 53 and it’s better!
  • I’m lazy and I like things to take time and cost more, so I pay for someone else to take days to make a change for me [yawhaaat?]
  • I use Cloudflare because someone gave me food and beer (and showed me dnsdumpster) at a sales pitch!
  • What is DNS? In which case please follow this link – https://www.google.com/search?q=funny+cat+videos

I’m starting off-topic (it does say Ramblings at the top of the screen after all) but stay with me!

😃👀🍻👀😃

Hiding your DNS might seem like a good idea; I have nothing against as long as it is free, but to me it is like changing RDP port 3389 to 4444 (not that you have RDP directly exposed to the internet of course – you have an RD Gateway!). Sure, you’ve made it harder to identify an available target, but if someone is really after you they’ll have methods to get around this pretty quickly using port scans or other techniques. You’ve also made it harder for people trying to help to work out what is going on.

The best thing you can do is hurry up on that cloud journey so you can offload concerns about external IP addresses exposing your on-premises entry points. And as long as you are keeping those entry points secure and up to date, there shouldn’t be any major concern here anyway.

If you are running RDS or Citrix services, generally these do not work well with SSL inspection or pre-authentication so are configured to pass-through directly to the entry point i.e. Cloudflare is providing minimal benefit here.

Instead of being fancy pants and paying for these services to provide minimal benefit, let’s look at a great set up for minimal cost that let’s you easily add, remove or change DNS entries right in the portal you use everyday!

First, make sure your entry points are secure:

  • Ideally, make sure you have a Web Application Firewall in front of web facing services. This comes at a cost though, which some of us like to avoid if possible 😜
  • Whether you do or not, review the ciphers available and remove them from least secure upwards until you work out the minimal and most secure configuration. For Windows IIS, remove any ciphers you don’t need as described here: Secure your Web Server
  • Do you have an RD Gateway / Web server? The same applies with IIS, and make sure you don’t allow ‘Domain Users’ in the access policies. Refine this to a group that contains only the users that need access. I’m sure you can get by without ‘Administrator’ being available to brute force hack from outside.
  • Geo-blocking – use it! Most firewalls have some geo capability these days. If your users are in New Zealand and Australia, restricting access to those regions only at the firewall provides a huge security benefit.

Right what was I actually posting about? Ah yes – Azure DNS is easy to set up and costs literally a dollar and cents per month. You don’t transfer your records to Azure though as they are not (and may never be) a registrar. But no bother, once you’ve set up your records in Azure, you simply change the ‘nameserver’ configuration with your existing provider. I use Free Parking in New Zealand, a great low(ish) cost no-frills provider. About $45/year for a domain name and of course they have DNS management, but I’d rather do it in Azure so I after I configured the zone, I copied the four nameserver entries on the right:

…then changed my nameservers from Free Parking to Azure – there will be somewhere you can do this in your providers portal, or just log a request for them to do it:

Done! Once the update is complete you are now serving and managing your DNS entries from the familiar Azure portal.

Here is my Azure DNS zone cost… as you can see – cheap as cheeps mate! Yes I’ll pay a whopping NZD$1.58/month or $18.96/year. That’s only 5.42857 Steak ‘n’ Cheese from Mrs. Miggins pie shop! 🥧

The console is intuitive and I love the fact I can manage my DNS easily and securely from within my Azure tenant. I can also get some metrics about the DNS usage that I couldn’t get before:

Thanks Azure – you get better every day!

TTFN!! 🍻👀🍻👀🍻👀🍻👀🍻👀🍻👀🍻👀🍻👀🍻👀🍻👀🍻👀🍻👀🍻👀🍻👀🍻👀🍻

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

PLEASE 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 mail-enabled security group in the source tenant Exchange Admin console, ‘zzMigUsers@sourcedomain.com’ should do the trick! ..then add the users (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 below code bits in, then save it for later. First, we need our commands to be able to switch quickly between tenants (disconnect before connecting each time):

# 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 to set up the App Registration and Enterprise App, easy as!): Target tenant app setup.

# 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="zzMigUsers@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 – you should see the expected values returned:

# 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 disconnect / connect to 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 (yes I know we could have done that earlier =)). Limit this if required based on the group we created earlier, scoped to your migration users.

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

Disconnect / connect to the target tenant again. 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'

NOTE: Recently I found this command was not working and I had to use a one-liner copy of the batch1-users.csv mentioned below. After the test user is successfully migrated, reference your original file with all the mailboxes in it. Just in case you get this issue in your scenario 😃

Okay! When this is working and we are confident all is well, copy the sourceusers.csv to a new file named batch1-detail.csv. Open the file and add three columns before DisplayName as below. ‘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-detail.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 from batch1-detail.csv into the EmailAddress column and populate the other columns as below:

Now you can use this code to create the MailUser objects with the batch1-detail.csv file:

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

foreach ( $line in $csv ) {

$originalalias = $line.OriginalAlias
$newalias = $line.NewAlias
$newupn = $line.NewUPN
$userdisplayname = $line.DisplayName
$exchguid = $line.ExchangeGuid
$archguid = $line.ArchiveGuid
$x500address = "X500:" + $line.LegacyExchangeDN
$usersourceaddress = $originalalias + "@sourcedomain.onmicrosoft.com"
$usertargetaddress = $newupn
$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'

NOTE: At this point you are migrating the mailboxes because of the “-Autostart” switch. Users in the batch will have ‘Syncing’ status. When ready to complete they will be ‘Synced’.

You can run this command to check the status of a batch:

# Get status of a migration batch:
Get-MigrationBatch $batchname | fl

Or this to check status of a single user:

# Get details for a single mailbox
Get-MigrationUser 'insertupnhere' | Get-MigrationUserStatistics | fl

Or this to get the status of all migration users, updated every 5 minutes:

# output progress of all users every 5 mins
$x = 0
Do{
$x = $x + 1
Get-MigrationUser | Get-MigrationUserStatistics

Start-Sleep -Seconds 300
}
Until ( $x -eq 480 )

When the mailboxes in a batch have ‘Synced’ status, go ahead and complete the batch using this command:

# Complete a Synced batch
Complete-MigrationBatch $batchname -Confirm:$false

This can take a bit of time so have a beer or cup of tea then come back – done! Once completed the mailuser you created in the target tenant will become a mailbox, and the mailbox in the source tenant will become a mailuser with a forwarding address set to the users target onmicrosoft.com address 👍

Some other commands I used are:

# Restart a failed batch
## Start-MigrationBatch $batchname

# remove a completed batch
## Remove-MigrationBatch $batchname -Confirm:$false

# Remove failed users from batches due to failure
## Get-MigrationUser | ? { $_.status -eq 'failed' } | Remove-MigrationUser -Confirm:$false

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… I’m impressed with how relatively easy this was!

Until next time… ‘Hei kone ra’ from New Zealand Aotearoa! 🍻😜

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