Securing Applications and Automation - Training Series
Section 4 of 6
Reference Guide

Automation Tools

Automation Accounts, Logic Apps, Function Apps, and CI/CD


Section
Section 4 - Automation Tools
Document Type
Reference Guide
Format
Instructor-led with guided lab
Audience
Infrastructure and security administrators

Section purpose

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.

Learning Objectives

Platform Choice Managed Identity WIF / CI-CD Credential Auditing Network Security Automation Mastery Journey

Section 4 learning journey - platform choice through network security

Automation platform overview

Automation Accounts Logic Apps Function Apps GitHub Actions Azure DevOps ? Which platforms does your team use?

Five automation platforms in scope for this section

Which automation tool? Needs complex code logic? Yes Function Apps No Tied to code deployment? Yes GitHub / Azure DevOps No Low-code connectors / alerts? Yes Logic Apps No Automation Accounts

Automation platform overview - decision tree for platform choice

Azure Automation Accounts

Automation Account Managed Infrastructure Runbooks PS / Python Schedules & Webhooks Assets Vars / Certs Azure or Arc-enabled host Hybrid Runbook Worker

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 retirement

Run As Account SP cert stored in Automation Sep 30, 2023 RETIRED Managed Identity No stored creds Entra-native Action Required Audit every Automation Account Migrate to managed identity now Run As Account deprecation timeline

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.

⚠️Risk - Run As Account Retirement

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 Automation Account 1 : 1 binding Identity Lifecycle Created & deleted with the account Scope Single Automation Account only Connect Connect-AzAccount -Identity User-Assigned Shared Identity independent resource AA #1 AA #2 Function Lifecycle Persists independently Scope Reusable across multiple resources Connect Connect-AzAccount -Identity -AccountId Grant Azure RBAC roles to the managed identity at narrowest scope Subscription → Resource Group → Resource - always choose the tightest

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
📚Prerequisite Note

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.

PowerShell 7 source control limitation

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

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 (Recurrence / HTTP) Action (Connector call) Condition True False Hundreds of managed connectors

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:

Connection objects as hidden credential stores

User Creates connection OAuth token stored Connection Object refresh_token: *** Logic App Risk 1: User Leaves Connection breaks silently Automation fails undetected Risk 2: Over-Privilege Logic App inherits user permissions via token Fix: Managed Identity No stored token - no expiry risk Audit connections in Azure Portal Connection objects as a credential governance risk

Logic App connection objects - hidden credential store risks

⚠️Risk - Hidden Credentials in Connection Objects

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 pattern

Logic App (Standard) System MI Blob Storage Key Vault Service Bus OAuth Token (not needed) Managed identity replaces stored tokens

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

HTTP/Webhook Timer/Schedule Queue/Service Bus Event Grid Function C# / Python / JS PowerShell Output Result Consumption Cheap Cold start No vNet Premium Pre-warmed vNet ✓ Higher cost Dedicated Full control vNet ✓ Always-on Trigger sources and hosting plan comparison

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:

Managed identity and Application Insights

Function App + Managed Identity System-Assigned MI Key Vault Azure Services Application Insights Telemetry / Errors / Dependencies

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"
})
📚Function App Note

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

Code Repo / PR Build YAML pipeline Deploy Azure target Service Connection: WIF (default) OIDC token exchange - no stored secret federated credential in Entra app registration YAML Pipeline Advantages Versionable · PR-reviewable · Auditable Stored in source control with the code Azure DevOps pipeline with WIF authentication

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

GitHub Actions is a CI/CD and automation platform. Workload Identity Federation with Entra via OIDC is the modern authentication approach.

External IdP(GitHub / DevOps /Kubernetes / Custom) ① OIDC token Microsoft Entra IDValidates issuer,subject, audience Federated CredentialIssuer + Subject filterconfigured on SP/MI ② Access token Azure ResourcesGraph, ARM, KV,Storage, etc. ✓ No Secret✓ No Cert✓ No RotationRequired ⚠ Subject Claim Filterrepo:org/repo:ref:refs/heads/main ← tighten this!

Wif Flow

The flow:

  1. A GitHub Actions workflow requests an OIDC token from GitHub's identity provider (https://token.actions.githubusercontent.com/).
  2. The token contains claims including sub (subject), repository, ref, and environment.
  3. Entra validates the token against the federated credential configuration on the app registration.
  4. If the claims match, Entra issues an access token.
  5. The workflow uses the access token for Azure API calls.

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 }}
on: push / workflow_dispatch permissions: id-token: write - uses: azure/login@v3 client-id: ${{ vars.CLIENT_ID }} No secret stored

WIF workflow YAML structure - three vars, no secrets

WIF subject claim security

Tight Scope repo:org/app:ref:main 1 repo, 1 branch 1 token Wide Scope repo:org/*:ref:* Any repo, any branch ANY workflow Cross-Repo Privilege Escalation Any matching workflow claims Entra token and accesses Azure resources Fix: Scope to branch or environment Never use wildcards in production WIF subject claim scope comparison

WIF subject claim scope - tight vs wide

✓ GOOD - Narrow Subject Claims Branch-scoped repo:contoso/security-automation:ref:refs/heads/main Only main branch can authenticate - PRs cannot Environment-scoped repo:contoso/security-automation:environment:production Requires environment approval gate before token exchange Tag-scoped repo:contoso/security-automation:ref:refs/tags/v* Only tagged releases - good for deployment pipelines ✗ BAD - Overly Broad Subject Claims Wildcard branch repo:contoso/*:ref:refs/heads/* Any repo in org repo:contoso/*:ref:refs/heads/main Any matching workflow can obtain an Entra access token → cross-repo privilege escalation

Subject claim examples - good narrow scoping vs bad wildcards

portal.azure.com - Entra App Registration Issuer: token.actions.githubusercontent.com Subject: repo:org/app:ref:refs/heads/main Audience: api://AzureADTokenExchange Name: github-main-deploy Save Federated credential saved Workflow uses azure/login@v3 - no secret

Instructor demo - federated credential portal form

⚠️Risk - Overly Permissive Subject Claims

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:

Bad subject claim example:

Choosing the right tool

Which automation tool? Needs complex code logic? Yes Function Apps No Tied to code deployment? Yes GitHub / Azure DevOps No Low-code connectors / alerts? Yes Logic Apps No Automation Accounts

The decision framework - which automation tool to choose

Scheduled Polling Latency: up to 15 min Runs even when nothing changed Event-Driven Latency: seconds Fires only when event occurs Event Sources Event Grid Graph Change Notifications Service Bus Sentinel Automation Rules Use Cases Secret expiry alerts User lifecycle changes Incident response Resource changes Scheduled poll vs. event-driven trigger

Discussion: wrong tool for the job - scheduled polling vs event-driven

Use this decision framework:

Authentication and authorization best practices

Managed Identity first WIF for CI/CD - no secrets in pipelines Least privilege scope Secrets in Key Vault only Expiry monitoring on all SP certs Auth Hardened

Authentication best practices - five-point checklist

Network security controls

Virtual Network (vNet Integration) Standard Logic App vNet integrated outbound → private Premium Function vNet integrated outbound → private Automation Account Hybrid Worker on vNet outbound → private Key Vault Private Endpoint Storage Account Private Endpoint Service Bus Private Endpoint Public access: OFF Public access: OFF Public access: OFF Identity Layer Managed Identity No stored credentials Workload ID CA IP-based restrictions on service principals Azure RBAC Least privilege roles Defense in depth: Network isolation + Identity controls + Least privilege They are layers, not alternatives - apply all three

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

Scheduled Cron, recurrence AA · Logic · Functions Alert / Incident Sentinel playbooks Logic Apps Event-Driven Event Grid, Service Bus Logic · Functions CI/CD Pipeline Code push, PR merge DevOps · GitHub HTTP/Webhook External integrations Logic · Functions

Trigger types overview - five categories and supporting platforms

Detection Sentinel / Monitor Alert / Incident NRT analytics rule Auto Rule Logic App Playbook Triage · Notify · Contain · Enrich Notify Teams / Email PagerDuty Contain Revoke tokens Block account Enrich Threat intel User context SOAR pattern: detection to automated response

Alert and incident triggers - SOAR pattern from detection to response

Event Grid Key Vault Secret Expiry Storage Blob Events Graph API Change Notification Service Bus High-volume Logic App / Function App Event sources feeding automation

Event-driven triggers - Event Grid, Graph notifications, Service Bus

Match the trigger to the business event:

Lab readiness notes

🧪Lab Readiness

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.

Guided lab

Lab goal

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.

Prerequisites

Task 1: Configure WIF for a GitHub Actions workflow

Use one repository throughout this task. The commands below were validated against a disposable private GitHub repository and a disposable Entra application.

Entra Portal:

Create the app registration and federated credential
  1. Go to Identity > Applications > App registrations > New registration.
  2. Name the app Lab4-GitHubWIF-<initials> and choose Accounts in this organizational directory only.
  3. After the app is created, go to Certificates & secrets > Federated credentials.
  4. Choose GitHub Actions deploying Azure resources, set your GitHub owner and repository, and scope the subject to the main branch.
  5. Record the application (client) ID and tenant ID for GitHub Actions variables.

Graph PowerShell:

Create the Entra objects with Invoke-MgGraphRequest
Connect-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

GitHub Actions:

Validate Azure login and a Graph organization read with no client secret
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

Task 2: Build a Logic App with an Entra audit log trigger

Azure Portal:

Create the workflow and point Entra diagnostics at the callback URL
  1. Create a Logic App (Consumption) in the lab resource group.
  2. Use When a HTTP request is received as the trigger and add a response or compose action so you can inspect the payload.
  3. Save the workflow and copy the generated callback URL.
  4. In Entra, go to 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

Task 3: Function App with managed identity calling Key Vault

Azure Portal:

Create the Key Vault secret and assign the Function App identity
  1. Create the Key Vault and the Function App.
  2. Enable the Function App system-assigned managed identity.
  3. If the vault uses RBAC, grant yourself Key Vault Secrets Officer or Key Vault Administrator before creating the first secret.
  4. Grant the Function App managed identity 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

Common pitfalls

Admin takeaways

1 Migrate Run As Accounts Audit every Automation Account - retired Sep 2023 2 WIF for CI/CD - no secrets Scope subject claims to repo+branch or environment 3 Audit Logic App connections Replace user OAuth tokens with managed identity 4 Network controls required vNet integration + Private Endpoints for automation 5 Event-driven over scheduled polling Match trigger to event - polling is a last resort Section 4 key principles

Section takeaways - five principles for automation tool selection and hardening

Quick recap questions

  1. Why was the Automation Account Run As Account retired and what should replace it?
  2. What is the security risk of an overly permissive WIF subject claim?
  3. Where do Logic App connection objects store credentials and why is this a governance concern?
  4. What two network controls can restrict outbound traffic from a Function App to only private network paths?
  5. Which trigger type is most appropriate for a Sentinel playbook that responds to security alerts?
  6. When should you choose an Automation Account over a Function App?

Key reminders

Source Control CI/CD Pipelines SP Lifecycle Section 4 Bridging to Section 5

Bridge to Section 5 - source control, CI/CD pipelines, SP lifecycle

Current source notes

Volatile platform behavior and dated claims in this section were checked against these current sources on April 26, 2026:

References

References Appendix

All links below were reviewed on 2026-03-10.

Azure Automation Accounts

Azure Logic Apps

Azure Function Apps

Workload Identity Federation

Azure DevOps Pipelines

Triggers and event-driven automation

Network security

Section 4 - Automation Tools - Reference Guide