loading...

RunAS account in Azure Automation, ARM Template and deployment script

omiossec profile image Olivier Miossec ・6 min read

There are few things you cannot do in Azure with ARM templates. Among them, there is the Automation Run As Account. Creating a Run As account is not supported in ARM Template (You can Check this on Microsoft Docs). You only have two ways to create a Run As Account, via the portal or by using PowerShell or Azure CLI. It means a two steps process.

But what if you can create a Run As Account directly with an ARM template deployment after all? We can build the Automation Account by using the "Microsoft.Automation/automationAccounts" and we can use a deploymentScripts resource to add the Run As Account to the newly created automation account.
If you need to see how deploymentScripts work you can check my previous blog post

But before that, we need to learn more about Run As Account.
Run As account is used to provide an authentication mechanism to manage Azure Resources. It’s a service principal in the Azure Active directory associated with the subscription. This service principal uses a certificate for authentication and it’s used as a connection asset in the Automation resource. The service principal certificate is also added to a certificate asset to enable authentication.

Creating a run as account process include:

  • Creating a self-signed certificate
  • Creating a new Azure Ad application and an Azure Ad application credential
  • Creating a service principal with the Azure ApplicationID
  • Adding authorization to the Service Principal on the subscription (or only on a selection of resource groups)
  • Adding the connection to Azure automation connection asset.

For the self-signed certificate, you will need an Azure KeyVault. Azure KeyVault can create a self-signed certificate for you by using the Az PowerShell module.
To create a certificate in KeyVault you need to perform two operations, create a policy with the subject name, the key type, the usage, and the validity and then create the certificate with the policy

$AzureKeyVaultCertificatePolicy = New-AzKeyVaultCertificatePolicy -SubjectName $CertificatSubjectName -IssuerName "Self" -KeyType "RSA" -KeyUsage "DigitalSignature" -ValidityInMonths 12 -RenewAtNumberOfDaysBeforeExpiry 20 -KeyNotExportable:$False -ReuseKeyOnRenewal:$False

$AzureKeyVaultCertificate = Add-AzKeyVaultCertificate -VaultName $keyvaultName -Name $RunAsAccountName -CertificatePolicy $AzureKeyVaultCertificatePolicy

The certificate can take a few seconds to be generated by Azure Key Vault. It’s impossible to use before its status is completed.

do {
    start-sleep -Seconds 20
} until ((Get-AzKeyVaultCertificateOperation -Name $RunAsAccountName -vaultName $keyvaultName).Status -eq "completed")

Azure Automation certificate asset only accept PFX format. You will need to export the certificate to a .pfx file from your KeyVault.

First, you will need a password for the PFX file, and a path to store the PFX file.

To create a random password

$PfxPassword = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 48| foreach-object {[char]$_})  

And for the path to the pfx file.

$PfxFilePath = join-path -Path (get-location).path -ChildPath "cert.pfx"

Now you can export the certificate.
You will need to have the certificate object and its secret.

$AzKeyVaultCertificatObject = Get-AzKeyVaultCertificate -VaultName $keyvaultName -Name $RunAsAccountName
$AzKeyVaultCertificatSecret = Get-AzKeyVaultSecret -VaultName $keyvaultName -Name $AzKeyVaultCertificatObject.Name

And then you can get the data and write the PFX file.

$AzKeyVaultCertificatSecretBytes = [System.Convert]::FromBase64String($AzKeyVaultCertificatSecret.SecretValueText)
$certCollection = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
$certCollection.Import($AzKeyVaultCertificatSecretBytes,$null,[System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)
$protectedCertificateBytes = $certCollection.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pkcs12, $PfxPassword)
[System.IO.File]::WriteAllBytes($PfxFilePath, $protectedCertificateBytes)

Now you can create the Run As Account. You need to create an application object in your Azure Active Directory and define the credential with the certificate representation (not the PFX file).

To register the app you will need a homepage and the string representation of the certificate you created in the previous step.

First, create the App registration

$AzADApplicationRegistration = New-AzADApplication -DisplayName $RunAsAccountName -HomePage "http://$($RunAsAccountName)" -IdentifierUris $AzAdAppURI

To add a certificate credential to the new app, you will need to convert the binary date from the certificate to a base64String.

$AzKeyVaultCertificatStringValue = [System.Convert]::ToBase64String($certCollection.GetRawCertData())

and add the value as credential

New-AzADAppCredential -ApplicationId $AzADApplicationRegistration.ApplicationId -CertValue $AzKeyVaultCertificatStringValue -StartDate $certCollection.NotBefore -EndDate $certCollection.NotAfter

The second step is to create the service principal by using the applicationID

$AzADServicePrincipal = New-AzADServicePrincipal -ApplicationId $AzADApplicationRegistration.ApplicationId -SkipAssignment

Note the -skipAssignement. By default, the New-AzADServicePrincipal apply the role contributor to the current subscription. It’s not a problem for manual actions. But for an automated process, it can be risky, and you may end giving too many privileges to a script someone can modify in case of misconfiguration.
But remember without any role assignment, the Run As Account will be incomplete.

Now that you have your service principal and its PFX file you can create the Run As Account in your automation account.

First, you need to convert the String password you created earlier into a SecureString object.

PfxPassword = ConvertTo-SecureString $PfxPassword -AsPlainText -Force

And create the Certificate asset in your Automation account.

New-AzAutomationCertificate -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccount -Path $PfxFilePath -Name "AzureRunAsCertificate" -Password $PfxPassword -Exportable:$Exportable 

Then create the Connection asset. For that you will need a Hash table containing:

  • TenantID
  • SubscriptionID
  • ApplicationID
  • Certificate Thumbprint
$ConnectionFieldData = @{
        "ApplicationId" = $AzADApplicationRegistration.ApplicationId
        "TenantId" = (Get-AzContext).Tenant.ID
        "CertificateThumbprint" = $certCollection.Thumbprint
        "SubscriptionId" = (Get-AzContext).Subscription.ID
    }

New-AzAutomationConnection -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccount -Name "AzureRunAsConnection" -ConnectionTypeName "AzureServicePrincipal" -ConnectionFieldValues $ConnectionFieldData

Note the name for the certificate and the connection asset, AzureRunAsCertificate, and AzureRunAsConnection. Without these names, the Run As Account will not be created and you will end with two useless objects in your automation account.

Now how to automate this process in ARM template?
The new resource DeploymentScripts is here to help. This resource creates a Linux Container instance with a managed identity to execute any PowerShell or Azure CLI script.

The first thing to do is to give the managed identity of the Deployment Script the contributor role to the target Resource Group.
As the script needs to create a self-signed certificate with the Azure KeyVault it needs an access policy for certificate and secret.
And finally, you need to assign the role Application Administrator to the managed identity. It will allow the container to register an application and create a service principal.

There is two way to insert your script in the resource. You can use the inline form, where the script is in the ARM template or linked from where your script is hosted in a globally available URI. As you want to be sure to control how to access the script you can use an Azure blob container with a SAS token.

The ARM template is pretty simple. It takes 3 parameters, the automation account name, the location of the script, and the SAS token.

It creates the automation account and then deploys the DeploymentScripts resources that will execute the script.

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "automationAccountName": {
            "type": "string",
            "metadata": {
                "description": "Automation Account Name"
            },
            "defaultValue": "mytestautomationomc0001"
        },
        "artifactLocation": {
            "type": "string",
            "metadata": {
                "description": "Azure Blob URI"
            }
        },
        "artifactSASToken": {
            "type": "string",
            "metadata": {
                "description": "SAS Token to retreive files"
            }
        }
    },
    "functions": [],
    "variables": {
        "scriptUri": "[concat(parameters('artifactLocation'),parameters('artifactSASToken'))]"
    },
    "resources": [
        {
            "name": "[parameters('automationAccountName')]",
            "type": "Microsoft.Automation/automationAccounts",
            "apiVersion": "2015-10-31",
            "location": "[resourceGroup().location]",
            "tags": {},
            "properties": {
                "sku": {
                    "name": "Basic"
                }
            }
        },
        {
            "type": "Microsoft.Resources/deploymentScripts",
            "apiVersion": "2019-10-01-preview",
            "name": "CreateAzureAutomationRunAs",
            "location": "[resourceGroup().location]",
            "dependsOn": [
                "[resourceId('Microsoft.Automation/automationAccounts', parameters('automationAccountName'))]"
            ],
            "kind": "AzurePowerShell",
            "identity": {
                "type": "UserAssigned",
                "userAssignedIdentities": {
                    "/subscriptions/xxxxx-xxxx-xxxxx/resourceGroups/01-test-arm/providers/Microsoft.ManagedIdentity/userAssignedIdentities/testidentiry": {}
                }
            },
            "properties": {
                "azPowerShellVersion": "3.0",
                "primaryScriptUri": "[variables('scriptUri')]",
                "arguments": "[concat('-AutomationAccount ', parameters('automationAccountName'), ' -ResourceGroupName ', resourceGroup().name, ' -KeyVaultName testomc-automation001')]",
                "timeout": "PT15M",
                "cleanupPreference": "OnExpiration",
                "retentionInterval": "P4D"
            }
        }
    ],
    "outputs": {}
}

The primaryScriptUri contain the URI of the script with the SAS token. The arguments parameter is a concatenation function that will create the argument list for the script.
I added a timeout of 15 minutes (PT15M) to trigger an error if things go wrong with the script. And I keep the DeploymentScripts resources (storage account and container instance) for four days upon completion for debugging.

This is an example of what you can do with DeploymentScript resources. The complete script can be found here
DeploymentScripts is still in preview, you can check the documentation here

Posted on by:

omiossec profile

Olivier Miossec

@omiossec

Microsoft Azure MVP, Passionate about Cloud and DevOps. Co-organizers of the French PowerShell & DevOps UG . Find me on https://github.com/omiossec

Discussion

pic
Editor guide