Notes on upgrading an Exchange On-Premises Certificate in a Exchange Online Hybrid Environment

Scenario: Here are some notes on upgrading an Exchange On-Premises Certificate in an Exchange Online Hybrid Environment.

Notes:

#After installing a new Exchange On-Premises Server, Check the following:
    


    #0. Renew the Cert(On-Prem EAC)

            #Create a req
            #Submit the req
            #Complete the Cert Request with your Cert Provider



    #1. Perform a fresh Export and Import of the Cert to all Servers
            #Do this via Exchange On-Premises EAC --> Servers --> Certificates.   Export the Certificate with a password, then turn around and import the certificate on all other servers.




    #2. Configure Services on the Cert
            #Find the thumbprint of the Cert on the new server
                Get-ExchangeCertificate -server <server> 
            #Move the services to the new Cert, #Note: If prompted, select yes or accept to move the SMTP from the old cert to the new cert.
                Enable-Exchangecertificate -thumbprint <thumbprint> -server <servername> -services IMAP,POP,IIS,SMTP

           
        



    #3. SMTP Routing:
            #Within Exchange On-Premises
            
                #a. Check and Set the Transport Server to the new cert:
                        Get-transportserver -identity <servername> | Select InternalTransportCertificateThumbprint
                    #If the thumbprint is not the new cert, move the SMTP Service, and any other service, to the new certificate
                        enable-exchangecertificate -thumbprint <thumbprint> -services SMTP 
                    #Notes: The new Exchange certificate needs to be registered to SMTP for TLS. If this is not done, message routing will break.


                    #For the next two, build the TLSCert Name in Issuer/Subject format:
                        $Cert = Get-ExchangeCertificate -Thumbprint <thumbprint of new certificate> -server <Exchange Server name>
                        $TLSCert = (‘<I>’+$cert.issuer+'<S>’+$cert.subject)

            #b. Check and Set the $TLSCert on the Send Connector to ExOnline
                        Get-SendConnector -identity <Office 365 send Connector> | Select TLSCertificateName
                    #If its not using the new cert,  run the following
                        Set-SendConnector -Identity <Office 365 send Connector> -TLSCertificateName $TLSCert
                    #Notes: You will not be able to send email from ExOnPrem to ExOnline until the new $TLSCert matches.  Messages will become Queued On-Premises.
    
            
            #c. Check and Set the $TLSCert the Receive Connector
                        Get-ReceiveConnector "<Servername>\Default Frontend <servername>" | Select TLSCertificateName
                    #If its not using the new cert,  run the following
                        Get-ReceiveConnector "<Servername>\Default Frontend <Servername>" | Set-ReceiveConnector -TlsCertificateName $TLSCert
                    #Notes: Until the $TLSCert is configured correctly, or matches, there may be a 2-minute delay in receiving messages From Exchange Online to Exchange On-Premises. There could potentially be a delay in message routing breaking completely if the older, expired cert is no longer on the server.

           
           #Within Exchange Online 
            
           #a. Verify the Exchange Online Hybrid Inbound connector (Connector labeled  'Inbound from <GUID>    FROM:  Your org    To: O365')
                    #Within EAC --> Mail Flow --> Connectors, select the inbound connector from 'Your Org' to 'O365' and make sure the $TLSCert matches the "Authenticating Sent Email" Cert name
            
            #b. Verify that the TLS properties of the Exchange On-Premises Hybrid Outbound Connector is using a namespace that is hosted as the Subject or Subject Alternative name, in your new certificate
                    #Within EAC --> Mail Flow --> Connectors, select the outbound connector from 'O365' to 'Your Org' and make sure it references namespace (mail.domain.com) that is listed in your certificate as the Subject, or Subject Alternative name.





    #4. Restart your On-Premises Services
         #a. Restart IIS, MSExchangePop*, MSExchangeImap*, MSExchangeTransport*

    


    #5. Azure App Proxy:  Upload and replace the Cert (from step 1) on Azure App Proxy (APP)

450 4.4.317 Cannot connect to remote server [Message=451 5.7.3 StartTLS is required to send mail]


Scenario: After a recent renewal of one of our Exchange On-Premises Certificates in our Exchange Online Hybrid Environment, we noticed a 2 minute delay when messages were sending from Exchange Online to Exchange On-Premises via a custom send connector (not the hybrid connector)

Investigating: After some troubleshooting, we realized the TLS string , $TLSCert = (‘<I>’+$cert.issuer+'<S>’+$cert.subject), was not the exact same than what was configured in various properties on our Exchange On-Premises Servers.

Solution: We ran the following PowerShell to check and set the correct certificate properties. In our specific scenario, it was the send connector (which was no longer able to send email to Exchange Online) and it was our receive connector, which was giving us a 2 minute email delay. I suspect this would have been a bigger issue if we removed the older certificate that it was still pointing to right away.

 #Find the TLSCert Name:
        $Cert = Get-ExchangeCertificate -Thumbprint <thumbprint of new certificate> -server <Exchange Server name>
        $TLSCert = (‘<I>’+$cert.issuer+'<S>’+$cert.subject)

    #Check and set the Transport Server to the new cert:
       Get-transportserver -identity <servername> | Select InternalTransportCertificateThumbprint
       #If the thumbprint is not the new cert, move the SMTP Service, and any other service, to the new certificate
       enable-exchangecertificate -thumbprint <thumbprint> -services SMTP 
    
    #Check and Set the Send Connector to the new TLS Certificate Name
        Get-SendConnector -identity <Office 365 send Connector> | Select TLSCertificateName
        #If its not using the new cert,  run the following
        Set-SendConnector -Identity <Office 365 send Connector> -TLSCertificateName $TLSCert
    
    #Check and Set the Receive Connector
        Get-ReceiveConnector "ServerName\Default Frontend ReceiveConnector" | Select TLSCertificateName
        #If its not using the new cert,  run the following
        Get-ReceiveConnector "ServerName\Default Frontend ReceiveConnector" | Set-ReceiveConnector -TlsCertificateName $TLSCert


Graph API – Search AuditLogs\SignIns for users to see who has used MFA within 30 Days


Scenario: You want to query the Azure AD SignIn Logs to see who has used MFA within the last 30 days via Graph and PowerShell. You have the Userprincipalnames in a CSV already.

Scriptlet:
Notes:
1. It performs a get-accesstoken function which can be found and loaded from this blog: Get an Access Token for Graph API via PowerShell – Ex-Shell


   #Declare global variables
   $i = import-csv "C:\temp\userprincipalnames.csv"  #With Userprincipalname as the column header 
   $start = get-date((get-date).adddays(-30)) -Format "yyyy-MM-dd"

   #Loop it
    $i.userprincipalname | Sort | %{
        #Declare UPN
        $n = $_
        "Checking $N"

         #Build the URI   
         $appuri = "https://graph.microsoft.com/v1.0/auditlogs/signIns?$('$filter')=(userprincipalname eq '$n') and (createdDateTime ge $start)"
         $appuri = ([System.Uri]$appuri).AbsoluteUri

        #Get the token and create the RestSplat
        $header = get-accesstoken 
        $results = @()
        $RestSplat = @{ 
            URI         = $appuri
            Headers     = $header
            Method      = 'GET' 
            ContentType = "application/json" 
            } 

        #Invoke the Rest URI
        $Tempresults =  Invoke-RestMethod @RestSplat


        #Play with results
            #MFA check    
            $Tempresults.value.appliedConditionalAccessPolicies | Where {($_.result -eq "Success") -and ($_.enforcedGrantControls -like "*MFA*")}

            #Signin at all?
           $Tempresults.value

    }


Graph API via PowerShell: Find Sign In Logs for Specific user and time

Scenario: You want to use Graph API to query the SignIn logs.

Scriptlet:
Notes:
1. It performs a get-accesstoken function which can be found and loaded from this blog: Get an Access Token for Graph API via PowerShell – Ex-Shell


#Sign-In Logs Varaibles, edit below:
$Start = "2021-10-01"
$End = "2021-10-05"
$appuri = "https://graph.microsoft.com/v1.0/auditlogs/signIns?$('$filter')=(userprincipalname eq 'steveman@superhero.com') and (createdDateTime ge $start) and (createdDateTime lt $end)"

#Clean the URI
$appuri = ([System.Uri]$appuri).AbsoluteUri

#Loop it for ALL SignIn logs
Do{
"$appUri"
$header = get-accesstoken 
$results = @()
$RestSplat = @{ 
    URI         = $appuri
    Headers     = $header
    Method      = 'GET' 
    ContentType = "application/json" 
} 
$Tempresults =  Invoke-RestMethod @RestSplat 
$results += $tempresults.value
$appuri = $tempresults."@odata.NextLink"
}While($appuri -ne $null)

#
$results |Select CreatedDateTime,ClientAppUsed,userprincipalname

Graph API via PowerShell: Find Exchange Custom Attributes (or AD Extension Attributes) for a user

Scenario: Using Graph API via PowerShell, you can run the following to build and execute a URI to find the Exchange Custom Attributes (or AD Extension Attributes), or other information, for a user:

Scriptlet:
Note: Prerequisite: It performs a get-accesstoken function which can be found and loaded from this blog: Get an Access Token for Graph API via PowerShell – Ex-Shell

#Build the URI
$appuri = 'https://graph.microsoft.com/v1.0/users/SteveMan@SuperHero.com?$Select=id,displayname,mail,officeLocation,onPremisesExtensionAttributes'

$Execute the URI
$appuri = ([System.Uri]$appuri).AbsoluteUri
$header = get-accesstoken
$results = @()
$RestSplat = @{ 
    URI         = $appuri
    Headers     = $header
    Method      = 'GET' 
    ContentType = "application/json" 
} 
$Tempresults =  Invoke-RestMethod @RestSplat 


#Display the Results
$TempResults

#Display the ExtensionAttributes

$tempresults.onPremisesExtensionAttributes

Get an Access Token for Graph API via PowerShell

Scenario: You want to connect to Microsoft Graph API via PowerShell to pull in data so you can use it within PowerShell.

How to get an Azure Token:
First, this is an awesome article walking through A-Z on how to do this.
https://tech.nicolonsky.ch/explaining-microsoft-graph-access-token-acquisition/

Notes:
1. Make sure you give your Azure App the correct permissions for everything it will need.
2. I created a function to get the Access Token so I can call it anytime, see below. This may be a little older than what the article above references
3. When you can, use Certificates for authentication.

Function:
Edit the top 3 values; TenantName, AppID, and Certificate

function Get-AccessToken {

        $TenantName = "<whatever your tenant name is>"
        $AppId = "<whatever your app id is>"
        $Certificate = <Whatever your cert is,  example:  Get-Item Cert:\LocalMachine\My\CE0XXXXXXXXXXXXXXXXXXXXXXX>


#Acquire an Access Token (Using a Certificate) with Java Web Token (JWT)

$Scope = "https://graph.microsoft.com/.default"




# Create base64 hash of certificate
$CertificateBase64Hash = [System.Convert]::ToBase64String($Certificate.GetCertHash())

# Create JWT timestamp for expiration
$StartDate = (Get-Date "1970-01-01T00:00:00Z" ).ToUniversalTime()
$JWTExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End (Get-Date).ToUniversalTime().AddMinutes(2)).TotalSeconds
$JWTExpiration = [math]::Round($JWTExpirationTimeSpan,0)

# Create JWT validity start timestamp
$NotBeforeExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End ((Get-Date).ToUniversalTime())).TotalSeconds
$NotBefore = [math]::Round($NotBeforeExpirationTimeSpan,0)

# Create JWT header
$JWTHeader = @{
    alg = "RS256"
    typ = "JWT"
    # Use the CertificateBase64Hash and replace/strip to match web encoding of base64
    x5t = $CertificateBase64Hash -replace '\+','-' -replace '/','_' -replace '='
}

# Create JWT payload
$JWTPayLoad = @{
    # What endpoint is allowed to use this JWT
    aud = "https://login.microsoftonline.com/$TenantName/oauth2/token"

    # Expiration timestamp
    exp = $JWTExpiration

    # Issuer = your application
    iss = $AppId

    # JWT ID: random guid
    jti = [guid]::NewGuid()

    # Not to be used before
    nbf = $NotBefore

    # JWT Subject
    sub = $AppId
    }


# Convert header and payload to base64
$JWTHeaderToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json))
$EncodedHeader = [System.Convert]::ToBase64String($JWTHeaderToByte)

$JWTPayLoadToByte =  [System.Text.Encoding]::UTF8.GetBytes(($JWTPayload | ConvertTo-Json))
$EncodedPayload = [System.Convert]::ToBase64String($JWTPayLoadToByte)

# Join header and Payload with "." to create a valid (unsigned) JWT
$JWT = $EncodedHeader + "." + $EncodedPayload

# Get the private key object of your certificate
$PrivateKey = $Certificate.PrivateKey

# Define RSA signature and hashing algorithm
$RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1
$HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256

# Create a signature of the JWT
$Signature = [Convert]::ToBase64String($PrivateKey.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT),$HashAlgorithm,$RSAPadding)) -replace '\+','-' -replace '/','_' -replace '='

# Join the signature to the JWT with "."
$JWT = $JWT + "." + $Signature

# Create a hash with body parameters
$Body = @{
    #client_id = $AppId
    #client_id = $ClientAppId
    client_assertion = $JWT
    client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
    scope = $Scope
    grant_type = "client_credentials"

}

$Url = "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token"

# Use the self-generated JWT as Authorization
$Header = @{
    Authorization = "Bearer $JWT"
}

# Splat the parameters for Invoke-Restmethod for cleaner code
$PostSplat = @{
    ContentType = 'application/x-www-form-urlencoded'
    Method = 'POST'
    Body = $Body
    Uri = $Url
    Headers = $Header
}
$Request = Invoke-RestMethod @PostSplat

# Create header
$Header = @{
    Authorization = "$($Request.token_type) $($Request.access_token)"
}


return $header
         
       
   }

Cannot delete a Exchange Database because “This mailbox database contains one or more mailboxes, mailbox plans, archive mailboxes, public folder mailboxes or arbitration mailboxes, Audit mailboxes”

Scenario: When attempting to remove an Exchange database, you receive the following error:

This mailbox database contains one or more mailboxes, mailbox plans, archive mailboxes, public folder mailboxes or
arbitration mailboxes, Audit mailboxes.


Scriptlet: Here is a quick script to check that database for mailbox or mailbox data that may be active on it:

$Db = “DB01”
Get-Mailbox -Database $DB
Get-MailboxPlan
Get-Mailbox -Database $DB -Archive
Get-Mailbox -Database $DB -PublicFolder
Get-Mailbox -Database $DB -Arbitration.
Get-Mailbox -Database $DB -AuditLog
Get-MoveRequest |Where {($_.TargetDatabase -eq $db) -or ($_.SourceDatabase -eq $DB)}



#GoRavens

Check Microsoft, Security and Compliance, Azure, and Exchange Role Membership

Scenario: You want to quickly gather membership information on all of Microsoft Online, AzureAD, and Exchange Online Roles.

Scriptlets:

Check Microsoft Online for Role Membership

#Connect to Microsoft Online: Connect-MSOLService
$MSrole_user = @()
$MSroles = Get-MsolRole
$MSroles | Sort name | %{
$n = $_.name
“Checking Role: $n”
$MSrole_User += Get-MsolRoleMember -RoleObjectId $_.ObjectId | Select @{Name=”RoleName”;Expression={“$n”}},DisplayName, EmailAddress,RoleMemberType
}
$MSRole_User


Check Azure AD for Role Membership

#Connect to Azure AD: Connect-AzureAD
$AZrole_user = @()
$AZroles = Get-AzureADDirectoryRole
$AZroles | Sort DisplayName | %{
$n = $_.DisplayName
“Checking Role: $n”
$AZrole_User += Get-AzureADDirectoryRoleMember -ObjectId $_.ObjectId | Select @{Name=”RoleName”;Expression={“$n”}},DisplayName,ObjectType,Mail,SecurityEnabled
}
$AZRole_User

Check for Security and Compliance Role Membership

#Connect to IPPS Sessions Online: Connect-IPPSSession
$SCrole_user = @()
$SCroles = Get-RoleGroup
$SCRoles | Sort Name | %{
$n = $_.name
“Checking Role: $n”
$SCRole_User += get-rolegroupmember $N | select @{Name=”RoleName”;Expression={“$N”},name,windowsliveid
}
$SCRole_User

Check Exchange Online Online

#Connect to Exchange Online: Connect-ExchangeOnline
$Exchange_Roles = get-managementroleassignment -geteffectiveusers
$exchange_Roles_Unique = $Exchange_Roles | Select RoleAssigneeName,EffectiveUserName
$Exchange_Roles_Unique = $exchange_Roles_Unique | Select -Unique RoleAssigneeName,EffectiveUserName
$Exchange_Roles_Unique

Another DoWhile Loop method for performing an action on large datasets in smaller subsets

Scenario: You have a large data set, maybe a large amount of mailboxes, and you need to set the RoleAssignmentPolicy to a policy that does not allow email forwarding.

Solution:

Collect Mailboxes

$mbx = get-mailbox -resultsize unlimited | where RoleAssignmentPolicy -ne PolicyWithNoEmailForward

Create DoWhile Variables

$start = 0
$inc = 10
$end = $inc
$totalCount = $mbx.count

Loop It

Do{
“Running on $start..$end out of $totalCount”
$mbx[$start..$end] | Set-mailbox -RoleAssignmentPolicy PolicyWithNoEmailForward
$start = $start + $inc
$end = $end + $inc
}While($start -le $totalCount)

Use Paging within a Select statement when running PowerShell commands to avoid Throttling

Scenario: You have a large dataset that you want to use a Do-While Loop with a Paging system to separate the data into smaller data sets. In this example, I will use a Do-While loop to set a roleassignmentpolicy against may mailboxes:

Solution: Run the following commands

Collect the Mailboxes

$mbx = get-mailbox -resultsize unlimited | where RoleAssignmentPolicy -ne “PolicyWithNoEmailForward”

Create the Page Variables

$total_Count = $mbx.count
$Sleep = 60 #in seconds
$page = 10 #the amount of records you want to go through per set
$page_Count = $page #The amount the page will increase

Buffer for DoWhile Loop

$mbx | Select -first $page | %{
$N = $_.name
“Setting RoleAssignmentPolicy on Mbx: $N”
Set-mailbox $n -roleassignmentPolicy “PolicyWithNoEmailForward”
}

Run the DoWhile Loop

Do{

#Run through the first set of Mailboxes
$mbx | Select -first $page -skip $page | %{
    $N = $_.name
    "Setting RoleAssignmentPolicy on Mbx: $N" 
    Set-mailbox $n -roleassignmentPolicy "PolicyWithNoEmailForward"
  }

#Display a separator between each set
 "

  Next"

#Increase the Page and SKip Count
  $Page = $page + $page_Count

#Sleep Duration
  Sleep $Sleep

} While($Page -lt $Total_Count)