Skip to content
The Lazy Administrator
  • Home
  • Disclaimer
  • Contact
  • About Me
  • Search Icon

The Lazy Administrator

Finding ways to do the most work with the least effort possible

Automated Deployment of a Zero Trust Azure Automation Environment

Automated Deployment of a Zero Trust Azure Automation Environment

December 30, 2024 Brad Wyatt Comments 0 Comment

Table of Contents

  • Overview
    • Pre-requisites
    • Execution and Functional Overview
      • Deployment
      • Testing
  • Sources

Overview

A common and recommended security practice is only allowing access to an Azure Storage Account via a whitelisted IP address. While this is generally a good idea, a problem arises when you need an Azure Automation Account to access one of these Storage Accounts. Currently, even if you whitelist an entire Azure region, your automation runbook will fail to connect to your Storage Account. Instead, you must use an Azure Private Link to connect Azure Automation to your PaaS Azure Resources securely, but “in the current implementation of Private Link, Automation account cloud jobs cannot access Azure resources that are secured using private endpoint. For example, Azure Key Vault, Azure SQL, Azure Storage account, etc. To workaround this, use a Hybrid Runbook Worker instead. Hence, on-premises VMs are supported to run Hybrid Runbook Workers against an Automation Account with Private Link enabled.” 1

This configuration isn’t as simple as creating or deploying a traditional Azure Runbook; you must create private endpoints, subnets, DNS zones, hybrid worker groups, and more. I wanted to make a PowerShell script that anyone could run, and it would generate everything for you from start to finish, so in the end, it would be working out of the box without additional configuration needed. This includes installing PowerShell Core on the Hybrid Runbook Worker. The diagram below gives an architectural overview of the deployment and configuration.

Private Link support with Azure Automation is available only in Azure Commercial and Azure US Government clouds.

The script handles everything from creating the Virtual Network with proper subnet configuration, setting up Private Endpoints and DNS zones, configuring a Hybrid Worker VM, and implementing Managed Identity authentication. You can deploy this entire environment with a single PowerShell script rather than spending hours clicking through the Azure portal or writing multiple scripts.

Key features of this deployment:

  • Complete network isolation with Private Link and Private Endpoints
  • Automated DNS configuration for private networking
  • Hybrid Worker VM setup with proper security configuration
  • System-assigned Managed Identity implementation for secure authentication
  • Zero public endpoint exposure for Storage Account
  • Proper RBAC assignments for least privileged access
  • Network Security Group configuration for the Hybrid Worker VM
  • Tags all the resources it deploys for easy identification

While the script deploys the hybrid worker VM with a public IP address, you can remove the public IP address from the hybrid worker upon completion. I added it so I could install Pwsh7 and set up Bastion access.

Pre-requisites

  1. Az Module
    1. Az.Network
    2. Az.Storage
    3. Az.Automation
    4. Az.Compute
    5. Az.OperationalInsights
  2. Azure Subscription
  3. Proper Azure Permissions to create and manage resources within a subscription

Execution and Functional Overview

Deployment

  1. Download the current version of the PowerShell script here.
  2. Save the script somewhere we can reference later. I recommend a folder called Scripts on the root of C:\
  3. Next, we need to create the param block for our deployment.
    • The only items it will noNext, we need to create the param block for our deployment. This is the step where you name your resources and specify the location of them. Four parameters have default values (vnetAddressPrefix, PrivateEndpointSubnetAddressPrefix, HybridWorkerSUbnetAddressPrefix and Tags)
      1. The only items it will not create if they are present are the Automation Account and Resource Group. Everything else will not check to see if it’s already present before creation.
      2. vnetAddressPrefix has a default value of 10.0.0.0/16
      3. PrivateEndpointSUbnetAddressPrefix has a default value of 10.0.1.0/24
      4. HybridWorkerSubnetAddressPrefix has a default value of 10.0.2.0/24
      5. Tags has a default value of:
"Automation" = "HybridWorker"
"Department" = "DevOps"
$params = @{
    ResourceGroupName      = "rg-hybridRW"
    Location               = "northcentralus"
    StorageAccountName     = "sahybridrw" + (Get-Random)
    AutomationAccountName  = "StorageAccountName"
    VirtualNetworkName     = "vnet-hybridRW"
    VMName                 = "vm-hybridRW"
    AdminUsername          = "adminuser"
    AdminPassword          = (ConvertTo-SecureString "WeMuSTChAng3!Plz" -AsPlainText -Force)
    RunbookWorkerGroupName = "hybridRWgroup"
    TableName              = "demotable"
    Verbose                = $true
}

4. Now that we have specified deployment, open a PowerShell terminal and navigate to the location where you saved the script from step 1.

5. Next, let’s load our $params block from earlier

6. Now we can dot source our PowerShell script with the $params values

7. The first thing the script will do will be to check that you have the correct modules installed, if not it will download them and then load them into memory.

8. Next, it will launch a web login for you to log into Azure with.

9. If you have multiple subscriptions, it will prompt you which subscription you want to deploy into

10. After that, since we had verbose messaging turned on, we can see it creating resources

Testing

  1. Once it’s complete, I can navigate to the Azure Portal and see everything it deployed.

2. If I sign into the Hybrid Runbook worker using Bastion (the reason I gave this machine a public IP) I can go to C:\ and see that PowerShell Core 7 has been installed.

3. Next, I will create a test table entry in my table.

4. Next, I will go to my Azure Automation Account and create a new test runbook to ensure I have access to the Storage Table

5. Next, I will paste the following PowerShell code for the runbook content:

  • Make sure to change the Storage Account name from sahybridrw1226660012 to your Storage Account name
  • Make sure to change the Table name from demotable to your Table’s name.
Install-Module Az -Force
function Get-AZTableEntityAll {
    [CmdletBinding()]  # This enables -Verbose support
    param (
        [Parameter(Mandatory)]
        [string] $StorageAccount,
        [Parameter(Mandatory)]
        [string] $TableName,
        [string] $SASToken,
        [string] $AccessKey,
        [string] $AzToken,
        [string] $Filter
    )

    Write-Verbose "Starting Get-AZTableEntityAll for table '$TableName' in storage account '$StorageAccount'"
    
    $version = "2022-11-02"
    $resource = "$TableName"
    $GMTTime = (Get-Date).ToUniversalTime().toString('R')
    $stringToSign = "$GMTTime`n/$storageAccount/$resource"
    
    Write-Verbose "Building authentication headers"
    Write-Debug "String to sign: $stringToSign"
    
    # Create headers based on authentication method
    $headers = @{
        'x-ms-date'    = $GMTTime
        "x-ms-version" = $version
        Accept         = "application/json;odata=fullmetadata"
    }

    if ($AccessKey) {
        Write-Verbose "Using AccessKey authentication"
        $hmacsha = New-Object System.Security.Cryptography.HMACSHA256
        $hmacsha.key = [Convert]::FromBase64String($accesskey)
        $signature = $hmacsha.ComputeHash([Text.Encoding]::UTF8.GetBytes($stringToSign))
        $signature = [Convert]::ToBase64String($signature)
        $headers.Authorization = "SharedKeyLite " + $StorageAccount + ":" + $signature
    }

    # Build the URL
    $table_url = "https://$StorageAccount.table.core.windows.net/$resource"
    Write-Verbose "Base table URL: $table_url"

    if ($Filter) {
        Write-Verbose "Applying filter: $Filter"
        $table_url = $table_url + '?$filter=' + [uri]::EscapeDataString($filter)
        Write-Debug "URL with filter: $table_url"
    }

    if ($SASToken) {
        Write-Verbose "Using SAS Token authentication"
        $headers.remove('Authorization')
        $table_url = $table_url + '?' + $SASToken
    }
    elseif ($AzToken) {
        Write-Verbose "Using Azure AD Token authentication"
        $headers.Authorization = "Bearer " + $AzToken
    }

    Write-Verbose "Starting initial data retrieval"
    $totalRecords = 0
    $pageCount = 1

    try {
        Write-Progress -Activity "Retrieving table entities" -Status "Page $pageCount" -PercentComplete 0
        $item = Invoke-WebRequest -Method GET -Uri $table_url -Headers $headers -UseBasicParsing -ErrorAction Stop
        
        $pageData = ($item.content | ConvertFrom-JSON).Value
        $totalRecords += $pageData.Count
        Write-Verbose "Retrieved $($pageData.Count) records in page $pageCount"
        $pageData

        while ($item.headers.keys -contains 'x-ms-continuation-NextRowKey' -and 
               $item.headers.keys -contains 'x-ms-continuation-NextPartitionKey') {
            
            $NextRowKey = $item.headers.'x-ms-continuation-NextRowKey'
            $NextPartitionKey = $item.headers.'x-ms-continuation-NextPartitionKey'
            Write-Debug "Next Partition Key: $NextPartitionKey, Next Row Key: $NextRowKey"
            
            Clear-Variable item
            if ($filter) {
                $NewURL = ($table_url + '&NextPartitionKey=' + $NextPartitionKey + '&NextRowKey=' + $NextRowKey)
            }
            else {
                $NewURL = ($table_url + '?NextPartitionKey=' + $NextPartitionKey + '&NextRowKey=' + $NextRowKey)
            }
            
            Write-Verbose "Retrieving page $($pageCount + 1)"
            Write-Progress -Activity "Retrieving table entities" -Status "Page $($pageCount + 1)" -PercentComplete (($pageCount % 100) * 1)
            
            $item = Invoke-WebRequest -Method GET -Uri $NewURL -Headers $headers -UseBasicParsing -ErrorAction Stop
            $pageData = ($item.content | ConvertFrom-JSON).Value
            $totalRecords += $pageData.Count
            Write-Verbose "Retrieved $($pageData.Count) records in page $($pageCount + 1)"
            $pageData
            
            Clear-Variable NextPartitionKey, NextRowKey
            $pageCount++
            Start-Sleep -milliseconds 200
        }
    }
    catch {
    Write-Error "Error retrieving data from Azure Table: $($_.Exception.Message)"
    Write-Verbose "Status Code: $($_.Exception.Response.StatusCode.value__)"
    Write-Verbose "Status Description: $($_.Exception.Response.StatusDescription)"
    Write-Verbose "Request URL: $table_url"
    Write-Verbose "Headers used:"
    $headers.GetEnumerator() | ForEach-Object {
        Write-Verbose "  $($_.Key): $($_.Value)"
    }
    throw $_
    }
    finally {
        Write-Progress -Activity "Retrieving table entities" -Completed
        Write-Verbose "Operation completed. Total records retrieved: $totalRecords across $pageCount pages"
    }
}
Connect-AzAccount -Identity
$AzToken = (Get-AzAccessToken -ResourceUrl "https://sahybridrw1226660012.table.core.windows.net").Token

Get-AZTableEntityAll -StorageAccount 'sahybridrw1226660012' -TableName 'demotable' -AzToken $AzToken -Verbose

The first line is to install the Az module. We only need to do this once to install the module and then all other runbooks will be able to use the module. This is not part of the original build out script because the runbook runs as the system context.

7. When you test the runbook, select the Hybrid Worker for it to run on.

8. Once the runbook completes running, we can see that it was able to retrieve my table data.

9. Done! Reminder that you can delete or disassociate the public IP of the hybrid worker if you’d like.


Sources

  1. https://learn.microsoft.com/en-us/azure/automation/how-to/private-link-security#limitations ↩︎

Brad Wyatt
Brad Wyatt

My name is Bradley Wyatt; I am a 5x Microsoft Most Valuable Professional (MVP) in Microsoft Azure and Microsoft 365. I have given talks at many different conferences, user groups, and companies throughout the United States, ranging from PowerShell to DevOps Security best practices, and I am the 2022 North American Outstanding Contribution to the Microsoft Community winner.


Azure, PowerShell
Automation, Azure, PowerShell, Runbook

Post navigation

PREVIOUS
Automatically Detect and Prevent Secrets Leaked into Code within Azure DevOps
NEXT
Automating Code Compliance: AI-Driven Code Style Enforcement for Pull Requests

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Subscribe

Email


Categories

  • Active Directory (8)
  • AI (3)
  • API (1)
  • AutoPilot (2)
  • Azure (15)
  • Bicep (4)
  • Connectwise (1)
  • Defender for Cloud Apps (1)
  • Delegated Admin (1)
  • DevOps (6)
  • Graph (6)
  • Intune (15)
  • LabTech (1)
  • Microsoft Teams (6)
  • Office 365 (19)
  • Permissions (2)
  • PowerShell (50)
  • Security (1)
  • SharePoint (3)
  • Skype for Business (1)
  • Terraform (1)
  • Uncategorized (2)
  • Yammer (1)

Recent Comments

  • Kristopher Gates on Getting Started with GitHub Copilot in the CLI
  • MD SHARIQUE AKHTAR on Modern Active Directory – An update to PSHTML-AD-Report
  • TommyBoich on How The ConnectWise Manage API Handles Pagination with PowerShell
  • LOTTERY 365 LOGIN on Windows LAPS Management, Configuration and Troubleshooting Using Microsoft Intune
  • SPRUNKI PHASE 6 on Get a New Computer’s Auto Pilot Hash Without Going Through the Out of Box Experience (OOBE)

1,740,555 People Reached

© 2025   All Rights Reserved.