Skip to main content

Web Content Not Found on Azure Storage Account

· 2 min read

Azure Storage accounts can host static websites by opening up a public endpoint to an Azure storage container ($web), so anything inside of $web will be accessible publicly.

This can be enabled easily by toggling the Static website to Enabled.

Azure Storage Account - Static website

Once enabled, the Azure storage account will add a NEW endpoint - <storageaccname>.z*.web.core.windows.net.

Once you have enabled the static website functionality, a new container named: $web will be created; this is the root of your static website – and where your HTML or static website will go.

After you upload your website files to the $web folder.

Azure Storage Account - $web container

Add the index document name (i.e., index.html) and click Save.

Azure Storage Account - Static Website primary endpoint

If done correctly, your website should now show your website.Azure Storage account static websitev

If done incorrectly, you may get: The requested content does not exist.

The requested content does not exist

If this occurs, make sure:

  • There is no whitespace in the index document name.

Azure storage account - index.html

  • The Case matters, make sure if the filename is all lowercase in the container, then it’s all lowercase in the Azure storage account static website configuration.
  • Define a 404 page (the page that gets loaded) when attempting to browse paths that don’t match the index - make sure the site exists in a container and is added to the site storage account configuration, like the index document name.

404.html

  • If you don’t have a 404 page, you can have index.html as both.

Azure static web site - filenames

  • If you have a CDN (Content Delivery Network) in front of your Azure Storage account (Azure CDN, Cloudflare), you may need to adjust the access level of your Container from Private to: Blob (Anonymous). You shouldn’t have to adjust this usually, as the Access level controls the container endpoint access – not the static website endpoint.

Azure storage account - blob access level

Open multiple Microsoft Teams instances using PowerShell

· 2 min read

There may be circumstances, you need to open up multiple Microsoft Team instances, a reason for this - maybe to chat and join meetings across multiple accounts.

Microsoft are working on a version of Microsoft Teams that supports multiple-accounts, but until thats released - you can use a PowerShell script to open up another version of Microsoft Teams in another profile (or multiple, if you update the profilename).

This script also works within your LocalAppData, so you don't need local administrator rights to run.

# Uses the file name as the profile name
$MSTEAMS_PROFILE = 'CustomProfile'

Write-Host "- Using profile '$MSTEAMS_PROFILE'"

# Set the custom profile path
$USERPROFILE = Join-Path $env:LOCALAPPDATA "Microsoft\Teams\CustomProfiles\$MSTEAMS_PROFILE"

# Set the old user profile
$OLD_USERPROFILE = $env:USERPROFILE

# Launch MS Teams with the custom profile
Write-Host "- Launching MS Teams with profile '$MSTEAMS_PROFILE'"
Set-Location "$OLD_USERPROFILE\AppData\Local\Microsoft\Teams"

$teamsProcessStartInfo = New-Object System.Diagnostics.ProcessStartInfo
$teamsProcessStartInfo.FileName = "$OLD_USERPROFILE\AppData\Local\Microsoft\Teams\Update.exe"
$teamsProcessStartInfo.Arguments = "--processStart ""Teams.exe"""
$teamsProcessStartInfo.WorkingDirectory = "$OLD_USERPROFILE\AppData\Local\Microsoft\Teams"
$teamsProcessStartInfo.EnvironmentVariables["USERPROFILE"] = $USERPROFILE
$teamsProcessStartInfo.UseShellExecute = $false

[System.Diagnostics.Process]::Start($teamsProcessStartInfo) | Out-Null

# Set the user profile back to the old user profile
$env:USERPROFILE = $OLD_USERPROFILE

When the script is ran, a new profile will be created for Microsoft Teams, and then opened. You can then use that second Microsoft Teams instance, to connect to another account or tenancy.

To make it easier, you could also look at turning this script into an executable.

Failed to persist Terraform state using an Azure Blob Storage account

· 2 min read

When attempting to make changes with Terraform, and the state changes are in an Azure storage account, you may come across: Failed to save state.

Error: Failed to save state

Error saving state: blobs:Clien#GetProperties: Failure responding to request: StatusCode=403 -- Original Error: autorest/azure: error response cannot be parsed: {"" '\x00' '\x00'} error: EOF

And: Error: Failed to persist state to backend.

Or Error: Error releasing the state lock.

Terraform - Failed to save state

Recently, encountered this issue when attempting a Terraform deployment; the state file kept locking midway through a deployment, this was traced to the Terraform storage account being in the Terraform code itself - with Public access set to Deny.

So the flow was looking like this: Script ran to allow Azure DevOps IP to the Storage account Firewall, then Terraform would start deploying and Deny access - preventing the state file from saving.

The first thing you need to do is break the lease on the state file.

  1. Navigate to your Azure Storage account that contains the state file
  2. Navigate to the Container that contains the state file
  3. Click on your state file and select Break lease
  4. Azure Storage account - break lease
  5. Once the lease is broken - make sure that your state file is 'Available' and not 'Leased'
  6. Then check your Terraform to make sure that it wasn't changing your Storage account that contained the Terraform state file in any way, then re-run your deployment.

Note: It may be wise, to make sure that the Azure storage account containing your state file is not managed by Terraform, to avoid unintentional mishaps.

Azure Quick Review

· 2 min read

There are a lot of workbooks that help with Microsoft Azure cost optimization, but when having discussions and looking into SLA/SLO and availability scenarios, there are fewer options to select from - today, we are going to look at the deployment and output of Azure Quick Review.

Azure Quick Review (azqr) goal is to produce a high level assessment of an Azure Subscription or Resource Group providing the following information for each Azure Service:

Azure Quick Review (created by Microsoft Senior Cloud Solution Architect Carlos Mendible), can supplement other tools - to give you visibility into your Azure services and answer questions such as:

  • What is my expected SLA?
  • Are my resources protected against zone failures?
  • Am I collecting diagnostic logs for my resources?
  • Is Defender for Cloud-enabled for all my resource types?

Using this tool is pretty simple (and, as the name suggests, Quick), and today we will look at running it from a windows endpoint, but first, we need some prerequisites.

Install the Azure CLI and make sure you have Reader rights across the subscriptions you want to review; in this demo, we will scan all subscriptions I have access to.

The Azure Quick Review (azqr) windows binary is intended to be run from the command line, so let's run it.

  1. Open your Windows Terminal

  2. Navigate to the location of the azqr binary

  3. Azure Quick Review

  4. Login to Azure using the Azure CLI by typing:

    az login
  5. Once you have authenticated, run the executable.

  6. Run azqr-windows-latest

  7. Once it has been completed, there will be an excel spreadsheet in the same folder as the Azure Quick Review executable, with an output that contains something similar to the below:

  8. Azure Quick Review - Overview

  9. Azure Quick Review - Recommedations

  10. Azure Quick Review - Defender for Cloud

Create Azure Bastion with Shareable Link support with PowerShell

· 9 min read

Azure Bastion is a service you deploy that lets you connect to a virtual machine using your browser and the Azure portal or via the native SSH or RDP client installed on your local computer.

Overview

The Azure Bastion service is a fully platform-managed PaaS service you provision inside your virtual network. It provides secure and seamless RDP/SSH connectivity to your virtual machines directly from the Azure portal over TLS.

Because of this, if you don't have line-of-sight access to your Virtual Machines (via express route, Site-to-Site VPN etc.), Bastion becomes your jump box, allowing secure access to your virtual machines without needing a public IP.

There is a downside, though. To connect to a Virtual Machine secured by Bastion, you need access to the Azure Portal, or command line connectivity to Azure, to create the tunnel; this means that you may need to grant people elevated rights and access they may not need to connect.

As of November 2022, Microsoft introduced shareable links into public preview, solving two key pain points:

  • Administrators will no longer have to provide full access to their Azure accounts to one-time VM users—helping to maintain their privacy and security.
  • Users without Azure subscriptions can seamlessly connect to VMs without exposing RDP/SSH ports to the public internet.

The Bastion Shareable Link feature lets users connect to a target resource (virtual machine or virtual machine scale set) using Azure Bastion without accessing the Azure portal.

At the time of this writing, there are some scenarios where shareable links won't work - particularly across Network peering across subscriptions and regions.

Because the service is in Public Preview - native PowerShell cmdlet support, enabling and configuring this feature isn't available - but you can easily allow it via the Azure Portal.

Create Azure Bastion with Shareable Link Support

To get around that, we will leverage the Azure Rest API directly, using PowerShell to enable the Shareable Link feature and create and obtain a shareable link for a Virtual Machine.

Create Azure Bastion

I will assume there is already an Azure Virtual Network created; if not, you can follow the Microsoft documentation to get it up and running!

Also, make sure you have the Az Module installed.

The PowerShell function we will run will require a few parameters to create the Azure Bastion resource and enable Shared Link functionality; these parameters are:

ParametersNote
RGNameThe Resource Group of your Virtual Network
VNetNameThe Virtual Network name
addressPrefixThe address prefix for your new Bastion subnet. For Azure Bastion resources deployed on or after November 2, 2021, the minimum AzureBastionSubnet size is /26 or larger (/25, /24, etc.).
regionThe region, that Azure Bastion is deployed into (this needs to match your Virtual Network)
BastionPubIPNameThe name of the Public IP, used by the Azure Bastion resource (this is the Azure resource name, it doesn't have an external DNS alias, so doesn't need to be globally unique)
BastionResourceNameThe name of your Azure Bastion resource
  1. Copy the script below into a file named: New-AzBastionSharedLinkEnabled.ps1

    function New-AzBastionSharedLinkEnabled {
    <#
    .SYNOPSIS
    Creates an Azure Bastion resource with shared link enabled, on an already existing Azure Virtual Network.
    #>
    [CmdletBinding()]
    param
    (
    [Parameter(Mandatory = $false, Position = 0)]
    [System.String]
    $RGName = "BastionTest",

    [Parameter(Mandatory = $false, Position = 1)]
    [System.String]
    $VNetName = 'vnet-aue-dev',

    [Parameter(Mandatory = $false, Position = 2)]
    [System.String]
    $addressPrefix = '10.2.1.0/26',

    [Parameter(Mandatory = $false, Position = 3)]
    [System.String]
    $region = 'AustraliaEast',

    [Parameter(Mandatory = $false, Position = 4)]
    [System.String]
    $BastionPubIPName = 'VNet1-ip',

    [Parameter(Mandatory = $false, Position = 5)]
    [Object]
    $BastionResourceName = "$VNetName-bastion"
    )

    # Set variable values for Resource Group name, Virtual Network name, address prefix, region, and bastion-related resources.

    # Connect to Azure using Get-AzAccount cmdlet.
    Connect-AzAccount

    # Use Get-AzSubscription cmdlet to get all the subscriptions that the account has access to and allow the user to choose one using Out-GridView.
    Get-AzSubscription | Out-GridView -PassThru | Select-AzSubscription
    $token = (Get-AzAccessToken).Token
    $subscription = Get-AzContext | Select-Object Subscription

    # Use Get-AzVirtualNetwork cmdlet to get the virtual network object and then use Add-AzVirtualNetworkSubnetConfig cmdlet to create a new subnet for Azure Bastion service. Finally, use Set-AzVirtualNetwork cmdlet to update the virtual network configuration.
    $VNET = Get-AzVirtualNetwork -ResourceGroupName $RGName -Name $VNetName
    Add-AzVirtualNetworkSubnetConfig -VirtualNetwork $VNET -Name "AzureBastionSubnet" -AddressPrefix $addressPrefix | Set-AzVirtualNetwork
    $VNET = Get-AzVirtualNetwork -ResourceGroupName $RGName -Name $VNetName

    # Note: If there is an error message, it could indicate that the address prefix for the new subnet overlaps with existing address ranges or is too small.

    # Use New-AzPublicIpAddress cmdlet to create a new public IP address resource for the Bastion service.
    $publicip = New-AzPublicIpAddress -ResourceGroupName $RGName -name $BastionPubIPName -location $region -AllocationMethod Static -Sku Standard
    $publicip = Get-AzPublicIpAddress -ResourceGroupName $RGName -Name $BastionPubIPName
    # Use New-AzBastion cmdlet to create a new Azure Bastion resource with the specified configuration, including the virtual network and public IP address resources created earlier.
    New-AzBastion -ResourceGroupName $RGName -Name $BastionResourceName -PublicIpAddressRgName $publicip.ResourceGroupName -PublicIpAddressName $publicip.Name -VirtualNetwork $VNET -Sku 'Standard'

    #Enable Shareable links for VMs in Azure Bastion.
    $BastionSubnet = Get-AzVirtualNetworkSubnetConfig -Name 'AzureBastionSubnet' -VirtualNetwork $VNET

    $Body = [PSCustomObject]@{
    location = $region
    properties = @{
    enableShareableLink = "true"
    ipConfigurations = @(
    @{
    name = "bastionHostIpConfiguration"
    properties = @{
    subnet = @{
    id = $BastionSubnet.id
    }
    publicIPAddress = @{
    id = $publicip.Id
    }
    }
    }
    )
    }

    } | ConvertTo-Json -Depth 6

    $params = @{
    Uri = "https://management.azure.com/subscriptions/" + $subscription.Subscription.Id +
    "/resourceGroups/$($RGName)/providers/Microsoft.Network/bastionHosts/$($BastionResourceName)?api-version=2022-07-01"
    Headers = @{ 'Authorization' = "Bearer $token" }
    Method = 'Put'
    Body = $body
    ContentType = 'application/json'
    }

    # Invoke the REST API and store the response
    Invoke-RestMethod @Params
    }
  2. Open a Terminal or PowerShell prompt, and navigate to the folder containing the script.

  3. Dot source the script so that you can run it from the session: . .\New-AzBastionSharedLinkEnabled.ps1

  4. . .\New-AzBastionSharedLinkEnabled.ps1

  5. Once it's imported - we can now run it; make sure you replace your parameters that match your environment:

    New-AzBastionSharedLinkEnabled -RGName BastionTest -VNetName vnet-aue-dev -addressPrefix 10.2.1.0/26 -region AustraliaEast -BastionPubIPName VNet1-ip -BastionResourceName net-aue-dev-bastion
  6. The script will then prompt for your credentials to authenticate

  7. You will then need to select the Azure subscription containing your Azure Virtual Network, then select Ok

  8. Select Azure subscription

  9. The script will then go and provision Azure Bastion and enable Shared Links. It will take a few minutes to run while it provisions Bastion. Then you will get JSON output, indicating it has been completed.

  10. Windows PowerShell - New Azure Bastion

  11. Azure Bastion - Shareable Link

Now that we have an Azure Bastion instance and have Shareable Links enabled - it's time to create a Shareable Link for a Virtual Machine; this triggers 2 API endpoints - creating the shareable link and then retrieving the shareable link.

The same assumptions are made, so make sure you have the Az Module installed.

The script relies on the following parameters:

ParametersNote
BastionResourceNameThe name of your Azure Bastion resource
RGNameThe Resource Group of your Bastion resource
VMRGNameThe Resource Group of your Virtual Machine, you want a Shareable Link for
VmnameThe name of the Virtual Machine you want a shareable link for
  1. Copy the script below into a file named: New-AzBastionShareableLink.ps1

    function New-AzBastionShareableLink {
    <#
    .SYNOPSIS
    Creates an Azure Bastion shareable link.
    #>
    [CmdletBinding()]
    param
    (
    [Parameter(Mandatory = $false, Position = 0)]
    [System.String]
    $BastionResourceName = 'vnet-aue-dev-bastion',

    [Parameter(Mandatory = $false, Position = 1)]
    [System.String]
    $RGName = "BastionTest",

    [Parameter(Mandatory = $false, Position = 1)]
    [System.String]
    $VMRGName = "BastionTest",

    [Parameter(Mandatory = $false, Position = 2)]
    [System.String]
    $VMname = "2022ServerVM-2"
    )

    # Connect to Azure using Get-AzAccount
    Connect-AzAccount

    # Get all subscriptions that the account has access to
    Get-AzSubscription | Out-GridView -PassThru | Select-AzSubscription

    $subscription = Get-AzContext | Select-Object Subscription
    # Get the access token for the authenticated user
    $token = (Get-AzAccessToken).Token

    $ID = Get-AzVM -ResourceGroupName $VMRGName -Name $VMName | Select-Object Id -ExpandProperty id

    $body = @{

    vms = @(
    @{
    vm = @{
    id = $ID.Id
    }
    }
    )

    } | ConvertTo-Json -Depth 3

    #creates the shareable link for the VM
    $params = @{
    Uri = "https://management.azure.com/subscriptions/" + $subscription.Subscription.Id +
    "/resourceGroups/$RGName/providers/Microsoft.Network/bastionHosts/$BastionResourceName/createShareableLinks?api-version=2022-07-01"
    Headers = @{ 'Authorization' = "Bearer $token" }
    Method = 'POST'
    Body = $body
    ContentType = 'application/json'
    }

    # Invoke the REST API and store the response
    Invoke-RestMethod @Params

    #Gets the shareable link for the VM

    $params = @{
    Uri = "https://management.azure.com/subscriptions/" + $subscription.Subscription.Id +
    "/resourceGroups/$($RGName)/providers/Microsoft.Network/bastionHosts/$BastionResourceName/getShareableLinks?api-version=2022-09-01"
    Headers = @{ 'Authorization' = "Bearer $token" }
    Method = 'POST'
    # Body = $body
    ContentType = 'application/json'
    }

    # Invoke the REST API and store the response
    $ShareableLink = Invoke-RestMethod @Params
    Write-Output $ShareableLink.value.bsl
    }
  2. Open a Terminal or PowerShell prompt, and navigate to the folder containing the script.

  3. Dot source the script so that you can run it from the session: . .\New-AzBastionShareableLink.ps1

  4. s

  5. Once it's imported - we can now run it; make sure you replace your parameters that match your environment:

    New-AzBastionShareableLink -BastionResourceName net-aue-dev-bastion -RGName BastionTest -VMRGName BastionTest -VMname 2022ServerVM-2
  6. Azure Bastion - Create Shared Link

  7. The script will then prompt for your credentials to authenticate

  8. You will then need to select the Azure subscription containing your Azure Virtual Network, then select Ok

  9. Select Azure subscription

  10. The script will then go and collect the ID of the Virtual Machine, pass that through to the Create a Shareable Link, then wait 10 seconds for the Bastion Resource to update properly, then collect the Shareable Link and output it to the terminal.

  11. Azure Bastion - Shared Link

  12. You can also see the link created in the Azure Portal

  13. Microsoft Azure Portal - Shareable Link

  14. I can then copy the URL into my favourite browser and connect to your Virtual Machine securely!

  15. Microsoft Azure Bastion - Connect

The scripts can also be found directly on GitHub here: https://github.com/lukemurraynz/Azure