Skip to content

Instantly share code, notes, and snippets.

@omiossec
Created September 11, 2020 10:46
Show Gist options
  • Save omiossec/e64ec646668d8dda05502f07f9dbf11a to your computer and use it in GitHub Desktop.
Save omiossec/e64ec646668d8dda05502f07f9dbf11a to your computer and use it in GitHub Desktop.
<#
.SYNOPSIS
Create Azure Application, Certificate, SP and link them to Azure Automation Account as Run As Account
.DESCRIPTION
Create Azure Application, Certificate, SP and link them to Azure Automation Account as Run As Account
.PARAMETER ResourceGroupName
Name of the resource group where are located Automation Account and Keyvault
.PARAMETER AutomationAccount
Automation Account Name
.PARAMETER KeyVaultName
Keyvault name
.PARAMETER RunAsName
RunAs Account Name
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string]
$ResourceGroupName,
[Parameter(Mandatory = $true)]
[string]
$AutomationAccount,
[Parameter(Mandatory = $true)]
[string]
$KeyVaultName
)
$RunAsAccountName = "$($AutomationAccount)-runas"
$CertificatSubjectName = "CN=$($RunAsAccountName)"
$AzAppUniqueId = (New-Guid).Guid
$AzAdAppURI = "http://$($AutomationAccount)$($AzAppUniqueId)"
$AzureKeyVaultCertificatePolicy = New-AzKeyVaultCertificatePolicy -SubjectName $CertificatSubjectName -IssuerName "Self" -KeyType "RSA" -KeyUsage "DigitalSignature" -ValidityInMonths 12 -RenewAtNumberOfDaysBeforeExpiry 20 -KeyNotExportable:$False -ReuseKeyOnRenewal:$False
Add-AzKeyVaultCertificate -VaultName $keyvaultName -Name $RunAsAccountName -CertificatePolicy $AzureKeyVaultCertificatePolicy | out-null
do {
start-sleep -Seconds 20
} until ((Get-AzKeyVaultCertificateOperation -Name $RunAsAccountName -vaultName $keyvaultName).Status -eq "completed")
$PfxPassword = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 48| foreach-object {[char]$_})
$PfxFilePath = join-path -Path (get-location).path -ChildPath "cert.pfx"
start-sleep 30
$AzKeyVaultCertificatSecret = Get-AzKeyVaultSecret -VaultName $keyvaultName -Name $RunAsAccountName
$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)
$AzADApplicationRegistration = New-AzADApplication -DisplayName $RunAsAccountName -HomePage "http://$($RunAsAccountName)" -IdentifierUris $AzAdAppURI
$AzKeyVaultCertificatStringValue = [System.Convert]::ToBase64String($certCollection.GetRawCertData())
$AzADApplicationCredential = New-AzADAppCredential -ApplicationId $AzADApplicationRegistration.ApplicationId -CertValue $AzKeyVaultCertificatStringValue -StartDate $certCollection.NotBefore -EndDate $certCollection.NotAfter
$AzADServicePrincipal = New-AzADServicePrincipal -ApplicationId $AzADApplicationRegistration.ApplicationId -SkipAssignment
$PfxPassword = ConvertTo-SecureString $PfxPassword -AsPlainText -Force
New-AzAutomationCertificate -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccount -Path $PfxFilePath -Name "AzureRunAsCertificate" -Password $PfxPassword -Exportable:$Exportable
$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
@yaench
Copy link

yaench commented Oct 14, 2020

Thanks for sharing this script. I do have a question.

When running the script everything seems to go fine. But when I look into the Automation account, the RunAs account is marked as being "Incomplete". The following error is displayed:

The Run As account is incomplete. Either one of these was deleted or not created - Azure Active Directory Application, Service Principal, Role, Automation Certificate asset, Automation Connection asset - or the Thumbprint is not identical between Certificate and Connection. 
Please delete and then re-create the Run As account.

Have you ran into this problem? Any idea what could be the problem here?

I figured out that the name of the connection as well as the certificate have to be "AzureRunAsConnection" / "AzureRunAsCertificate". I tried everything I could imagine and it failed with the same error you have. After changing the name of the certificate to "AzureRunAsCertificate", the error disappeared. Now I'm wondering why we even have something like thumbprints on certs.

(<= frustrated after hours of troubleshooting)

@rwaal
Copy link

rwaal commented Oct 19, 2020

Hi @yaench, thanks for your response (and sorry for my late response). But I don't fully understand what you mean. The script in its current state creates a certificate with name AzureRunAsCertificate, and a connection named AzureRunAsConnection. This is exactly what you mentioned in your response.

So what exactly did you change? I'm running the script, where the certificate and connection are apparently named correctly. Still I get the error message.

@ColinEff
Copy link

Great script. Cheers

I had the same issue where the RunAs account appeared as Incomplete. I'd been working with RunAs accounts a month ago and automated replacement of the assigned Role. Realised this code did not assign the Contributor role, or any other role to the RunAs Account.
Use New-AzRoleAssignment to assign a role of choice.

@t8mas062184
Copy link

t8mas062184 commented Jan 14, 2021

Great script. Cheers

I had the same issue where the RunAs account appeared as Incomplete. I'd been working with RunAs accounts a month ago and automated replacement of the assigned Role. Realised this code did not assign the Contributor role, or any other role to the RunAs Account.
Use New-AzRoleAssignment to assign a role of choice.

Hello @ColinEff, can you please elaborate more on the New-AzRoleAssignment thing you introduced to make it work?

@ColinEff
Copy link

Hi @t8mas062184

Snippet from code I used to get around the 'incomplete' issue. It uses a custom role I created for manipulating VMs. Replace the role name with an existing role or custom role. Importantly, a role has to be assigned to the RunAs account

#// -------------------------------------------------------------------   
#// Define automation account variables
#// -------------------------------------------------------------------

$azEnvironment = [PSCustomObject]@{
    #// Global Settings
        Environment                 = "Production"

        SubscriptionId              = "11111111-2222-3333-4444-555555555555"
        Location                    = "Australia East"
        ResourceGroupName           = "company-AUE-PRD-ITOPS"
        AutomationAccountName       = "company-AUE-PRD-ITOPS-AA"
        CustomRoleName              = "Virtual Machine Operations (Custom)" # custom role with perms to manage VM state
        CustomRoleTempFile          = "$scriptPath\az-role.json"
        RunBookName                 = "ITOPS-category-scriptpurpose"
        RunBookScript               = "$scriptPath\az-script.ps1"
        RunBookScriptDependencies   = @("Az.Accounts","Az.Compute","Az.Resources")
        AssignCustomRole            = $False
        KeyVaultName                = "company-AUE-PRD-ITOPS-KV" # Key Vault Name, 24 chars max


        #// Tags for resources (LocalTags) and the Resource Group (GlobalTags)
        GlobalTags                  = @{Environment="Production";Owner="IT Operations Team";Purpose="IT Operations, Management and Automation";Production="True"}

    }

#// -------------------------------------------------------------------   
#// Create RunAs Account
#// -------------------------------------------------------------------
if ($azEnvironment.createRunAsAccount) {
    Write-host "[INFO]  Creating KeyVault and RunAs Account for Runbook $($azEnvironment.RunBookName)"
    $result = Generate-RunAsAccount -ResourceGroupName $azEnvironment.ResourceGroupName `
              -AutomationAccountName $azEnvironment.AutomationAccountName `
              -Location $azEnvironment.Location `
              -KeyVaultName $azEnvironment.KeyVaultName `
              -KeyVaultTag $azEnvironment.GlobalTags
              
    $result
} else {
    Write-host "[INFO]  Disabled. Skipped creating KeyVault and RunAs Account for Runbook $($azEnvironment.RunBookName)"
}

#// -------------------------------------------------------------------
#// Assign Custom Role to Automation Account RunAs Account (replace contributor role)
#// -------------------------------------------------------------------
if ($azEnvironment.assignCustomRole) {
    $Confirm = $False
    Write-host "[INFO]  Assiging custom role $($azEnvironment.CustomRoleName) to RunAs account for AA $($azEnvironment.AutomationAccountName)"

    # Force replace Contributor role if set to True
    $ReplaceCustomRoleAssignment = $True

    # Retrieve Automation Account
    $AutomationAccount = Get-AzAutomationAccount -Name $azEnvironment.AutomationAccountName -ResourceGroupName $azEnvironment.ResourceGroupName

    # Retrieve Automation Account App ID
    $runasAccountAADAplicationId = Get-RunAsAccountAADApplicationId -ResourceGroupName $AutomationAccount.ResourceGroupName -AutomationAccountName $AutomationAccount.AutomationAccountName

    # Replace role assignment
    if ($runasAccountAADAplicationId) { 
        $subscriptionScope = "/subscriptions/" + $azEnvironment.SubscriptionId
        #$subscriptionScope = $SubscriptionPath


        # Retrieve existing role assignments
        if ($ReplaceCustomRoleAssignment -eq $true) {
            $currentRoleAssignments = Get-AzRoleAssignment `
                -ServicePrincipalName $runasAccountAADAplicationId `
                -Scope $subscriptionScope `
                -ErrorAction Stop
        } else {
            # Retrieve 'assumed' contributor role assignment if ReplaceCustomRoleAssignment is false
            $currentRoleAssignments = Get-AzRoleAssignment `
                -ServicePrincipalName $runasAccountAADAplicationId `
                -RoleDefinitionName "Contributor" `
                -Scope $subscriptionScope `
                -ErrorAction Stop
        }

        Write-Host "[INFO]  Role assignments in this automation account:"
        $currentRoleAssignments



        # Check if contributor role is already assigned, if so nothing to do, exit
        $hasContributorRoleAssignment = $false
        foreach ($roleAssignment in $currentRoleAssignments) {
            if ($roleAssignment.RoleDefinitionName -eq "Contributor") {
                $hasContributorRoleAssignment = $true
                Break
            }
        }

        # Start process to replace existing role with contributor role
        if (($hasContributorRoleAssignment -eq $true) -or ($ReplaceCustomRoleAssignment -eq $true)) {
            Write-Host ("[INFO]  Deleting existing role assignments from " + $automationAccount.AutomationAccountName)
            foreach ($roleAssignment in $currentRoleAssignments)
            {
                if (($roleAssignment.RoleDefinitionName -eq "Contributor") -or ($ReplaceCustomRoleAssignment -eq $true))
                {
                    Remove-AzRoleAssignment -InputObject $roleAssignment -ErrorAction Stop
                    Write-Host ("[INFO]  Deleted role assignment to " + $roleAssignment.RoleDefinitionName)
                }
            }

            Write-Host ("[INFO]  Creating new role assignment for " + $automationAccount.AutomationAccountName)
            $customRoleAssignment = New-AzRoleAssignment `
                                -RoleDefinitionName $azEnvironment.CustomRoleName `
                                -ServicePrincipalName $runasAccountAADAplicationId `
                                -ErrorAction Stop
        
            Write-Host "[INFO]  Created a new role assignment:" -ForegroundColor Green
            $customRoleAssignment

        } else {
            Write-Host ("[ERROR] Automation account: " + $automationAccount.AutomationAccountName + " was not changed because Contributor role was not assigned to RunAs account.") -ForegroundColor Yellow
            Write-Host 
        }
    } else {
        Write-Host  ("[ERROR] Automation account: " + $AutomationAccount.AutomationAccountName + " was not changed because RunAs account does not exist.") -ForegroundColor Yellow
        Write-Host
    }    
}

@t8mas062184
Copy link

Thank you for your prompt response, @ColinEff! Creating RunAs Account is my first task in Azure and I find it challenging. This would definitely be a big help. I am creating my Automation Account via Ansible playbook using azure_rm_automationaccount (azcollections) but it lacks the functionality to create the RunAs Account. I'll see how this would be integrated with my Ansible playbook I've formulated for Automation Account creation.

Cheers!

@JonRunheim
Copy link

JonRunheim commented Feb 19, 2021

Hi,

The script needs to be adjusted with the below to solve the deprecation of the .secretvaluetext

#THIS SECTION IS REPLACED ADDED AS THE .secretvaluetext OPERATOR IS DEPRECATED
#$AzKeyVaultCertificatSecretBytes = [System.Convert]::FromBase64String($AzKeyVaultCertificatSecret.SecretValueText)

#THE BELOW IS THE REPLACEMENT
$secretValueText = '';
$ssPtr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($AzKeyVaultCertificatSecret.SecretValue)
try {
$secretValueText = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ssPtr)
} finally {
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ssPtr)
}
$AzKeyVaultCertificatSecretBytes = [Convert]::FromBase64String($secretValueText)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment