{"id":202,"date":"2026-05-11T08:47:59","date_gmt":"2026-05-11T15:47:59","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/azure-vm-runtime\/?p=202"},"modified":"2026-05-11T08:47:59","modified_gmt":"2026-05-11T15:47:59","slug":"publishing-vm-applications-in-a-secure-world","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/azure-vm-runtime\/publishing-vm-applications-in-a-secure-world\/","title":{"rendered":"Publishing VM Applications in a Secure World"},"content":{"rendered":"<p>Many of you who have published <a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/virtual-machines\/vm-applications\">VM Applications<\/a> 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.<\/p>\n<ol>\n<li>Make the blob public, meaning that anyone in the world can access the package.<\/li>\n<li>Create a SAS uri. This provides\u00a0<em>some<\/em> more security in that you can make the SAS read-only and with a limited expiration. However, if someone somehow obtains that url, they&#8217;ll have access to that application package. For this reason, many organizations discourage use of SAS urls.<\/li>\n<\/ol>\n<p>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&#8217;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.<\/p>\n<p>In general, we won&#8217;t access the original blob after the initial replication except in one important scenario: you&#8217;re adding another replication region to the application. In that case, we\u00a0<em>will<\/em> access the original blob, though in an increasing number of regions we won&#8217;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&#8217;re extending the application to a new region where this feature isn&#8217;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.<\/p>\n<p>However, the above is still not an ideal solution. A much\u00a0<em>better<\/em> solution is to remove\u00a0<em>all<\/em> 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.<\/p>\n<p>First, we&#8217;re going to set some variables.<\/p>\n<p><code>$rg = \"yourrg\"<\/code>\n<code>$galleryName = \"flipperdoodle\"<\/code>\n<code>$applicationName = \"flooperapp\"<\/code>\n<code>$location = \"someregion\"<\/code><\/p>\n<p>Now, you&#8217;re going to create the gallery, which will contain your application, making sure it has a managed identity.<\/p>\n<p><code>$requestBody = @{<\/code>\n<code>\u00a0 \u00a0location = $location<\/code>\n<code>\u00a0 \u00a0identity = @{<\/code>\n<code>\u00a0 \u00a0 \u00a0 type = \"SystemAssigned\"<\/code>\n<code>\u00a0 \u00a0}<\/code>\n<code>}<\/code><\/p>\n<p><code>$subscriptionId = (Get-AzContext).Subscription.Id<\/code>\n<code>$uri = \"https:\/\/eastus2euap.management.azure.com\/subscriptions\/$subscriptionId\/resourceGroups\/$rg\/providers\/Microsoft.Compute\/galleries\/${galleryName}?api-version=2023-07-03\"<\/code><\/p>\n<p><code>$requestBodyJson = $requestBody | ConvertTo-Json -Depth 8<\/code>\n<code>$secureToken = (Get-AzAccessToken -ResourceUrl \"https:\/\/management.azure.com\/\").Token<\/code>\n<code>$plainToken = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureToken)<\/code>\n<code>$params = @{ Headers = @{'authorization'=\"Bearer $($plainToken)\"}; Method = 'PUT'; URI = $uri; Body = $requestBodyJson; ContentType = 'application\/json' }<\/code>\n<code>$response = Invoke-RestMethod @params<\/code>\n<code>$response | convertto-json -Depth 8<\/code><\/p>\n<p>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.<\/p>\n<p><code>Get-AzGallery -ResourceGroupName $rg -Name $galleryName<\/code><\/p>\n<p><code>ResourceGroupName : {your RG}<\/code>\n<code>Identifier :<\/code>\n<code>UniqueName : {your subscription}-FLIPPERDOODLE<\/code>\n<code>ProvisioningState : Succeeded<\/code>\n<code>Id : \/subscriptions\/{your subscription}\/resourceGroups\/{your RG}\/providers\/Microsoft.Compute\/galleries\/flipperdoodle<\/code>\n<code>Name : flipperdoodle<\/code>\n<code>Type : Microsoft.Compute\/galleries<\/code>\n<code>Location : {location}<\/code>\n<code>Tags : {}<\/code><\/p>\n<p>Now, create the VM Application in the gallery. This is more straightforward.<\/p>\n<p><code>New-AzGalleryApplication -GalleryName $galleryName -Name $applicationName -ResourceGroupName $rg -SubscriptionId $subscriptionId -Location $location -SupportedOS \"Windows\"<\/code><\/p>\n<p>Now, you&#8217;re going to create the storage account and container, defaulting to no public access. This must be done using an ARM template.<\/p>\n<p>First, a few variables.<\/p>\n<p><code>$sa = \"flooperstorage\"<\/code>\n<code>$sc = \"floopercontainer\"<\/code><\/p>\n<p>Now, create a template similar to the following.<\/p>\n<p><code>{<\/code>\n<code>\u00a0 \u00a0\"$schema\": \"https:\/\/schema.management.azure.com\/schemas\/2019-04-01\/deploymentTemplate.json#\",<\/code>\n<code>\u00a0 \u00a0\"contentVersion\": \"1.0.0.0\",<\/code>\n<code>\u00a0 \u00a0\"resources\": [<\/code>\n<code>\u00a0 \u00a0{<\/code>\n<code>\u00a0 \u00a0 \u00a0 \"type\": \"Microsoft.Storage\/storageAccounts\",<\/code>\n<code>\u00a0 \u00a0 \u00a0 \"apiVersion\": \"2023-01-01\",<\/code>\n<code>\u00a0 \u00a0 \u00a0 \"name\": \"flooperstorage\",<\/code>\n<code>\u00a0 \u00a0 \u00a0 \"location\": \"[resourceGroup().location]\",<\/code>\n<code>\u00a0 \u00a0 \u00a0 \"sku\": {<\/code>\n<code>\u00a0 \u00a0 \u00a0 \u00a0 \u00a0\"name\": \"Standard_LRS\"<\/code>\n<code>\u00a0 \u00a0 \u00a0 },<\/code>\n<code>\u00a0 \u00a0 \u00a0 \"kind\": \"StorageV2\",<\/code>\n<code>\u00a0 \u00a0 \u00a0 \"properties\": {<\/code>\n<code>\u00a0 \u00a0 \u00a0 \u00a0 \u00a0\"allowSharedKeyAccess\": false,<\/code>\n<code>\u00a0 \u00a0 \u00a0 \u00a0 \u00a0\"allowBlobPublicAccess\": false,<\/code>\n<code>\u00a0 \u00a0 \u00a0 \u00a0 \u00a0\"defaultToOAuthAuthentication\": true<\/code>\n<code>\u00a0 \u00a0 \u00a0 }<\/code>\n<code>\u00a0 \u00a0}<\/code>\n<code>]<\/code>\n<code>}<\/code><\/p>\n<p>Create the account and container.<\/p>\n<p><code>az deployment group create --resource-group $rg --template-file .\\yourtemplate.json<\/code>\n<code>az storage container create --name $sc --account-name $sa --auth-mode login<\/code><\/p>\n<p>To upload your blob to the container, you&#8217;ll need to create an RBAC role for yourself.<\/p>\n<p><code>$objectId = (Get-AzADUser -SignedIn).Id<\/code><\/p>\n<p><code>az role assignment create `<\/code>\n<code>\u00a0 \u00a0--assignee-object-id $objectId `<\/code>\n<code>\u00a0 \u00a0--assignee-principal-type User `<\/code>\n<code>\u00a0 \u00a0--role \"Storage Blob Data Contributor\" `<\/code>\n<code>\u00a0 \u00a0--scope \"\/subscriptions\/$subscriptionId\/resourceGroups\/$rg\/providers\/Microsoft.Storage\/storageAccounts\/$sa\"<\/code><\/p>\n<p><code>$blobName = \"mypackage\"<\/code>\n<code>az storage blob upload --container-name $sc --account-name $sa --name $blobName --file \".\\myscript.ps1\" --auth-mode login<\/code><\/p>\n<p>Now, you&#8217;re going to ensure the Storage account is locked down, but Azure Services can still access it. You&#8217;ll define\u00a0<em>which<\/em> Azure Services in the next step.<\/p>\n<p><code>az storage account update --name $sa --resource-group $rg --bypass AzureServices<\/code><\/p>\n<p><code>Update-AzStorageAccountNetworkRuleSet `<\/code>\n<code>\u00a0 \u00a0-ResourceGroupName $rg `<\/code>\n<code>\u00a0 \u00a0-Name $sa `<\/code>\n<code>\u00a0 \u00a0-DefaultAction Deny `<\/code>\n<code>\u00a0 \u00a0-Bypass AzureServices<\/code><\/p>\n<p>We&#8217;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.<\/p>\n<p><code>$identityPrincipalId=$(az resource show --name $galleryName --resource-group $rg --resource-type Microsoft.Compute\/galleries --query identity.principalId -o tsv)<\/code><\/p>\n<p><code>az role assignment create `<\/code>\n<code>\u00a0 \u00a0--assignee-object-id $identityPrincipalId `<\/code>\n<code>\u00a0 \u00a0--assignee-principal-type ServicePrincipal `<\/code>\n<code>\u00a0 \u00a0--role \"Storage Blob Data Contributor\" `<\/code>\n<code>\u00a0 \u00a0--scope \"\/subscriptions\/$subscriptionId\/resourceGroups\/$rg\/providers\/Microsoft.Storage\/storageAccounts\/$sa\"<\/code><\/p>\n<p>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.<\/p>\n<p><code>$sourceBlobUri = \"https:\/\/$sa.blob.core.windows.net\/$sc\/$blobName\"<\/code>\n<code>New-AzGalleryApplicationVersion -GalleryApplicationName $applicationName -GalleryName $galleryName -Name \"1.0.0\" -ResourceGroupName $rg -SubscriptionId $subscriptionId -Location $location -Install \"echo blah\" -Remove \"echo blah\" -PackageFileLink $sourceBlobUri<\/code><\/p>\n<p>Note that in some sovereign clouds the sourceBlobUri will differ. Presumably, you&#8217;ll also have more complicated install and remove scripts.<\/p>\n<p>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&#8217;ll need to put the role back, though in the next few months this will also be unnecessary.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>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, [&hellip;]<\/p>\n","protected":false},"author":103696,"featured_media":203,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[2],"tags":[5,20,19,4],"class_list":["post-202","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-vm-applications","tag-azure","tag-managedidentity","tag-security","tag-vmapplications"],"acf":[],"blog_post_summary":"<p>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, [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/azure-vm-runtime\/wp-json\/wp\/v2\/posts\/202","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/azure-vm-runtime\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/azure-vm-runtime\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-vm-runtime\/wp-json\/wp\/v2\/users\/103696"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-vm-runtime\/wp-json\/wp\/v2\/comments?post=202"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/azure-vm-runtime\/wp-json\/wp\/v2\/posts\/202\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-vm-runtime\/wp-json\/wp\/v2\/media\/203"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/azure-vm-runtime\/wp-json\/wp\/v2\/media?parent=202"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-vm-runtime\/wp-json\/wp\/v2\/categories?post=202"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-vm-runtime\/wp-json\/wp\/v2\/tags?post=202"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}