Pages

Tuesday, July 4, 2023

Creating a Logic App that retrieves AAD sign-in events from Log Analytics and sends a report in an email with a CSV attachment and HTML table insert

Two of the common questions I’ve been asked since publishing the following post over a year ago:

Monitoring, Alerting, Reporting Azure AD logins and login failures with Log Analytics and Logic Apps

http://terenceluk.blogspot.com/2022/02/monitoring-alerting-reporting-azure-ad.html

… is whether there was a way to:

  1. Provide the report as a CSV attachment
  2. Pretty up the table that is inserted into the email

Providing the report as a CSV attachment is fairly easy but making the Html table more aesthetically pleasing wasn’t. After trying a few methods and not being very successful, I ended up landing on using an Azure Function App that takes the report in JSON format, create the HTML formatted table with colour output, then return it back to the Logic App. The method isn’t very efficient but provides the desired result so this post serves to demonstrate the configuration.

The screenshot below, shows two reports and emails sent out in the Logic App flow. The first Run query and visualize results and Send an email (V2) that is highlighted in red is what my previous post demonstrated, and it sends out an email that contain a plainly formatted HTML table in an email. The second Run query and list results, Create blob (V2), Convert JSON to HTML, Delete blob (V2), Initialize Variable, Set Variable, Create CSV table, Send an email (V2) 2 that I highlighted in green are the additional steps to create a CSV file with the report and send an email with a coloured HTML table:

image

Step #1 - Create Storage Account

While it is possible to send the full JSON directly to a Function App’s HTTP Trigger, logs exceeding the maximum size would fail so I opted to first create a JSON file and temporarily place it onto a Storage Account container so it can be retrieved by a Function App for processing. The storage of the JSON can be permanent as well but most environments I work with typically sends AAD logs to a storage account for audit retention so this design will only have the file stored for processing then deleted after.

Begin by creating a Storage Account and a container that will store the JSON file. For the purpose of this example, the container will be named: integration

image

The Function App that will be created will temporarily place a file similar to the one shown in this screenshot:

image

Due to the sensitivity of data, we want to ensure that the container is not publicly assessable so the Public access level should be configured as Private (no anonymous access):

image

For improved security, I always prefer to disable Allow storage account key access (Shared Key authorization) and use Azure Active Directory (Azure AD) for authorization. The method in which the Function App will securely access the Storage Account container is through a managed identity maintained by AAD so unless there is a need to allow shared key authorization, we can go ahead and disable it:

image

You’ll notice that browsing into the container through the will now require the Authentication method to be configured as Azure AD User Account:

image

Step #2 - Create Azure Function App

With the storage account created, we can proceed to create the Azure Function App that will be triggered via HTTP with the URL of the JSON file passed to it.

image

Create a new function of the type HTTP Trigger:

image

Open the function, navigate to Code + Test and paste the code from my GitHub repo into the function: https://github.com/terenceluk/Azure/blob/main/Function%20App/JSON-To-HTML-Function.ps1

image

Notable items in the code are the following:

  1. The container name is extracted from the full path to the JSON file with Regex
  2. The blob and storage account name are extracted from the full path to the JSON file with substring and indexOf method
  3. The function app expects the full URL path to be passed as a JSON like the following:

{

"body": "https://rgcacinfratemp.blob.core.windows.net/integration/AD-Report-06-29-2023.json"

}

Another way for defining the storage account and container name for the function app is in the Application settings but this hardcodes the value and requires updating:

image

The function app uses two Az modules to authenticate as a managed identity and retrieve the JSON file. Rather than loading the full Az module, which I have never had any luck because the amount of time it requires to be downloaded causes my function apps to time out, we will only load Az.Accounts and Az.Storage. Proceed to navigate to the App files blade, open the requirements.psd1 and edit the file as such:

# This file enables modules to be automatically managed by the Functions service.

# See https://aka.ms/functionsmanageddependency for additional information.

#

@{

# For latest supported version, go to 'https://www.powershellgallery.com/packages/Az'.

# To use the Az module in your function app, please uncomment the line below.

# 'Az' = '10.*'

'Az.Accounts' = '2.*'

'Az.Storage' = '4.*'

}

image

I’ve ran into scenarios where the modules do not get downloaded or loaded properly and the way I typically troubleshoot the issue is to navigate into Kudo for the function app to check the downloaded or not downloaded modules via the URL:

https://json-to-html-converter.scm.azurewebsites.net/

imageimage

Once the function app code has been saved and configuration updated, proceed to navigate to the Identity blade and turn on system managed identity:

image

Step #3 - Create Logic App

One of the key differences between the plain table report and the new report is that the old one uses Run query and visualize results to query Log Analytics or the report details, while the new report uses Run query and list results to query Log Analytics for the data. The Run query and visualize results action provides output options:

  • Html Table
  • Pie Chart
  • Time Chart
  • Bar Chart
image

In order to generate an output that will allow us to create a customized Html table and CSV file, we would need to use Run query and list results action that generates a JSON file. This JSON file will allows us to create a blob on a storage account container that will be used to generate a customized Html table, as well as create a CSV file:

image

We want to create the JSON file with a meaningful name so we’ll be using the concat function to name the file:

concat(‘AD-Report-‘,formatDateTime(utcNow(), ‘MM-dd-yyy’),’.json’)

This expression will generate a file with the name AD-Report-<today’s date>.json

The blob content will be provided by the results from the Run query and list results action.

image

Once the JSON file with the AAD logs is created and placed into a storage account, the Logic App will call an Azure Function App and pass the full URL path so the Function App can retrieve the retrieve the JSON file, format the data into a Html table, then return it to the Logic App. Upon receiving the Html formatted results, the Logic App will then delete the log file. The remaining 2 steps after obtaining the properly formatted Html code is to create and set a variable so it can be used to send the logs as a table.

image

With the Html email reported ready, we will then use the Create CSV table action to create a CSV file from the Run Query and List Results action and send the email:

image

The following is a screenshot of how the email is composed with the EmailBody variable containing the HTML content, attaching the CSV table as an attachment and provide it the same name format:

image

Once the Logic App has been saved, proceed to navigate to the Identity blade and turn on system managed identity:

image

Step 4 – Assign managed identity for the Function App and Logic App permissions to the Storage Account

The last step is to grant the managed identities the appropriate permissions to the storage account.

The Azure Function App will only need Storage Blob Data Reader because it will only need to retrieve the JSON file.

The Logic App will need Storage Blob Data Contributor because it will need to write the JSON file to the storage account and then delete it afterwards.

image

Step 5 – Test Report

Proceed to run the Logic App and the following report should arrive in the configured mailbox:

image

Note that the CSS nth-child selector for even and odd rows does not work with Outlook so while the Html generated would display alternating blues for rows as shown in the screenshot below, the report sent in Outlook would not be the same.

image

Troubleshooting

The following PowerShell script can call the Function App directly if the Logic App does not generate the report and you want to troubleshoot by calling the Function App directly.

GitHub: https://github.com/terenceluk/Azure/blob/main/Function%20App/Test-Calling-API.ps1$Body = @{

path = https://storageAccountName.blob.core.windows.net/integration/AD-Report-06-28-2023.json

}

$Parameters = @{

Method = "POST"

Uri = "https://youFunctionName.azurewebsites.net/api/Converter?code=xxxxxxxxxxxxm_Dnc_avHxxxxxxxxxxxxxxDH1A=="

Body = $Body | ConvertTo-Json

ContentType = "application/json"

}

Invoke-RestMethod @Parameters | Out-File "C:\Temp\Call-API.html"

The Function App URI can be located in the field shown in the screenshot below:

image

Friday, June 23, 2023

Useful Kusto Query / KQL queries for Azure Firewall Troubleshooting

I do not often have the opportunity to do as many hands on deployment of Azure services on projects due to my role as an architect so when I do, I tend to spend a lot of time working with the service to try and understand the ins and outs of the product. One of my recent projects provided me the opportunity to deploy the Azure firewall that I designed and I noticed that there weren’t many Kusto query examples available for troubleshooting inbound and outbound traffic so I wanted to post a link to my GitHub repo where I have and continue to build upon KQL queries for querying Azure Firewall logs to monitor traffic: https://github.com/terenceluk/Azure/blob/main/Kusto%20KQL/Azure-Firewall.kusto

I tried to demonstrate as many customizations such as time zones, days ago, start and end time, variables that allowed these basic KQL queries to help me troubleshoot all the Teams outbound traffic that were being blocked as well as weekly reporting I needed to deliver to the client. Hope this helps anyone who might be looking for example queries and can use these as a start.

Microsoft Teams audio calling fails with the error: “We ran into a problem – Try again in a few minutes” on Azure Virtual Desktop with Teams Media Optimization

One of the issues I recently encountered during a Azure Virtual Desktop deployment with Teams Media Optimization was where outbound calls from the virtual desktop would display the spinning wheel while constantly playing the dialing audio until the call fails with:

We ran into a problem
Try again in a few minutes

image

image

I wasn’t sure if it was the ordering of the software installation or the Remote Desktop Client app and after reinstalling all the components yet still receiving this error, I remembered that the profile cache may be the cause so went ahead and navigated to %appdata%\Microsoft\Teams to delete the files:

image

Then tried dialing again and this corrected the issue. The issue took a bit if time to resolve so I hope this short blog post will help anyone who may encounter this problem.

Thursday, June 22, 2023

Azure Virtual Desktop Teams Media Optimization fails to display local client devices

I’ve configured quite a few Teams Media Optimization with Azure Virtual Desktop as per the following Microsoft documentation in the past:

Use Microsoft Teams on Azure Virtual Desktop
https://learn.microsoft.com/en-us/azure/virtual-desktop/teams-on-avd

The configuration isn’t difficult and I never had any issues until recently when I had to repeat the same for an environment I worked on. After performing all the steps, I noticed that the settings in Teams would either display:

Audio Devices: Custom Setup
Speaker: None
Microphone: None

Which means no devices are redirected or optimized:

image

Or:

Audio Devices: Custom Setup
Speaker: Remote Audio
Microphone: Remote Audio
Camera: Integrated Camera (Redirected)

Which means redirected audio and video devices were taking place but not optimization.

**Note that redirect works if these RDP settings are configured:

audiocapturemode:i:1 Enable audio capture from the local device and redirection to an audio application in the remote session

audiomode:i:0 Plays sound on the local computer

camerastoredirect:s:* Redirect cameras

image

After going through all the steps multiple times and not having any luck, I recalled a long time ago when I experienced an issue where if I had logged into Teams on an Azure Virtual Desktop BEFORE configuring Microsoft Teams Media Optimization, the optimization would fail. This generally wasn’t an issue for me as I always configure the optimization before rolling out the desktops but for this instance I had not so I went into the folder %appdata%\Microsoft\Teams to delete all the items and long behold it corrected the issue.

image

I haven’t encountered this issue as much but this took up quite a bit of my time to troubleshoot so I hope others with this issue will find this post and be able to resolve it quicker.

The versions of the applications I used for this deployment are:

Microsoft Teams: 1.6.00.11166

Remote Desktop WebRTC Redirector Service: 1.33.2302.07001

Microsoft Remote Desktop: 1.2.4337.0 (x64)

Microsoft Visual C++ 2015-2022 Redistributable (x64): 14.36.32532.0

Tuesday, June 13, 2023

Designing Azure Storage Account Regional Failover with Private Endpoints

I’ve had the opportunity to work on several projects over the past year to design disaster recovery to recover from one Azure region to another. One of the most common topics that comes up is how to handle storage accounts that are accessed through private endpoints and have public endpoints disabled:

image

image

The purpose of this blog post is to provide a walkthrough of possible methods to design regional failover with private endpoints.

Sample Environment

Take the following topology as an example:

image

In this topology, we have a storage account in the East US region that is configured with Read-access geo-redundant storage (RA-GRS) so all data written to it will automatically get written to the paired region in West US:

image

Since Read Access is configured, a secondary endpoint is available for read access on the replicated copy in the secondary region:

image

A private endpoint is provisioned in the East US region so the vm-east-us-prod virtual machine can access the storage account privately from its subnet 10.1.0.4 to the private endpoint at 10.1.2.4 within the vnet-east-us VNet:

image

Although a secondary endpoint is available, this should not be mistaken for an endpoint that can be used for DR purposes because it allows for read access via the public endpoint to the replicated copy in West US during normal operation.

Notice that there is a pre-deployed virtual machine in the West US region that serve to provide continue operation of access to the storage account in the event where the East US region is unavailable. This type of very common for most environments as a DR failover region is typically pre-staged with networks that serve to host resources to continue operations in the event where the primary region is down.

Scenario #1 – Shared Private DNS Zone for Primary and Secondary Regions

One common design that can be used between two regions is where the Private DNS Zone is shared between the VNets in the two regions. This configuration allows for both VNets to use the same DNS zone for name resolution and therefore will resolve the same private IP address configured for the private endpoint in the primary region providing access to the storage account:

image

In the diagram above, the secondary region’s virtual machine is placed in a VNet that linked to the same private DNS zone:

image

It is important to note that the reason why we are able to link the two VNets to the same Private DNS Zone is because these are Global resources even though it is placed in a regional resource group:

image

This type of configuration means that attempting to resolve easteusblobprod.privatelink.blob.core.windows.net in both regions will direct the traffic to the private endpoint deployed in East US and since the two regions have Global VNet Peering configured, the West US traffic will traverse through that connection to the East US region.

image

In the event where the storage account is unavailable in East US or it has been manually failed over to the West US region, traffic will continue to be directed to the private endpoint in East US, then sent over a private link to the failed over storage account in the West US region, which now has become LRS (Locally-redundant storage):

image

image

Such a design unfortunately would not provide the required access in the event of an East US regional failure because the primary private endpoint will no longer be available if East US becomes unavailable:

image

A common design is to have a DR runbook that performs the following in the event of a regional failure:

  1. Provision a new private endpoint in the West US region
  2. Update the Private DNS Zone’s record to direct traffic to the new private endpoint
image

This type of design requires manual steps to be executed but saves cost in the disaster recovery region because while private endpoints costs $0.014 (CAD) per hour, which equates to around $10.22/month, larger environments can have many private endpoints and the charges for resources that are not actively used isn’t well received by organizations. Environments leveraging automation using Infrastructure as Code are great candidates for this type of design as the resources and changes can be executed with little manual labour. Furthermore, disaster recovery solutions are not always automatically invoked so having to provision private endpoints in the event of a catatrophic event is not uncommon. An example of this could be leveraging Azure Site Recovery to recover VMs with its recovery plan capability to execute Azure Automation runbooks.

Scenario #2 – Separate Private DNS Zone for Primary and Secondary Regions

If there is a desire to pre-provision all resources to either fully automate or reduce the amount of manual labour involved in the event of a DR, it is possible to provision a private link in the disaster recovery West US region that is linked to the storage account. The important design change here is that a second private DNS zone is created for the DR region and linked to the VNet as shown in the diagram below:

image

Notice that the pre-provisioned private link will now allow the virtual machine in the West US DR region to access the storage account through a private link rather than the global VNet peering. I won’t go into the details but I have had cross-region active/active deployments configured with such a design.

Here is how the configuration would look like in the Azure portal:

image

image

image

image

With the above design, a regional loss will require no manual configuration to access the storage account failed over to West US:

image

In summary, this design removes the requirement for provisioning a private endpoint and updating DNS in the event of a disaster recovery. However, this does incur additional cost as well as maintaining multiple private DNS zones that are associated to the different VNets in each region. There will also be additional considerations required when there is an on-premise hybrid cloud connectivity to the Azure regions and traffic originating outside of Azure needs to reach the private endpoint.

Hope this gives the reader a good idea about the designs available for providing private endpoint connectivity in the event of a disaster recovery.