The shell script for Azure app registration
You can run this helper shell script to set up your resources and retrieve their IDs for use while creating your Azure BYOA outpost. This page provides technical, "read-me style" details about the scr
The script described and included on this page is an example of how to provision the Azure resources that the Bring Your Own App (BYOA) feature needs. You can run it as-is, or treat it as a reference and create the equivalent resources by hand or via your own IaC.
The script creates the Entra-side identities and delegates the minimum Azure RBAC the Terraform runner needs, so that the subsequent terraform apply can attach all Federated Identity Credentials (FICs) without ever holding a client secret or any Entra-level powers of its own.
README-style details
The script supports the following modes:
A, app registration only
(default)
App registration and service principal
Ownership of the app registration
B, app registration and scanner identities
--add-uamis
All of Mode A, and the scanner User-Assigned Managed Identities (UAMIs) (agentless, dspm, registry, serverless, proxy) in a customer-owned resource group
App registration ownership and Managed Identity Contributor on the UAMI resource group
Mode B additionally grants the BYO app registration's service principal the Managed Identity Operator role on the UAMI resource group, which Cortex's scanner-dispatcher needs at scan time to attach BYO UAMIs to scanner workload VMs. Without it, scans won't complete and stay stuck in Azure error 403 LinkedAuthorizationFailed).
The script prints a tfvars-style block to stdout; you wll paste those values into the Cortex outpost onboarding UI to continue the deployment.
Principals and permissions
The BYOA flow involves several principals with deliberately disjoint, least-privilege permission sets. There is no special "admin" role, whoever runs this script just needs the permissions listed below. The following table maps each principal to what it needs, who grants it, and at which scope.
Terraform runner (human or CI service principal, the one running terraform apply)
Entra: Owner of this one app registration. Mode B also: Managed Identity Contributor on this one UAMI resource group. Sub: Whatever the outpost Terraform itself needs (typically Contributor on the monitored sub, out of scope for this script).
This script (--tf-runner-object-id)
The one app registration and, in Mode B, the one UAMI resource group
BYO app registration / service principal (non-human, created by this script)
Sub: Read and scan roles on the monitored sub (granted via the Cortex onboarding URL, out of scope for this script). Mode B also: Managed Identity Operator on the UAMI resource group (so the scanner-dispatcher can attach BYO UAMIs to scanner VMs).
This script grants Managed Identity Operator (Mode B only). Sub-level scan roles are granted via the onboarding URL after terraform apply.
UAMI resource group (this script) and monitored sub (onboarding URL)
Scanner UAMIs (non-human, created by this script in Mode B)
Sub: Workload roles on the monitored sub (granted by the outpost Terraform later, not by this script). FIC trust is set up by terraform apply.
terraform apply (FICs) and outpost Terraform modules (workload roles)
Monitored sub and resource groups
Who runs this script vs. who runs Terraform
The following activities happen, possibly by different identities (they may be the same identity):
Running this script
Terraform runner (terraform apply)
Mode A needs: Entra: Application Developer (or equivalent, see below)
Mode A gets: Owner on the app registration (granted by this script)
Mode B also needs: Azure RBAC on the UAMI resource group: Contributor and User Access Administrator (or Owner)
Mode B also gets: Managed Identity Contributor on the UAMI resource group (granted by this script)
Why the split exists
The split reflects a least-privilege model that keeps powerful roles out of long-lived automation identities.
You typically don't want the Terraform runner (especially a CI service principal stored in a pipeline secret store) to hold tenant-wide Application Developer or sub-wide User Access Administrator. Those are powerful, broad roles. So the model is:
The person running this script holds the powerful, tenant- or sub-scoped permissions, but only briefly, to run it once.
The script translates those into narrow, resource-scoped grants on the Terraform runner: Owner of one app registration, Managed Identity Contributor on one resource group.
The Terraform runner can then attach all FICs at
terraform applytime, and reuse the same RBAC for every subsequentapplyordestroy, without ever needing to be re-elevated.
The --tf-runner-object-id flag is the linchpin: It's how the script delegates app-registration ownership (and in Mode B, UAMI resource group write) to the Terraform runner. Get this wrong (wrong GUID, wrong tenant, wrong kind of ID) and terraform apply will fail with Insufficient privileges on the FIC resources. See Troubleshooting.
How to identify each principal
Use the following commands to look up the object ID for each principal type.
Terraform runner (user)
az ad signed-in-user show --query id -o tsv (run as that user)
Terraform runner (CI service principal)
az ad sp show --id <sp-client-id> --query id -o tsv
BYO app registration service principal (after running this script)
az ad sp show --id <customer_app_client_id> --query id -o tsv (returns customer_sp_object_id)
Scanner UAMIs (after running this script in Mode B)
az identity show --name <prefix>-<role> --resource-group <RG> --query id -o tsv
Prerequisites
Before running the script, ensure the following tooling and identifiers are available.
Azure CLI (
az), logged in (az login) against the app registration's home tenant (usually the customer tenant, not necessarily the monitored-subscription tenant). For Mode B that tenant must also home the target subscription.POSIX
sh: macOS, Linux, or Windows via Git Bash or WSL2.object_id: The GUID of the identity that will run
terraform apply. See Usage below for how to look it up.
Required permissions
The permissions the script-runner needs depend on the mode you invoke.
Mode A (app registration only):
Entra
Application Developer (cf1c38e5-3621-4004-a7cb-879624dced7c)
Create app registrations and own or manage the ones you created. Covers az ad app create, az ad sp create, and az ad app owner add.
Azure RBAC
None
This mode never calls ARM.
By default, any user can register applications, unless the tenant has set Microsoft Entra ID > User settings > Users can register applications to No. If so, an admin (Cloud Application Administrator, Application Administrator, or Global Administrator) must either flip the toggle, assign you Application Developer, or run the script for you.
Adding a service principal (vs. a user) as app registration Owner may additionally require the script-runner to hold the Application Administrator directory role.
Mode B (--add-uamis), all of Mode A, and one of:
Easy: Contributor and User Access Administrator at subscription scope (or just Owner, which includes both).
Least-privilege:
Pre-create the UAMI resource group out-of-band.
Grant the script-runner Managed Identity Contributor on that resource group.
Grant the script-runner User Access Administrator on that resource group (so the script can grant the Terraform runner Managed Identity Contributor on the same resource group).
The Terraform-runner identity itself needs no special tenant-wide permissions. Being an Owner of this one app registration (Mode A) and Managed Identity Contributor on this one resource group (Mode B) is enough for it to attach all FICs.
What the script grants, and why
The script makes the following permission grants. Each is the minimum needed for a later stage of the BYOA flow to work, nothing is granted "just in case". Below is what each grant is, who receives it, and the concrete failure you'd hit without it.
App registration ownership
Terraform runner
The one app registration
Owners of an app registration can add and remove credentials on it. The Terraform runner attaches Federated Identity Credentials (FICs) to the app registration at apply time, federating Cortex's GCP service account into the app registration so Cortex can authenticate without a client secret. Only an Owner, or a directory admin, may write FICs.
terraform apply fails with Insufficient privileges when creating the azuread_application_federated_identity_credential resources.
Managed Identity Contributor (Mode B)
Terraform runner
The UAMI resource group
Each scanner UAMI needs a self-FIC, a federated credential on the UAMI itself, trusting Cortex's GCP service account. Writing a FIC onto a UAMI is a write operation on the UAMI resource, which this role grants. This is what lets the Terraform runner attach the UAMI FICs without being a sub-Owner.
terraform apply fails with AuthorizationFailed on the UAMI FIC or write operations.
Managed Identity Operator (Mode B)
BYO app registration's service principal
The UAMI resource group
At scan time, not apply time, Cortex's scanner-dispatcher, acting as the BYO app registration service principal, creates scanner workload VMs with a UAMI attached. Azure validates "may this principal attach this identity?" via Microsoft.ManagedIdentity/userAssignedIdentities/assign/action, which is exactly what Managed Identity Operator grants. For Cortex-managed UAMIs this is implicit. For BYO UAMIs in a customer-owned resource group it must be granted explicitly.
Scans get stuck in error state with Azure 403 LinkedAuthorizationFailed on .../userAssignedIdentities/assign/action.
Why these specific grants and not broader roles:
App registration ownership instead of a directory role (for example, Application Administrator): Ownership is scoped to one app registration, so the Terraform runner can manage credentials on that single app and nothing else in the directory.
Managed Identity Contributor instead of Contributor or Owner on the resource group: It grants UAMI CRUD (enough to write the self-FICs) but not unrelated resource or role-assignment powers.
Managed Identity Operator instead of Contributor on the resource group: It grants only the
assign/actionthe scanner-dispatcher needs to attach UAMIs to VMs, not the ability to modify the UAMIs themselves.
What the script does not grant: No Microsoft Graph application permissions on the home tenant (the app registration never authenticates to itself), no admin consent on the monitored tenant (done later via the Cortex onboarding flow), and no sub-level scan or workload roles on the app registration or UAMIs (those are granted by the outpost Terraform or onboarding flow, not here).
Usage
Run the script with the arguments that match the mode you want, as shown below.
Look up the object_id (not the client_id) of the Terraform runner:
Mode A, app registration only
This mode creates the app registration and its service principal, and adds the Terraform runner as Owner.
Example:
Mode B, app registration and scanner identities
This mode does everything Mode A does, and additionally creates the scanner UAMIs in a customer-owned resource group and grants the roles Mode B requires.
Example:
Flags
The following flags are supported.
--app-name <NAME>
Required. Display name of the new app registration. Must be unique in the tenant.
--tf-runner-object-id <GUID>
Required. Object ID of the user or service principal that will run terraform apply. Added as Owner of the app registration. With --add-uamis, also granted Managed Identity Contributor on the UAMI resource group.
--add-uamis
Enable Mode B. Requires --uami-subscription, --uami-resource-group, and --uami-location.
--uami-subscription <GUID>
Mode B. Azure subscription ID where the UAMIs will be created.
--uami-resource-group <NAME>
Mode B. Resource group that will hold the UAMIs. Created if it doesn't exist.
--uami-location <REGION>
Mode B. Azure region for the resource group and UAMIs, for example australiaeast or eastus.
--uami-name-prefix <PREFIX>
Mode B, optional. Prefix for UAMI names. Default: cortex. Each UAMI is named <prefix>-<role>, for example cortex-agentless.
--copy-to-clipboard
Also copy the tfvars output to the system clipboard. Auto-detects pbcopy, wl-copy, xclip, xsel, or clip.exe.
--rollback --app-client-id <APP_ID> [--uami-* ...]
Manually delete a previously created app registration and service principal, and Mode B UAMIs. See Rollback.
-h, --help
Show usage.
Output
The script separates data from diagnostics so that its output can be piped or redirected.
The tfvars lines are written to stdout; all diagnostics go to stderr, so the output is pipe-safe.
Mode A:
Mode B:
Paste these values into the Cortex outpost onboarding UI to continue the deployment. The UI drives the Terraform run that attaches the Federated Identity Credentials.
In Mode B, Terraform creates all Federated Identity Credentials (on the app registration and as UAMI self-FICs). No manual az federated-credential create commands are required.
Once the deployment completes, follow the Cortex onboarding flow to grant admin consent in the monitored tenant.
Rollback
The script supports both automatic rollback on failure and a manual rollback path.
On failure the script auto-rolls-back: An EXIT trap removes anything it created in this run (role assignments, UAMIs, app registration, in reverse order).
Manually, after a successful run:
Troubleshooting
The following sections cover the non-obvious, BYO-specific failures. Generic issues (az not installed, not logged in, etc.) are clear from the script's own error output.
does not resolve to any user or service principal in this Azure AD tenant
The GUID is well-formed but doesn't match anything in the current tenant. Either:
You're logged into the wrong tenant,
az login --tenant <tenant-id>and retry, orYou pasted the wrong kind of GUID (for example,
client_idinstead ofobject_id, or a subscription, tenant, or managed-identity resource ID). Re-derive with the lookup commands in Usage.
Insufficient privileges on az ad app create
Tenant policy "Users can register applications = No". See Required permissions, the script cannot work around this.
Insufficient privileges on az ad app owner add
You're trying to add a service principal as Owner and lack the Application Administrator role. Either add a user instead, or have an Application Administrator run it for you. The script's EXIT trap will roll the app registration back so you can retry cleanly.
failed to grant 'Managed Identity Contributor' to TF runner (Mode B)
You lack User Access Administrator (or Owner) on the UAMI resource group or subscription, those are the only roles that can create role assignments. Either:
Have a sub-Owner run the script, or
Pre-grant the script-runner User Access Administrator on the (pre-created) UAMI resource group and use the least-privilege option in Required permissions.
The EXIT trap will roll back the UAMIs and app registration so you can retry cleanly.
failed to grant 'Managed Identity Operator' to BYO AppReg SP (Mode B)
Same root cause as above (missing User Access Administrator). Without this role grant, scans will later fail with 403 LinkedAuthorizationFailed on Microsoft.ManagedIdentity/userAssignedIdentities/assign/action, the script fails-fast here on purpose rather than leaving you to discover it at scan-time.
terraform apply later fails with Insufficient privileges on FIC resources
The Terraform-runner is not actually an Owner of the app registration. Verify and fix:
terraform apply later fails with AuthorizationFailed on UAMI write (Mode B)
The Terraform-runner doesn't have Managed Identity Contributor on the UAMI resource group. Verify and fix:
terraform plan fails with sign_in_audience must be 'AzureADMultipleOrgs'
The app registration was created single-tenant (for example, via the Portal with defaults). The postcondition in data-azuread-customer.tf blocks the plan. Recreate with this script (which defaults to multi-tenant):
terraform plan fails with customer_sp_object_id ... is the SP of a different AppReg
You pasted the wrong service principal object ID into tfvars. The postcondition in data-azuread-customer.tf detects this. Get the correct one:
Scan tasks stuck in error with 403 LinkedAuthorizationFailed (Mode B)
The BYO app registration service principal is missing Managed Identity Operator on the UAMI resource group. This script grants it automatically in Mode B. If you created UAMIs manually (without --add-uamis), grant it yourself:
Re-running with the same --app-name fails at az ad app create
Display names must be unique per tenant. The script is not idempotent, use --rollback first, then re-run.
Notes
The following notes describe security-relevant properties of what the script produces.
No client secret is created. Auth from both Cortex (GCP service account) and Azure UAMIs into the app registration uses Federated Identity Credentials.
The app registration is multi-tenant (
AzureADMultipleOrgs). Required because it consents into the monitored tenant, which may be different from the home tenant.The Terraform runner becomes an Owner of only this one app registration, least privilege; it can manage credentials on this app registration but nothing else in the directory. With
--add-uamis, the same principle applies to the UAMI resource group: Managed Identity Contributor is scoped to the single resource group.The script does not grant admin consent on the monitored tenant, that happens via the Cortex onboarding URL after
terraform apply.The script does not request or grant any Microsoft Graph permissions on the app registration's home tenant. The app registration never authenticates to itself.
Mode B resource-group handling: If the resource group already exists, it's reused as-is (the script only ensures it exists at the requested location). Rollback never deletes the resource group, only the UAMIs and role assignments it created.
A sample shell script
You can use this sample setup-byo-app-registration.sh script as a basis to set up the app registration for your Azure BYOA outpost.
Last updated
Was this helpful?
