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
Top comments (1)
I'm trying to implement this with a slight difference (using an existing service principal). I'm having a problem with the cert. Specifically the $certCollection.Import() call. The error I'm getting is "Exception calling "Import" with "3" argument(s): "Error occurred during a cryptographic operation."
Did you get this, or know where this might be coming from? TIA.