Download the Deployment Scripts. Download the Warewolf Azure Execution Engine deployment scripts from warewolf.io/release-notes.php and extract the
.zipso that the scripts land inD:\ExecutionEngine\Scripts. All examples in this article assume that path.
This article is a companion to How to configure Client Apps with Azure Entra Id and Easy Auth, narrowed to one specific, very common scenario: one Azure Function App calling another Azure Function App — where the caller is your own app and the callee is the Warewolf Execution Engine. It is the full copy-paste sequence from docs/Deploy-EndToEnd-Runbook.md, shipped alongside the deployment scripts, reproduced here end to end with explanation.
Scenario
Two Azure Function Apps, two separate Entra app registrations, one HTTP call between them, secured with Entra ID + Easy Auth and no secrets at rest:
Caller Function App Warewolf Execution Engine (Function App)
───────────────────── ──────────────────────────────────────
System-assigned Managed Identity Easy Auth (Entra ID) on the Function App
│ │
│ 1. acquire app-only token │
│ scope: api://<ResourceAppId>/.default │
▼ │
Microsoft Entra ID ────────────────────────────────────────┤
│ token: aud=api://<ResourceAppId>, │
│ roles=["Warewolf_ClientApps"] │
▼ ▼
GET /secure/{workflow}.json ────────────────────► validates token + secure.config
Authorization: Bearer <token> returns workflow JSON
Both the caller and the engine in this walkthrough are the AzureFunction reference example and the Warewolf Execution Engine respectively — see Dev\Warewolf.Execution.Lightweight.ClientExamples\AzureFunction\README.md for the caller’s own code-level detail (typed HTTP client, token handler, exception-handling middleware). This article focuses on the Entra ID/Azure provisioning steps that connect the two.
The authorization contract (read this first)
An app-only caller — this is exactly what a Function App’s Managed Identity is — is authorized by two things that must both be true. Skipping either one produces a denial, not a helpful error:
| Side | What it means | Where it lives |
|---|---|---|
| Token | The caller’s Managed Identity is assigned the engine’s Warewolf_ClientApps app role, so its access token carries roles: ["Warewolf_ClientApps"]. A roleless caller is rejected outright. |
Entra ID — app role assignment on the engine’s service principal |
| Policy | The engine’s permission store has a per-workflow row granting that role Execute=true (and View=true). |
secure.config — WindowsGroupPermissions, WindowsGroup = "Warewolf_ClientApps", IsServer=false |
Permissions are resolved from secure.config at request time — never from the token’s claims alone. A denial currently surfaces as HTTP 500 with a nested Error{…} body rather than a clean 403 (tracked as WOLF-8418); don’t mistake that for a server bug when you see it — check both sides of the contract above first. The shipped Deploy-WwExecutionEngine.authconfig.example.json and secure.config.example.json already include a working Warewolf_ClientApps pair, so this works out of the box for the example workflows if you deploy from the examples unmodified.
Prerequisites
| Tool | Minimum version | Why |
|---|---|---|
| PowerShell | 7.0+ | The scripts declare #Requires -Version 7.0 — Windows PowerShell 5.1 will not run them. |
Azure CLI (az) |
2.55.0+ | All cloud provisioning runs through az. Must be logged in (az login). |
Azure Functions Core Tools (func) |
4.x | Only needed if you publish the client app with func azure functionapp publish. |
| Role the signed-in user needs | Why |
|---|---|
| Contributor (+ User Access Administrator if a Key Vault is involved) on the subscription/resource group | Control-plane: create resource groups, storage, Function Apps |
Application Administrator (or Graph Application.ReadWrite.All) |
Create/patch Entra app registrations, app roles, and app-role assignments |
Run PowerShell 7 as Administrator — some
azand role-assignment operations require elevation.
Step 1 — Deploy the Execution Engine
If the engine is already deployed, skip to Step 1c to capture its identifiers. Otherwise, see Configure-WwExecutionAuth.ps1 and docs/Deploy-RunGuide.md for the full parameter reference — this is the minimum path.
1a. Session setup (run once)
az login
# Resolve tenant + subscription from the current az context
$TenantId = az account show --query tenantId -o tsv
$SubscriptionId = az account show --query id -o tsv
# ── Engine targeting ────────────────────────────────────────────────────────
$ResourceGroup = '<your-resource-group>' # e.g. DEV2
$Location = 'southafricanorth'
$StorageAccount = '<your-storage-account>' # 3-24 lowercase chars
$AppName = '<your-function-app-name>' # engine app, globally unique
# ── Engine inputs (already-published package + config files) ─────────────────
$ScriptsDir = '<path>\ExecutionEngine\Scripts'
$PublishPath = '<path>\ExecutionEngine\AzureFunctionsPackage-<version>.zip' # folder or .zip
$AuthConfigPath = "$ScriptsDir\Deploy-WwExecutionEngine.authconfig.json"
$SecureConfigPath = 'C:\ProgramData\Warewolf\Server Settings\secure.config' # or plaintext JSON (auto-encrypted)
$WorkflowsSourcePath = 'C:\ProgramData\Warewolf\Resources'
$LicenseConfigPath = '<path>\ExecutionEngine\Warewolf License.secureconfig'
$ElasticsearchSourcePath = '<path>\ExecutionEngine\ElasticsearchLoggingSource.bite'
$KeyVaultName = '<your-key-vault-name>'
$KeyVaultSecretName = '<your-kv-secret-name>'
$ExecutionLogLevel = 'ERROR' # TRACE|DEBUG|INFO|WARN|ERROR|FATAL|OFF
$LogDir = "$ScriptsDir\logs"
az account set --subscription $SubscriptionId
Set-Location $ScriptsDir
Copy Deploy-WwExecutionEngine.authconfig.example.json to $AuthConfigPath and confirm GroupPermissions includes "Warewolf_ClientApps": [], and confirm secure.config (or its example) has the matching per-workflow Execute=true row for WindowsGroup = "Warewolf_ClientApps". Both files must agree — see the contract above.
1b. Deploy
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
Start-Transcript -Path "$LogDir\deploy-$ts.log" | Out-Null
.\Deploy-WwExecutionEngine.ps1 `
-ResourceGroup $ResourceGroup -Location $Location `
-StorageAccount $StorageAccount -AppName $AppName `
-PublishPath $PublishPath `
-AuthConfigPath $AuthConfigPath `
-SecureConfigPath $SecureConfigPath `
-WorkflowsSourcePath $WorkflowsSourcePath `
-LicenseConfigPath $LicenseConfigPath `
-EncryptResources:$true -VerifyDecryption `
-KeyVaultName $KeyVaultName -KeyVaultSecretName $KeyVaultSecretName `
-EnableAppInsights:$true `
-ExecutionLogLevel $ExecutionLogLevel `
-EnableElasticsearch:$false `
-LogDir $LogDir `
-PublishMethod Zip `
-NonInteractive
Stop-Transcript | Out-Null
This provisions the resource group, storage, Function App (dotnet-isolated 8, Functions v4, HTTPS-only), the engine’s Entra app registration + service principal + OAuth scope + one app role per GroupPermissions key (including Warewolf_ClientApps), and Easy Auth.
1c. Capture the engine’s identifiers
$ResourceAppId = az webapp auth show -g $ResourceGroup -n $AppName `
--query 'identityProviders.azureActiveDirectory.registration.clientId' -o tsv
$EngineUrl = "https://$AppName.azurewebsites.net"
az webapp auth show‘s output shape varies by CLI extension version. If the query above returns blank, retry with a properties. prefix: --query 'properties.identityProviders.azureActiveDirectory.registration.clientId'.
1d. Smoke-test the engine on its own
# Public route, no auth — should return workflow JSON
curl "$EngineUrl/public/Hello%20World.json?Name=Anon"
# Secure route, no token — should be 401/redirect, proving auth is enforced
curl -i "$EngineUrl/secure/Hello%20World.json?Name=Anon"
Step 2 — Create the caller Azure Function App
Skip this step if you already have a Function App that will make the call — note its name and resource group and use them in Step 3 instead.
$ClientRg = $ResourceGroup # or a separate resource group
$ClientStorage = '<client-storage-account>'
$ClientAppName = '<client-function-app-name>'
az group create --name $ClientRg --location $Location | Out-Null
az storage account create --name $ClientStorage --resource-group $ClientRg `
--location $Location --sku Standard_LRS --kind StorageV2 --min-tls-version TLS1_2 | Out-Null
az functionapp create --name $ClientAppName --resource-group $ClientRg `
--storage-account $ClientStorage --consumption-plan-location $Location `
--runtime dotnet-isolated --runtime-version 8 --functions-version 4 `
--https-only true --os-type Windows | Out-Null
Publish the caller’s own code to it — the AzureFunction reference example (Dev\Warewolf.Execution.Lightweight.ClientExamples\AzureFunction) is a ready-made starting point:
cd Dev\Warewolf.Execution.Lightweight.ClientExamples\AzureFunction
func azure functionapp publish $ClientAppName
It authenticates with DefaultAzureCredential, which resolves to Managed Identity in Azure automatically — no secret is ever stored on this app.
Step 3 — Register the caller as a Daemon
This does the two things the authorization contract requires in a single operation: enables the caller Function App’s system-assigned Managed Identity, and assigns it the Warewolf_ClientApps role on the engine. Three equivalent options, in order of preference:
Option A — orchestrator script (recommended for the AzureFunction example)
.\Configure-WwExecutionAuth-ClientApps.ps1 `
-ResourceAppId $ResourceAppId -TenantId $TenantId `
-Apps azurefunction `
-AzureFunctionClientAppName $ClientAppName `
-AzureFunctionClientResourceGroup $ClientRg `
-FunctionAppName $AppName -FunctionAppResourceGroup $ResourceGroup `
-NonInteractive
-AppRolesToAssign is not passed here — for the daemon path this orchestrator defaults it to Warewolf_ClientApps and fails loudly if that role doesn’t exist on the engine (i.e. if Step 1a’s auth config edit was skipped).
Option B — by-type script (single daemon registration, framework-agnostic)
.\Configure-WwExecutionAuth-Clients.ps1 `
-ResourceAppId $ResourceAppId -TenantId $TenantId `
-ClientType Daemon -DaemonUseManagedIdentity `
-DaemonFunctionAppName $ClientAppName -DaemonFunctionAppResourceGroup $ClientRg `
-NonInteractive
Unlike the orchestrator, -AppRolesToAssign has no default on this script — pass -AppRolesToAssign 'Warewolf_ClientApps' explicitly if you call it this way. If you already know the Managed Identity’s service-principal object id, pass -ManagedIdentityObjectId <id> instead of the Function App name/resource group pair.
Option C — raw az/Graph REST calls (manual equivalent, for troubleshooting)
# Step 1 — enable the caller's system-assigned Managed Identity and read its principalId
$CallerPrincipalId = az functionapp identity assign `
--name $ClientAppName --resource-group $ClientRg --query principalId -o tsv
# Step 2 — resolve the engine SP + the Warewolf_ClientApps app-role id, then assign it
$EngineSpId = az ad sp show --id $ResourceAppId --query id -o tsv
$AppRoleId = az ad sp show --id $ResourceAppId `
--query "appRoles[?value=='Warewolf_ClientApps'].id | [0]" -o tsv
if ([string]::IsNullOrWhiteSpace($AppRoleId)) {
throw "Warewolf_ClientApps app role not found on the engine. Add it to the auth config and re-deploy (see Step 1a)."
}
$body = @{ principalId = $CallerPrincipalId; resourceId = $EngineSpId; appRoleId = $AppRoleId } |
ConvertTo-Json -Compress
$bodyFile = [System.IO.Path]::GetTempFileName()
[System.IO.File]::WriteAllText($bodyFile, $body, (New-Object System.Text.UTF8Encoding $false))
az rest --method POST `
--url "https://graph.microsoft.com/v1.0/servicePrincipals/$EngineSpId/appRoleAssignedTo" `
--headers "Content-Type=application/json" --body "@$bodyFile"
Remove-Item $bodyFile -Force
Options A and B do exactly this under the hood, plus fail-loud role resolution, replication-wait retries, and a masked summary JSON on disk — prefer them over Option C unless you specifically need the raw calls for debugging.
Step 4 — Point the caller at the engine, and test
az functionapp config appsettings set --name $ClientAppName --resource-group $ClientRg --settings `
"WwExecution:BaseUrl=$EngineUrl" `
"WwExecution:TenantId=$TenantId" `
"WwExecution:ResourceAppId=$ResourceAppId" | Out-Null
# WwExecution:Scope is optional — it defaults to api://
# Leave WwExecution:ClientId / :ClientSecret unset in Azure — the Managed Identity handles auth.
Test through the caller’s own proxy endpoint (the AzureFunction example exposes GET|POST /api/run/{workflow}, which forwards to the engine’s /secure/{workflow}.json):
curl "https://$ClientAppName.azurewebsites.net/api/run/Hello%20World?Name=FromDaemon"
Expected: the engine’s JSON for the workflow, echoing your query string.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
401 from the engine |
Token aud mismatch |
Confirm WwExecution:ResourceAppId resolves to api://<ResourceAppId> as the audience. |
403 / 500 denial |
Caller’s Managed Identity has no Warewolf_ClientApps role, or secure.config has no matching Execute=true row for that workflow |
Re-run Step 3; confirm the secure.config row from Step 1a. Denials are wrapped as HTTP 500 (WOLF-8418), not a clean 403. |
DefaultAzureCredential fails locally |
No Managed Identity outside Azure, and no fallback credential | Run az login locally, or set WwExecution:ClientId/:ClientSecret for the MSAL client-credentials fallback (dev only). |
| Role assignment succeeds but calls still fail immediately after | Entra directory replication lag | Wait 1–2 minutes and retry — the provisioning scripts already build in a replication-wait retry for this. |
404 on a workflow path |
Workflow name wrong, or not deployed to the engine | Verify the exact name (spaces are URL-encoded automatically); check the engine’s /apis.json discovery route. |
Tearing it down
# Client registration / role assignment (safe to re-run)
.\Remove-WwExecutionAuth-Clients.ps1 -ResourceAppId $ResourceAppId -TenantId $TenantId -ClientType Daemon
# Engine deployment — preview by default, only removes what that run created
$summary = (Get-ChildItem "$LogDir\deploy-WwExecutionEngine-*.summary.json" |
Where-Object { $_.Name -notlike '*dryrun*' } | Sort-Object LastWriteTime | Select-Object -Last 1).FullName
.\Rollback-WwExecutionEngine.ps1 -SummaryPath $summary
See also
All
Scripts/*.ps1anddocs/*.mdfiles referenced below ship inside the Warewolf Azure Execution Engine deployment scripts.zip, downloadable from warewolf.io/release-notes.php. TheAzureFunctionclient example ships separately in source.
docs/Deploy-EndToEnd-Runbook.md— the full copy-paste runbook this article is drawn from, including every option and detail not repeated here.docs/Deploy-RunGuide.md— full engine-deploy parameter reference, role/privilege matrix, and encryption detail.Dev\Warewolf.Execution.Lightweight.ClientExamples\AzureFunction\README.md— the caller sample’s own architecture, configuration keys, and code-level troubleshooting.- How to configure Client Apps with Azure Entra Id and Easy Auth — the parent article covering all six client types (SPA, Confidential, Daemon, Console).
- Configure-WwExecutionAuth.ps1 — provisions the engine’s Entra app + Easy Auth + interactive-user groups.
- Security — secure.config — the permission model every role above resolves against.




