May 11th, 2026
0 reactions

Publishing VM Applications in a Secure World

Principal Software Engineer, Azure Core Compute

Many of you who have published VM Applications may have come across a difficult conundrum regarding security. To create an application version, you must upload your application package to a Storage blob, then provide the address of that Storage blob for replication to occur. The problem comes to the uri of the package. Until recently, there have been two options.

  1. Make the blob public, meaning that anyone in the world can access the package.
  2. Create a SAS uri. This provides some more security in that you can make the SAS read-only and with a limited expiration. However, if someone somehow obtains that url, they’ll have access to that application package. For this reason, many organizations discourage use of SAS urls.

Before I move to the solution, a few quick words about SAS urls. First, user delegated SAS urls are sadly not supported, because the service that manages the replication can’t impersonate the user. Second, you may have a question about reuse of the url. For how long must you set the expiry? Well. That depends.

In general, we won’t access the original blob after the initial replication except in one important scenario: you’re adding another replication region to the application. In that case, we will access the original blob, though in an increasing number of regions we won’t. There is a change rolling out where the replication service will reuse the blob from the home region and therefore the original blob will never be referenced again. In the meantime, if you’re extending the application to a new region where this feature isn’t rolled out yet, then we do allow you to modify the SAS (but not the rest of the url) for an existing application version. Thus, you may initially specify a limited expiry when publishing the initial version, then reset that expiry shortly before moving your application to one or more new regions.

However, the above is still not an ideal solution. A much better solution is to remove all public access to your blob and only allow specific Microsoft services to access it. That is what we now support. The following are sample instructions on making this happen in Powershell/CLI.

First, we’re going to set some variables.

$rg = "yourrg" $galleryName = "flipperdoodle" $applicationName = "flooperapp" $location = "someregion"

Now, you’re going to create the gallery, which will contain your application, making sure it has a managed identity.

$requestBody = @{    location = $location    identity = @{       type = "SystemAssigned"    } }

$subscriptionId = (Get-AzContext).Subscription.Id $uri = "https://eastus2euap.management.azure.com/subscriptions/$subscriptionId/resourceGroups/$rg/providers/Microsoft.Compute/galleries/${galleryName}?api-version=2023-07-03"

$requestBodyJson = $requestBody | ConvertTo-Json -Depth 8 $secureToken = (Get-AzAccessToken -ResourceUrl "https://management.azure.com/").Token $plainToken = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureToken) $params = @{ Headers = @{'authorization'="Bearer $($plainToken)"}; Method = 'PUT'; URI = $uri; Body = $requestBodyJson; ContentType = 'application/json' } $response = Invoke-RestMethod @params $response | convertto-json -Depth 8

This is a bit more convoluted than it needs to be because you need to construct the raw request, but once complete you can validate that your gallery is there.

Get-AzGallery -ResourceGroupName $rg -Name $galleryName

ResourceGroupName : {your RG} Identifier : UniqueName : {your subscription}-FLIPPERDOODLE ProvisioningState : Succeeded Id : /subscriptions/{your subscription}/resourceGroups/{your RG}/providers/Microsoft.Compute/galleries/flipperdoodle Name : flipperdoodle Type : Microsoft.Compute/galleries Location : {location} Tags : {}

Now, create the VM Application in the gallery. This is more straightforward.

New-AzGalleryApplication -GalleryName $galleryName -Name $applicationName -ResourceGroupName $rg -SubscriptionId $subscriptionId -Location $location -SupportedOS "Windows"

Now, you’re going to create the storage account and container, defaulting to no public access. This must be done using an ARM template.

First, a few variables.

$sa = "flooperstorage" $sc = "floopercontainer"

Now, create a template similar to the following.

{    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",    "contentVersion": "1.0.0.0",    "resources": [    {       "type": "Microsoft.Storage/storageAccounts",       "apiVersion": "2023-01-01",       "name": "flooperstorage",       "location": "[resourceGroup().location]",       "sku": {          "name": "Standard_LRS"       },       "kind": "StorageV2",       "properties": {          "allowSharedKeyAccess": false,          "allowBlobPublicAccess": false,          "defaultToOAuthAuthentication": true       }    } ] }

Create the account and container.

az deployment group create --resource-group $rg --template-file .\yourtemplate.json az storage container create --name $sc --account-name $sa --auth-mode login

To upload your blob to the container, you’ll need to create an RBAC role for yourself.

$objectId = (Get-AzADUser -SignedIn).Id

az role assignment create `    --assignee-object-id $objectId `    --assignee-principal-type User `    --role "Storage Blob Data Contributor" `    --scope "/subscriptions/$subscriptionId/resourceGroups/$rg/providers/Microsoft.Storage/storageAccounts/$sa"

$blobName = "mypackage" az storage blob upload --container-name $sc --account-name $sa --name $blobName --file ".\myscript.ps1" --auth-mode login

Now, you’re going to ensure the Storage account is locked down, but Azure Services can still access it. You’ll define which Azure Services in the next step.

az storage account update --name $sa --resource-group $rg --bypass AzureServices

Update-AzStorageAccountNetworkRuleSet `    -ResourceGroupName $rg `    -Name $sa `    -DefaultAction Deny `    -Bypass AzureServices

We’re getting close. The next step is to explicitly allow your gallery to access the Storage Account by creating a role for its system assigned managed identity.

$identityPrincipalId=$(az resource show --name $galleryName --resource-group $rg --resource-type Microsoft.Compute/galleries --query identity.principalId -o tsv)

az role assignment create `    --assignee-object-id $identityPrincipalId `    --assignee-principal-type ServicePrincipal `    --role "Storage Blob Data Contributor" `    --scope "/subscriptions/$subscriptionId/resourceGroups/$rg/providers/Microsoft.Storage/storageAccounts/$sa"

Now, your Storage account is blocked for public access, but your gallery will be able to access it for replication. You may now create the application version.

$sourceBlobUri = "https://$sa.blob.core.windows.net/$sc/$blobName" New-AzGalleryApplicationVersion -GalleryApplicationName $applicationName -GalleryName $galleryName -Name "1.0.0" -ResourceGroupName $rg -SubscriptionId $subscriptionId -Location $location -Install "echo blah" -Remove "echo blah" -PackageFileLink $sourceBlobUri

Note that in some sovereign clouds the sourceBlobUri will differ. Presumably, you’ll also have more complicated install and remove scripts.

If you wish, you may remove access from the gallery to the blob once the application has replicated. For the time being, if you need to later expand the application to another region, you’ll need to put the role back, though in the next few months this will also be unnecessary.

Author

Joseph Calev
Principal Software Engineer, Azure Core Compute

Software Engineer Lead at Microsoft. Focusing on enabling customer scenarios for VM extensions and applications.

0 comments