Azure Automation Runbooks with Azure AD Service Principals and Custom RBAC Roles

Published on
Reading time
Authors

If you've ever worked in any form of systems administrator role then you will be familiar with process automation, even only for simple tasks like automating backups. You will also be familiar with the pain of configuring and managing identities for these automated processes (expired password or disabled/deleted account ever caused you any pain?!)

While the cloud offers us many new ways of working and solving traditional problems it also maintains many concepts we'd be familiar from environments of old. As you might have guessed, the idea of specifying user context for automated processes has not gone away (yet).

In this post I am going to look at how in Azure Automation Runbooks we can leverage a combination of an Azure Active Directory Service Principal and an Azure RBAC Custom Role to configure an non-user identity with constrained execution scope.

The benefits of this approach are two-fold:

  1. No password expiry to deal with, or accidental account disablement/deletion. I still need to manage keys (in place of passwords), but these are centrally managed and are subject to an expiry policy I define.
  2. Reduced blast radius. Creating a custom role that restricts actions available means the account can't be used for more actions that intended.

My specific (simple) scenario is stopping all running v2 VMs in a Subscription.

Create the Custom Role

The Azure Resource Manager (ARM) platform provides a flexible RBAC model within which we can build our own Roles based on a combination of existing Actions. In-built Roles bundle these Actions into logical groups, but there are times we may want something a little different.

The in-built "Virtual Machine Contributor" Role isn't suitable for my purposes because it provides too much scope by letting assigned users create and delete Virtual Machines. In my case, I want a Role that allows an assigned user to start, stop, restart and monitor existing Virtual Machines only.

To this end I defined a custom Role as shown below which allows any assigned user to perform the functions I need them to.

vm-power-manager-custom-role.json
{
  "Name": "Virtual Machine Power Manager",
  "IsCustom": true,
  "Description": "Can monitor, stop, start and restart v2 ARM virtual machines.",
  "Actions": [
    "Microsoft.Storage/*/read",
    "Microsoft.Network/*/read",
    "Microsoft.Compute/*/read",
    "Microsoft.Compute/virtualMachines/start/action",
    "Microsoft.Compute/virtualMachines/powerOff/action",
    "Microsoft.Compute/virtualMachines/deallocate/action",
    "Microsoft.Compute/virtualMachines/restart/action",
    "Microsoft.Authorization/*/read",
    "Microsoft.Resources/subscriptions/resourceGroups/read",
    "Microsoft.Insights/alertRules/*",
    "Microsoft.Insights/diagnosticSettings/*",
    "Microsoft.Support/*"
  ],
  "NotActions": [

  ],
  "AssignableScopes": [
    "/subscriptions/c25b1c8e-xxxx-1111-abcd-1a12d7012123"
  ]
}

Let's add this Role by executing the following PowerShell (you'll need to be logged into your Subscription with a user who has enough rights to create custom role definitions). You'll need to grab the above definition, change the scope and save it as a file named 'vm-power-manager-customerole.json' for this to work.

New-AzureRmRoleDefinition -InputFile vm-power-manager-customrole.json

which will return a result similar to the below.

Name             : Virtual Machine Power Manager
Id               : 6270aabc-0698-4380-a9a7-7df889e9e67b
IsCustom         : True
Description      : Can monitor, stop, start and restart v2 ARM virtual machines.
Actions          : {Microsoft.Storage/\*/read, Microsoft.Network/\*/read, Microsoft.Compute/\*/read
                   Microsoft.Compute/virtualMachines/start/action...}
NotActions       : {}
AssignableScopes : {/subscriptions/c25b1c8e-1111-4421-9090-1a12d7012dd3}

that means the Role shows up in the Portal and can be assigned to users. 🙂

VM Power Manager Role

Now we have that, let's setup our Service Principal.

Setup Service Principal

Microsoft provides a good guide on creating a Service Principal on the Azure documentation site already so I'm not going to reproduce that all here.

When you get to "Assign application to role" hop back here and we'll continue on without needing to dive into the Azure Portal.

For the purpose of the rest of this post, these are the parameters I used to create my Service Principal.

**Name:** Azure VM Start Stop SP
**Sign-on URL / App URI:** http://myvmautomation
**Client ID:** c6f7c745-1234-5678-0000-8d14611e75f4
**Tenant ID:** c7a48abc-1990-4fef-e941-a1cd55422e41
  • Client ID is returned once you save your Application. Tenant ID comes from your Azure AD tenant ID (see Microsoft setup instructions referenced above).

Important: you will also have to generate and grab a key value that you will need to use as it is the password for the Service Principal. Don't forget to grab it when it's displayed!

Assign the Service Principal the necessary Azure Roles

# Assign our custom Role
New-AzureRmRoleAssignment -ServicePrincipalName http://myvmautomation `
                          -RoleDefinitionName 'Virtual Machine Power Manager' `
                          -Scope '/subscriptions/c25b1c8e-xxxx-1111-abcd-1a12d7012123'

# Assign the built-in 'Reader' Role
New-AzureRmRoleAssignment -ServicePrincipalName http://myvmautomation `
                               -RoleDefinitionName 'Reader' `
                               -Scope '/subscriptions/c25b1c8e-xxxx-1111-abcd-1a12d7012123'

We now have all the baseline mechanics out of the way - next it's onto using this information in our Runbooks.

Asset Setup

Azure Automation has the concept of an Asset that can be one of six items: Schedules, PowerShell Modules, Certificates, Connections, Variables and Credentials.

These are shared between all Runbooks in an Automation Account and are extremely useful in helping you deliver generic re-usable Runbooks.

For this post we are going to create a new Credential using the following process.

Our Automation Account is called 'Core-Services' and is hosted in a Resource Group 'rg-test-01'

$username = "c6f7c745-1234-5678-0000-8d14611e75f4"
$password = ConvertTo-SecureString -String "YOUR_SERVICE_PRINCIPAL_KEY" -AsPlainText -Force

$newCreds = New-Object –TypeName System.Management.Automation.PSCredential –ArgumentList $username,$password

New-AzureRmAutomationCredential -Name "VMPowerServicePrincipal" `
                                -Description 'Service Principal used to control power state of VMs' `
                                -Value $newCreds `
                                -ResourceGroupName 'rg-test-01' `
                                -AutomationAccountName 'Core-Services'

This creates a Credential we can now use in any Runbook.

The sample PowerShell Runbook below shows how we do this using the Login-AzureRmAccount Cmdlet using the -ServicePrincipal switch.

I also specify a Tenant identifier (this is the Azure AD Tenant identifier from when you setup the Service Principal) and the Subscription identifier so we set context in one call.

The Tenant and Subcsription identifiers are held as Automation Account Variables which we read in at the start of execution (the pattern below allows you to override which Variables you pass in should you want to use different ones).

sample-runbook-creds.ps1
param (
    [Parameter(Mandatory=$false)]
    [String]$AzureCredentialAssetName = "VMPowerServicePrincipal",
    [Parameter(Mandatory=$false)]
    [String]$AzureSubscriptionIDAssetName = "VMShutdownTargetSubscription",
    [Parameter(Mandatory=$false)]
    [String]$AzureTenantIDAssetName = "VMShutdownTargetTenant"
)

# Setting error and warning action preferences
$ErrorActionPreference = "SilentlyContinue"
$WarningPreference = "SilentlyContinue"

# Read in Creds and Variables
$TenantId = Get-AutomationVariable -Name $AzureTenantIDAssetName
$Cred = Get-AutomationPSCredential -Name $AzureCredentialAssetName -ErrorAction Stop
$SubsciptionId = Get-AutomationVariable -Name $AzureSubscriptionIDAssetName

# Connecting to Azure using Service Principal
$null = Login-AzureRmAccount -ServicePrincipal `
        -Tenant $TenantId `
        -SubscriptionId $SubsciptionId `
        -Credential $Cred -ErrorAction Stop

# Read all VMs in the current Subscription
$vmList = Get-AzureRmVM

$stoppedVMCount = 0

# Iterate over each VM to get detailed status
foreach($vmEntry in $vmList)
{
  # get the power state of the VM
  $vmDetail = Get-AzureRmVM -Name $vmEntry.Name `
              -ResourceGroupName $vmEntry.ResourceGRoupName `
              -Status -WarningAction $WarningPreference `
              -ErrorAction $ErrorActionPreference

  # If this string is present then the VM is on.
  if($vmDetail.StatusesText.Contains("PowerState/running"))
  {
    Stop-AzureRmVM -Name $vmDetail.Name `
                    -ResourceGroupName $vmDetail.ResourceGRoupName `
                    -Force -WarningAction $WarningPreference `
                    -ErrorAction $ErrorActionPreference
    $stoppedVMCount++
  }
}
Write-Output "Stopped $($stoppedVMCount) Virtual Machine(s)."

So there we have it - a way to perform VM power state management in an Azure Automation Runbook that uses a non-user account for authentication along with custom RBAC roles for authorisation.

Enjoy!