Automation Accounts, Logic Apps, Function Apps, and CI/CD
This section maps the five major Azure automation platforms to their security properties, authentication requirements, and appropriate use cases. The goal is to help you choose the right tool for a scenario, configure it with the strongest available credential pattern, and connect it to the right trigger type. By the end of this section you should be able to evaluate an existing automation workload and identify whether it is using the best available authentication and design.
Section 4 learning journey - platform choice through network security
Five automation platforms in scope for this section
Automation platform overview - decision tree for platform choice
Azure Automation Accounts - Runbooks, Schedules, Assets, and Hybrid Workers
Azure Automation Accounts are the most IT-admin-friendly Azure automation platform. They support PowerShell 5.1, PowerShell 7.x, and Python 3 runbooks, and they run on a Microsoft-managed infrastructure. Key use cases include scheduled administrative tasks, event-driven Azure resource management, and hybrid on-premises automation via Hybrid Runbook Workers.
Run As Account retired Sep 30, 2023 - managed identity replaces it
The Run As Account was a certificate-backed service principal that Automation Accounts used to authenticate to Azure. Microsoft retired it on September 30, 2023. Any Automation Account still using a Run As Account must migrate to managed identity. There is no supported path forward that restores Run As Account functionality.
Any Automation Account still using a Run As Account after the September 30, 2023 retirement date is operating on an authentication mechanism that Microsoft no longer supports. Migrate to managed identity immediately.
Managed identity for Automation Accounts - system-assigned vs user-assigned
Automation Accounts support both system-assigned and user-assigned managed identities:
In a runbook, authenticate using the Connect-AzAccount -Identity pattern:
Connect-AzAccount -Identity
# For user-assigned, add: -AccountId "<client-id>"
$secret = Get-AzKeyVaultSecret `
-VaultName "my-keyvault" `
-Name "my-secret" `
-AsPlainText
Get-AzKeyVaultSecret requires the Az.KeyVault module in addition to Az.Accounts. The managed identity also needs a Key Vault data-plane role such as Key Vault Secrets User on the target vault.
Automation Account native source control sync has incomplete support for PowerShell 7.x runbooks. Teams using PS7 should use a CI/CD pipeline (GitHub Actions or Azure DevOps) to deploy runbooks rather than relying on the built-in sync. This limitation does not affect PS 5.1 runbooks.
Hybrid Runbook Workers allow Automation Account runbooks to execute on machines in your on-premises network, in other clouds, or in Azure virtual networks. This enables managing on-premises Active Directory, SQL Server, or other resources that are not reachable from the public internet. Hybrid workers use the Azure Arc agent for connectivity and do not require opening inbound firewall ports.
Azure Logic Apps - trigger, actions, conditions, managed connectors
Logic Apps provide low-code workflow orchestration with hundreds of managed connectors for services like Exchange Online, SharePoint Online, Teams, ServiceNow, and many others. They are the primary platform for Microsoft Sentinel playbooks.
Two hosting models exist:
Logic App connection objects - hidden credential store risks
Logic App connection objects often store OAuth refresh tokens tied to a specific user account. If that user leaves the organization, the connection breaks silently. If the user's account is over-privileged, the Logic App inherits that over-privilege. Audit connector connections regularly and replace user credential connections with managed identity or service principal connections wherever the connector supports it.
Most built-in Azure connectors (Blob Storage, Key Vault, Service Bus) support managed identity. The Standard Logic App tier enables system-assigned managed identity by default.
Logic App managed identity replaces stored OAuth tokens
For connectors that support it, configure authentication at the connection level to use the Logic App's managed identity rather than a stored user credential. This eliminates OAuth token expiry failures and removes the dependency on a specific user account.
Azure Function Apps - trigger sources and three hosting plans
Function Apps are code-based serverless compute that supports C#, Python, JavaScript, and PowerShell. They are the best choice when Logic App connectors do not cover the required integration or when complex data transformation is needed.
Hosting plans:
Function App with managed identity and Application Insights
All Function App hosting plans support managed identity. Enable Application Insights on every Function App used for security automation. Application Insights provides execution telemetry, error tracking, dependency tracing, and performance metrics that are essential for monitoring automation health.
# PowerShell Function App: retrieve secret using managed identity
using namespace System.Net
param($Request, $TriggerMetadata)
Connect-AzAccount -Identity | Out-Null
$secret = Get-AzKeyVaultSecret `
-VaultName $env:KEY_VAULT_NAME `
-Name "api-key" `
-AsPlainText
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = [HttpStatusCode]::OK
Body = "Retrieved successfully"
})
This example assumes the Function App PowerShell worker has both Az.Accounts and Az.KeyVault available through managed dependencies or bundled modules. Without Az.KeyVault, Get-AzKeyVaultSecret will fail even though the managed identity sign-in succeeds.
Azure DevOps Pipelines - WIF service connection as default
Azure DevOps Pipelines is a CI/CD platform for deploying code and infrastructure. Service connections to Azure use Workload Identity Federation as the default and recommended pattern. WIF-based service connections issue OIDC tokens per pipeline run with no stored secret in Azure DevOps.
Use YAML pipelines for all new work. Classic pipelines cannot be stored in source control, reviewed as code, or audited through normal pull request processes.
Managed identity service connections are also available for self-hosted agents running on Azure VMs or Arc-enabled machines.
GitHub Actions is a CI/CD and automation platform. Workload Identity Federation with Entra via OIDC is the modern authentication approach.
Wif Flow
The flow:
https://token.actions.githubusercontent.com/).sub (subject), repository, ref, and environment.Workflow configuration (no secrets stored in GitHub):
permissions:
id-token: write
contents: read
steps:
- uses: azure/login@v3
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
WIF workflow YAML structure - three vars, no secrets
WIF subject claim scope - tight vs wide
Subject claim examples - good narrow scoping vs bad wildcards
Instructor demo - federated credential portal form
A federated credential with an overly permissive subject claim - for example, using a wildcard that matches any branch or any repository - allows any workflow in scope to obtain an Entra access token. This can enable cross-repository privilege escalation if an attacker can trigger a workflow in any matching repository. Always scope subject claims to a specific repository and branch or environment.
Good subject claim examples:
repo:contoso/security-automation:ref:refs/heads/main - only the main branch of one specific reporepo:contoso/security-automation:environment:production - only the production environment of one specific repoBad subject claim example:
repo:org/*:ref:* - any branch in any repo under the organization (overly broad)The decision framework - which automation tool to choose
Discussion: wrong tool for the job - scheduled polling vs event-driven
Use this decision framework:
Authentication best practices - five-point checklist
Sites.Selected over Sites.ReadWrite.All.Network security controls - vNet integration and Private Endpoints with identity layer
Defense-in-depth applies at the network layer in addition to identity controls.
Trigger types overview - five categories and supporting platforms
Alert and incident triggers - SOAR pattern from detection to response
Event-driven triggers - Event Grid, Graph notifications, Service Bus
Match the trigger to the business event:
Have the prepared resource group, repository, and checkpoint screenshots available. Expect audit events and Key Vault RBAC to take a few minutes to settle; retry once before moving to the checkpoint path. Clean up test workflows, Logic Apps, Function Apps, and role assignments created during the lab.
Configure Workload Identity Federation for a GitHub Actions workflow, deploy an HTTP-triggered Logic App you can wire to Entra audit events, and publish a Function App that retrieves a Key Vault secret using managed identity.
gh) optional but helpfulUse one repository throughout this task. The commands below were validated against a disposable private GitHub repository and a disposable Entra application.
Identity > Applications > App registrations > New registration.Lab4-GitHubWIF-<initials> and choose Accounts in this organizational directory only.Certificates & secrets > Federated credentials.GitHub Actions deploying Azure resources, set your GitHub owner and repository, and scope the subject to the main branch.Graph PowerShell:
Create the Entra objects with
Invoke-MgGraphRequestConnect-MgGraph -Scopes "Application.ReadWrite.All","Application.Read.All","AppRoleAssignment.ReadWrite.All" $repoOwner = "<github-owner>" $repoName = "<repo-name>" $displayName = "Lab4-GitHubWIF-$(Get-Date -Format 'yyyyMMddHHmmss')" $app = Invoke-MgGraphRequest ` -Method POST ` -Uri "https://graph.microsoft.com/v1.0/applications" ` -Body @{ displayName = $displayName; signInAudience = "AzureADMyOrg" } $servicePrincipal = Invoke-MgGraphRequest ` -Method POST ` -Uri "https://graph.microsoft.com/v1.0/servicePrincipals" ` -Body @{ appId = $app.appId } Invoke-MgGraphRequest ` -Method POST ` -Uri "https://graph.microsoft.com/v1.0/applications/$($app.id)/federatedIdentityCredentials" ` -Body @{ name = "github-main" issuer = "https://token.actions.githubusercontent.com" subject = "repo:$repoOwner/$repoName:ref:refs/heads/main" audiences = @("api://AzureADTokenExchange") description = "GitHub Actions WIF for main branch" } $graphSp = Invoke-MgGraphRequest ` -Method GET ` -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=appId eq '00000003-0000-0000-c000-000000000000'&`$select=id,appRoles" $organizationReadAllRoleId = ($graphSp.value[0].appRoles | Where-Object { $_.value -eq "Organization.Read.All" -and $_.allowedMemberTypes -contains "Application" } | Select-Object -First 1).id Invoke-MgGraphRequest ` -Method POST ` -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$($servicePrincipal.id)/appRoleAssignments" ` -Body @{ principalId = $servicePrincipal.id resourceId = $graphSp.value[0].id appRoleId = $organizationReadAllRoleId }
Az CLI:
Create the same trust and seed GitHub Actions variables
$repoOwner = "<github-owner>" $repoName = "<repo-name>" $resourceGroupName = "<resource-group>" $displayName = "Lab4-GitHubWIF-$(Get-Date -Format 'yyyyMMddHHmmss')" $app = az ad app create --display-name $displayName --sign-in-audience AzureADMyOrg | ConvertFrom-Json $servicePrincipal = az ad sp create --id $app.appId | ConvertFrom-Json $federatedCredentialBodyPath = Join-Path $env:TEMP "section4-federated-credential.json" (@{ name = "github-main" issuer = "https://token.actions.githubusercontent.com" subject = "repo:$repoOwner/$repoName:ref:refs/heads/main" audiences = @("api://AzureADTokenExchange") description = "GitHub Actions WIF for main branch" } | ConvertTo-Json -Depth 5) | Set-Content -Path $federatedCredentialBodyPath -Encoding ascii az ad app federated-credential create --id $app.id --parameters "@$federatedCredentialBodyPath" | Out-Null $resourceGroupId = az group show --name $resourceGroupName --query id -o tsv az role assignment create ` --assignee-object-id $servicePrincipal.id ` --assignee-principal-type ServicePrincipal ` --role Reader ` --scope $resourceGroupId | Out-Null $graphSpId = az ad sp list ` --filter "appId eq '00000003-0000-0000-c000-000000000000'" ` --query "[0].id" ` -o tsv $organizationReadAllRoleId = az rest ` --method get ` --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$graphSpId/appRoles?`$select=id,value,allowedMemberTypes" ` --query "value[?value=='Organization.Read.All' && contains(allowedMemberTypes, 'Application')].id | [0]" ` --output tsv $appRoleAssignmentBodyPath = Join-Path $env:TEMP "section4-org-read-all.json" (@{ principalId = $servicePrincipal.id resourceId = $graphSpId appRoleId = $organizationReadAllRoleId } | ConvertTo-Json -Compress) | Set-Content -Path $appRoleAssignmentBodyPath -Encoding ascii az rest ` --method post ` --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$($servicePrincipal.id)/appRoleAssignments" ` --headers "Content-Type=application/json" ` --body "@$appRoleAssignmentBodyPath" | Out-Null $tenantId = az account show --query tenantId -o tsv $subscriptionId = az account show --query id -o tsv gh secret set AZURE_CLIENT_ID --repo "$repoOwner/$repoName" --body $app.appId gh secret set AZURE_TENANT_ID --repo "$repoOwner/$repoName" --body $tenantId gh secret set AZURE_SUBSCRIPTION_ID --repo "$repoOwner/$repoName" --body $subscriptionId
name: oidc-validation
on:
workflow_dispatch:
push:
branches:
- main
jobs:
validate:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Show Azure context
run: az account show --query '{subscription:name, tenant:tenantId, user:user.name}' -o json
- name: Read organization from Microsoft Graph
shell: bash
run: |
graph_token=$(az account get-access-token --resource-type ms-graph --query accessToken -o tsv)
curl -sS -H "Authorization: Bearer ${graph_token}" "https://graph.microsoft.com/v1.0/organization?\$select=id,displayName" | jq .
Expected outcomes:
- The workflow authenticates through OIDC with no stored client secret
- az account show succeeds in GitHub Actions
- The Graph organization read returns tenant details
Logic App (Consumption) in the lab resource group.When a HTTP request is received as the trigger and add a response or compose action so you can inspect the payload.Monitoring > Diagnostic settings and send AuditLogs to that callback URL.Native PowerShell:
Deploy a disposable HTTP-triggered Logic App through ARM
$resourceGroupName = "<resource-group>" $logicAppName = "Lab4-AuditLogApp-$(Get-Date -Format 'MMddHHmm')" $location = "<location>" $logicDefinition = @' { "location": "<location>", "properties": { "state": "Enabled", "definition": { "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", "contentVersion": "1.0.0.0", "parameters": {}, "triggers": { "manual": { "type": "Request", "kind": "Http", "inputs": { "schema": { "type": "object", "properties": { "event": { "type": "string" } } } } } }, "actions": { "response": { "type": "Response", "kind": "Http", "runAfter": {}, "inputs": { "statusCode": 200, "body": { "receivedEvent": "@triggerBody()?['event']" } } } }, "outputs": {} }, "parameters": {} } } '@.Replace('<location>', $location) $logicDefinitionPath = Join-Path $env:TEMP "section4-logic-app.json" $logicDefinition | Set-Content -Path $logicDefinitionPath -Encoding utf8 az resource create ` --resource-group $resourceGroupName ` --resource-type Microsoft.Logic/workflows ` --name $logicAppName ` --is-full-object ` --properties "@$logicDefinitionPath"
Az CLI:
Get the callback URL and post a sample event
$subscriptionId = az account show --query id -o tsv $callbackUrl = az rest ` --method post ` --uri "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/<resource-group>/providers/Microsoft.Logic/workflows/<logic-app-name>/triggers/manual/listCallbackUrl?api-version=2019-05-01" ` --query value ` --output tsv Invoke-RestMethod ` -Method Post ` -Uri $callbackUrl ` -Body '{"event":"AuditEvent"}' ` -ContentType "application/json"
Expected outcomes:
- The workflow can be created and tested without the Logic App CLI extension
- The callback URL accepts a sample event immediately
- After you wire Entra diagnostics to the same callback, audit log payloads follow the same path
Key Vault Secrets Officer or Key Vault Administrator before creating the first secret.Key Vault Secrets User on the vault.Az CLI:
Provision the storage account, Key Vault, secret, and Function App identity
$resourceGroupName = "<resource-group>" $location = "<location>" $storageAccountName = "<storage-account>" $keyVaultName = "<key-vault-name>" $functionAppName = "<function-app-name>" az storage account create ` --name $storageAccountName ` --resource-group $resourceGroupName ` --location $location ` --sku Standard_LRS ` --kind StorageV2 az keyvault create ` --name $keyVaultName ` --resource-group $resourceGroupName ` --location $location ` --enable-rbac-authorization true az keyvault secret set ` --vault-name $keyVaultName ` --name DemoSecret ` --value "SuperSecretValue123" az functionapp create ` --name $functionAppName ` --resource-group $resourceGroupName ` --storage-account $storageAccountName ` --consumption-plan-location $location ` --os-type Windows ` --runtime powershell ` --functions-version 4 ` --assign-identity [system] ` --disable-app-insights true $functionPrincipalId = az functionapp identity show ` --name $functionAppName ` --resource-group $resourceGroupName ` --query principalId ` -o tsv $keyVaultId = az keyvault show --name $keyVaultName --query id -o tsv az role assignment create ` --assignee-object-id $functionPrincipalId ` --assignee-principal-type ServicePrincipal ` --role "Key Vault Secrets User" ` --scope $keyVaultId
Native PowerShell:
Package, deploy, and test the PowerShell function
$functionAppName = "<function-app-name>" $resourceGroupName = "<resource-group>" $keyVaultName = "<key-vault-name>" $tempRoot = Join-Path $env:TEMP "section4-functionapp" Remove-Item $tempRoot -Recurse -Force -ErrorAction SilentlyContinue New-Item -ItemType Directory -Path (Join-Path $tempRoot "GetSecret") -Force | Out-Null @' { "version": "2.0" } '@ | Set-Content -Path (Join-Path $tempRoot "host.json") -Encoding utf8 @' { "bindings": [ { "authLevel": "anonymous", "type": "httpTrigger", "direction": "in", "name": "Request", "methods": ["get"] }, { "type": "http", "direction": "out", "name": "Response" } ] } '@ | Set-Content -Path (Join-Path $tempRoot "GetSecret\function.json") -Encoding utf8 @' param($Request) $tokenResponse = Invoke-RestMethod -Method GET -Uri "$($env:IDENTITY_ENDPOINT)?resource=https://vault.azure.net&api-version=2019-08-01" -Headers @{ 'X-IDENTITY-HEADER' = $env:IDENTITY_HEADER } $secretResponse = Invoke-RestMethod -Method GET -Uri "https://$($env:KEY_VAULT_NAME).vault.azure.net/secrets/$($env:SECRET_NAME)?api-version=7.4" -Headers @{ Authorization = "Bearer $($tokenResponse.access_token)" } Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = 200 Body = @{ secretName = $env:SECRET_NAME secretLength = $secretResponse.value.Length } }) '@ | Set-Content -Path (Join-Path $tempRoot "GetSecret\run.ps1") -Encoding utf8 $zipPath = Join-Path $env:TEMP "section4-functionapp.zip" Remove-Item $zipPath -Force -ErrorAction SilentlyContinue Compress-Archive -Path (Join-Path $tempRoot '*') -DestinationPath $zipPath az functionapp config appsettings set ` --name $functionAppName ` --resource-group $resourceGroupName ` --settings KEY_VAULT_NAME=$keyVaultName SECRET_NAME=DemoSecret WEBSITE_RUN_FROM_PACKAGE=1 | Out-Null az functionapp deployment source config-zip ` --name $functionAppName ` --resource-group $resourceGroupName ` --src $zipPath Invoke-RestMethod -Method Get -Uri "https://$functionAppName.azurewebsites.net/api/GetSecret"
Expected outcomes:
- The Function App retrieves the secret through managed identity
- No client secret or Key Vault secret value is stored in configuration
- The HTTP response shows the secret name and length, not the secret itself
id-token: write or OIDC login fails even when the federated credential is correct.Contributor is not enough to create the first Key Vault secret when the vault uses RBAC. You need a data-plane role such as Key Vault Secrets Officer.Section takeaways - five principles for automation tool selection and hardening
Bridge to Section 5 - source control, CI/CD pipelines, SP lifecycle
Volatile platform behavior and dated claims in this section were checked against these current sources on April 26, 2026:
All links below were reviewed on 2026-03-10.
Section 4 - Automation Tools - Reference Guide