> For the complete documentation index, see [llms.txt](https://cortex-docs.paloaltonetworks.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://cortex-docs.paloaltonetworks.com/cortex-xsiam/configure-cortex-xsiam/cortex-xsiam-data-sources/cloud-service-provider-csp-onboarding/outpost-onboarding/working-with-bringing-your-own-azure-app-byoa-outposts/the-shell-script-for-azure-app-registration.md).

# The shell script for Azure app registration

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:

<table><thead><tr><th valign="top">Mode</th><th valign="top">Flag</th><th valign="top">What it creates</th><th valign="top">What it delegates to the Terraform runner</th></tr></thead><tbody><tr><td valign="top">A, app registration only</td><td valign="top">(default)</td><td valign="top">App registration and service principal</td><td valign="top">Ownership of the app registration</td></tr><tr><td valign="top">B, app registration and scanner identities</td><td valign="top"><code>--add-uamis</code></td><td valign="top">All of Mode A, and the scanner User-Assigned Managed Identities (UAMIs) (<code>agentless</code>, <code>dspm</code>, <code>registry</code>, <code>serverless</code>, <code>proxy</code>) in a customer-owned resource group</td><td valign="top">App registration ownership and Managed Identity Contributor on the UAMI resource group</td></tr></tbody></table>

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.

<table><thead><tr><th valign="top">Principal</th><th valign="top">Permissions needed</th><th valign="top">Granted by</th><th valign="top">Scope</th></tr></thead><tbody><tr><td valign="top">Terraform runner (human or CI service principal, the one running <code>terraform apply</code>)</td><td valign="top">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).</td><td valign="top">This script (<code>--tf-runner-object-id</code>)</td><td valign="top">The one app registration and, in Mode B, the one UAMI resource group</td></tr><tr><td valign="top">BYO app registration / service principal (non-human, created by this script)</td><td valign="top">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).</td><td valign="top">This script grants Managed Identity Operator (Mode B only). Sub-level scan roles are granted via the onboarding URL after <code>terraform apply</code>.</td><td valign="top">UAMI resource group (this script) and monitored sub (onboarding URL)</td></tr><tr><td valign="top">Scanner UAMIs (non-human, created by this script in Mode B)</td><td valign="top">Sub: Workload roles on the monitored sub (granted by the outpost Terraform later, not by this script). FIC trust is set up by <code>terraform apply</code>.</td><td valign="top"><code>terraform apply</code> (FICs) and outpost Terraform modules (workload roles)</td><td valign="top">Monitored sub and resource groups</td></tr></tbody></table>

#### Who runs this script vs. who runs Terraform

The following activities happen, possibly by different identities (they may be the same identity):

<table data-header-hidden><thead><tr><th valign="top"></th><th valign="top"></th></tr></thead><tbody><tr><td valign="top">Running this script</td><td valign="top">Terraform runner (<code>terraform apply</code>)</td></tr><tr><td valign="top"><strong>Mode A</strong> needs: Entra: Application Developer (or equivalent, see below)</td><td valign="top"><strong>Mode A</strong> gets: Owner on the app registration (granted by this script)</td></tr><tr><td valign="top"><strong>Mode B</strong> also needs: Azure RBAC on the UAMI resource group: Contributor and User Access Administrator (or Owner)</td><td valign="top"><strong>Mode B</strong> also gets: Managed Identity Contributor on the UAMI resource group (granted by this script)</td></tr></tbody></table>

#### 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 apply` time, and reuse the same RBAC for every subsequent `apply` or `destroy`, 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](/cortex-xsiam/configure-cortex-xsiam/cortex-xsiam-data-sources/cloud-service-provider-csp-onboarding/outpost-onboarding/working-with-bringing-your-own-azure-app-byoa-outposts/the-shell-script-for-azure-app-registration.md#troubleshooting).

#### How to identify each principal

Use the following commands to look up the object ID for each principal type.

<table><thead><tr><th valign="top">Principal</th><th valign="top">How to look it up</th></tr></thead><tbody><tr><td valign="top">Terraform runner (user)</td><td valign="top"><code>az ad signed-in-user show --query id -o tsv</code> (run as that user)</td></tr><tr><td valign="top">Terraform runner (CI service principal)</td><td valign="top"><code>az ad sp show --id &#x3C;sp-client-id> --query id -o tsv</code></td></tr><tr><td valign="top">BYO app registration service principal (after running this script)</td><td valign="top"><code>az ad sp show --id &#x3C;customer_app_client_id> --query id -o tsv</code> (returns <code>customer_sp_object_id</code>)</td></tr><tr><td valign="top">Scanner UAMIs (after running this script in Mode B)</td><td valign="top"><code>az identity show --name &#x3C;prefix>-&#x3C;role> --resource-group &#x3C;RG> --query id -o tsv</code></td></tr></tbody></table>

{% hint style="info" %}
**Note**: `object_id` is not `client_id`. Both are GUIDs, both come back from `az`, and they are not interchangeable. The script validates the Terraform-runner GUID resolves to a real User or service principal in the current tenant and prints `tf-runner resolved as: user|service principal` on success. If you see `does not resolve to any user or service principal in this Azure AD tenant`, you almost certainly pasted a `client_id` or logged into the wrong tenant.
{% endhint %}

### 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):**

<table><thead><tr><th valign="top">Layer</th><th valign="top">Role</th><th valign="top">Why</th></tr></thead><tbody><tr><td valign="top">Entra</td><td valign="top">Application Developer (<code>cf1c38e5-3621-4004-a7cb-879624dced7c</code>)</td><td valign="top">Create app registrations and own or manage the ones you created. Covers <code>az ad app create</code>, <code>az ad sp create</code>, and <code>az ad app owner add</code>.</td></tr><tr><td valign="top">Azure RBAC</td><td valign="top">None</td><td valign="top">This mode never calls ARM.</td></tr></tbody></table>

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**:
  1. Pre-create the UAMI resource group out-of-band.
  2. Grant the script-runner Managed Identity Contributor on that resource group.
  3. 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.

<table><thead><tr><th valign="top">Grant</th><th width="149" valign="top">Recipient</th><th valign="top">Scope</th><th valign="top">Why it's needed</th><th valign="top">Symptom if missing</th></tr></thead><tbody><tr><td valign="top">App registration ownership</td><td valign="top">Terraform runner</td><td valign="top">The one app registration</td><td valign="top">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.</td><td valign="top"><code>terraform apply</code> fails with <code>Insufficient privileges</code> when creating the <code>azuread_application_federated_identity_credential</code> resources.</td></tr><tr><td valign="top">Managed Identity Contributor (Mode B)</td><td valign="top">Terraform runner</td><td valign="top">The UAMI resource group</td><td valign="top">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.</td><td valign="top"><code>terraform apply</code> fails with <code>AuthorizationFailed</code> on the UAMI FIC or write operations.</td></tr><tr><td valign="top">Managed Identity Operator (Mode B)</td><td valign="top">BYO app registration's service principal</td><td valign="top">The UAMI resource group</td><td valign="top">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 <code>Microsoft.ManagedIdentity/userAssignedIdentities/assign/action</code>, 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.</td><td valign="top">Scans get stuck in <code>error</code> state with Azure <code>403 LinkedAuthorizationFailed</code> on <code>.../userAssignedIdentities/assign/action</code>.</td></tr></tbody></table>

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/action` the 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:

```sh
# User identity
az ad signed-in-user show --query id -o tsv

# Service principal (CI/CD)
az ad sp show --id <sp-client-id> --query id -o tsv
```

#### Mode A, app registration only

This mode creates the app registration and its service principal, and adds the Terraform runner as Owner.

```sh
./setup-byo-app-registration.sh \
  --app-name <APP_DISPLAY_NAME> \
  --tf-runner-object-id <GUID> \
  [--copy-to-clipboard]
```

Example:

```sh
./setup-byo-app-registration.sh \
  --app-name cortex-scan-platform-my-subscription \
  --tf-runner-object-id 12345678-1234-1234-1234-123456789abc
```

#### 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.

```sh
./setup-byo-app-registration.sh \
  --app-name <APP_DISPLAY_NAME> \
  --tf-runner-object-id <GUID> \
  --add-uamis \
  --uami-subscription <SUB-ID> \
  --uami-resource-group <RG-NAME> \
  --uami-location <REGION> \
  [--uami-name-prefix <PREFIX>] \
  [--copy-to-clipboard]
```

Example:

```sh
./setup-byo-app-registration.sh \
  --app-name cortex-scan-platform-my-subscription \
  --tf-runner-object-id 12345678-1234-1234-1234-123456789abc \
  --add-uamis \
  --uami-subscription 3ee44654-9e52-41a0-82ca-f5d5956452d6 \
  --uami-resource-group cortex-outpost-rg \
  --uami-location australiaeast
```

#### Flags

The following flags are supported.

<table><thead><tr><th valign="top">Flag</th><th valign="top">Description</th></tr></thead><tbody><tr><td valign="top"><code>--app-name &#x3C;NAME></code></td><td valign="top">Required. Display name of the new app registration. Must be unique in the tenant.</td></tr><tr><td valign="top"><code>--tf-runner-object-id &#x3C;GUID></code></td><td valign="top">Required. Object ID of the user or service principal that will run <code>terraform apply</code>. Added as Owner of the app registration. With <code>--add-uamis</code>, also granted Managed Identity Contributor on the UAMI resource group.</td></tr><tr><td valign="top"><code>--add-uamis</code></td><td valign="top">Enable Mode B. Requires <code>--uami-subscription</code>, <code>--uami-resource-group</code>, and <code>--uami-location</code>.</td></tr><tr><td valign="top"><code>--uami-subscription &#x3C;GUID></code></td><td valign="top">Mode B. Azure subscription ID where the UAMIs will be created.</td></tr><tr><td valign="top"><code>--uami-resource-group &#x3C;NAME></code></td><td valign="top">Mode B. Resource group that will hold the UAMIs. Created if it doesn't exist.</td></tr><tr><td valign="top"><code>--uami-location &#x3C;REGION></code></td><td valign="top">Mode B. Azure region for the resource group and UAMIs, for example <code>australiaeast</code> or <code>eastus</code>.</td></tr><tr><td valign="top"><code>--uami-name-prefix &#x3C;PREFIX></code></td><td valign="top">Mode B, optional. Prefix for UAMI names. Default: <code>cortex</code>. Each UAMI is named <code>&#x3C;prefix>-&#x3C;role></code>, for example <code>cortex-agentless</code>.</td></tr><tr><td valign="top"><code>--copy-to-clipboard</code></td><td valign="top">Also copy the <code>tfvars</code> output to the system clipboard. Auto-detects <code>pbcopy</code>, <code>wl-copy</code>, <code>xclip</code>, <code>xsel</code>, or <code>clip.exe</code>.</td></tr><tr><td valign="top"><code>--rollback --app-client-id &#x3C;APP_ID> [--uami-* ...]</code></td><td valign="top">Manually delete a previously created app registration and service principal, and Mode B UAMIs. See Rollback.</td></tr><tr><td valign="top"><code>-h</code>, <code>--help</code></td><td valign="top">Show usage.</td></tr></tbody></table>

#### 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:

```hcl
app_registration_mode  = "customer_managed"
customer_app_client_id = "<APP_ID>"
customer_sp_object_id  = "<SP_OBJECT_ID>"
```

Mode B:

```hcl
app_registration_mode       = "customer_managed"
customer_app_client_id      = "<APP_ID>"
customer_sp_object_id       = "<SP_OBJECT_ID>"
uami_mode                   = "customer_managed"
customer_uami_agentless_id  = "<UAMI_ARM_ID>"
customer_uami_dspm_id       = "<UAMI_ARM_ID>"
customer_uami_registry_id   = "<UAMI_ARM_ID>"
customer_uami_serverless_id = "<UAMI_ARM_ID>"
customer_uami_proxy_id      = "<UAMI_ARM_ID>"
```

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:

```sh
# Mode A
./setup-byo-app-registration.sh --rollback --app-client-id <APP_ID>

# Mode B (also deletes the scanner UAMIs; the resource group itself is
# left intact since it may pre-exist and be shared with other workloads)
./setup-byo-app-registration.sh --rollback \
  --app-client-id <APP_ID> \
  --uami-subscription <SUB> \
  --uami-resource-group <RG> \
  --uami-name-prefix <PREFIX>   # only if you used a non-default prefix
```

{% hint style="info" %}
**Note**: If you've already run `terraform apply`, run `terraform destroy` first, otherwise the app registration or UAMIs disappear while Terraform still tracks FICs on them, causing drift on the next plan.

However, keep in mind that `terraform destroy` can fail if ephemeral scan resources (VMs, disks, snapshots, NICs) remain in the outpost resource group. If this occurs, manually delete the lingering resources (or the entire resource group) in the Azure portal, rerun `terraform destroy`, and follow the cleanup steps in the outpost module README. Additionally, to prevent accidental deletion, always keep your customer-created User-Assigned Managed Identities (UAMIs) in a separate, customer-owned resource group; any UAMIs placed in the outpost resource group will be wiped during destruction.
{% endhint %}

### 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, or
* You pasted the wrong kind of GUID (for example, `client_id` instead of `object_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:

```sh
az ad app owner list --id <customer_app_client_id> --query "[].id" -o tsv
az ad app owner add  --id <customer_app_client_id> --owner-object-id <tf-runner-object-id>
```

**`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:

```sh
RG_SCOPE="/subscriptions/<SUB>/resourceGroups/<RG>"
az role assignment list --assignee <tf-runner-object-id> --scope "$RG_SCOPE" -o table
az role assignment create \
  --assignee-object-id <tf-runner-object-id> \
  --assignee-principal-type {User|ServicePrincipal} \
  --role e40ec5ca-96e0-45a2-b4ff-59039f2c2b59 \
  --scope "$RG_SCOPE"
```

**`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):

```sh
./setup-byo-app-registration.sh --rollback --app-client-id <APP_ID>
./setup-byo-app-registration.sh --app-name <NAME> --tf-runner-object-id <GUID>
```

**`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:

```sh
az ad sp show --id <customer_app_client_id> --query id -o tsv
```

**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:

```sh
RG_SCOPE="/subscriptions/<SUB>/resourceGroups/<RG>"
az role assignment create \
  --assignee-object-id <customer_sp_object_id> \
  --assignee-principal-type ServicePrincipal \
  --role f1a07417-d97a-45cb-824c-7a7467783830 \
  --scope "$RG_SCOPE"
```

**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.

{% hint style="info" %}
**Important**! Because if you modify this script to suit your organization's needs, Palo Alto Networks won't be able to offer additional support.
{% endhint %}

```sh
#!/bin/sh
# ------------------------------------------------------------------------------
# BYO App Registration Setup Script (+ optional UAMI provisioning)
#
# Creates an Azure AD App Registration and Service Principal for the
# customer-managed (BYO) outpost deployment flow, and adds the Terraform
# runner identity as an owner of the AppReg so it can attach Federated
# Identity Credentials via Terraform.
#
# Optional --add-uamis mode ALSO creates the 5 scanner UAMIs and grants the
# TF runner 'Managed Identity Contributor' on the UAMI resource group, which
# is the permission TF needs at `terraform apply` time to attach UAMI self-FICs.
# This consolidates what used to be two scripts (setup-byo-app-registration.sh
# + setup-byo-uamis.sh) into a single AD-admin entry-point, and removes 9
# manual `az federated-credential create` steps from the customer workflow.
#
# Note: this script does NOT request or grant any Microsoft Graph permissions
# on the AppReg's home tenant. The AppReg never authenticates to itself -
# Federated Identity Credentials are added by the Terraform runner acting as
# an *owner* of the AppReg. Admin consent against the *monitored* tenant
# (where the AppReg actually scans resources) is granted separately via the
# Cortex onboarding URL after `terraform apply`.
#
# Portability: written to POSIX sh - runs on macOS, Linux (bash/dash/ash),
# and Windows via WSL or Git Bash.
#
# ──────────────────────────────────────────────────────────────────────────────
# SEPARATION OF DUTIES
# ──────────────────────────────────────────────────────────────────────────────
# This script is designed for an AD-admin persona to run ONCE; afterwards a
# separate TF-runner persona (often a CI service principal) does the
# subscription-scoped `terraform apply`. The two personas may or may not be
# the same human / SP.
#
#   AD admin (this script)                      TF runner (`terraform apply`)
#   ----------------------------------------    --------------------------------
#   - Entra: Application Developer              - Owner on the AppReg (granted
#   - (with --add-uamis only):                    by this script)
#     Azure RBAC Contributor on the sub OR      - (with --add-uamis only)
#     'Managed Identity Contributor' on a         Managed Identity Contributor
#     pre-created RG + 'User Access               on the UAMI RG (granted by
#     Administrator' to grant the role            this script)
#     assignment to the TF runner
#
# The --tf-runner-object-id flag is the linchpin: it's what the AD admin uses
# to *delegate* the AppReg-ownership + UAMI-RG-write permissions to the TF
# runner so the TF runner can attach all 9 FICs at apply time without any
# Entra-level powers of its own.
#
# ──────────────────────────────────────────────────────────────────────────────
# REQUIRED PERMISSIONS
# ──────────────────────────────────────────────────────────────────────────────
# Mode A — AppReg only (default, NO --add-uamis):
#   - Entra role:    'Application Developer' (built-in,
#                     cf1c38e5-3621-4004-a7cb-879624dced7c) on the tenant.
#                     Grants exactly: create AppRegs + own / manage the ones
#                     you created. Sufficient for all three AppReg-side
#                     operations (az ad app create, az ad sp create,
#                     az ad app owner add).
#   - Azure RBAC:    None. This mode never calls ARM.
#   - Admin consent: Not required at this stage. This script intentionally
#                     does NOT grant any Microsoft Graph application
#                     permissions on the AppReg's home tenant. Admin consent
#                     against the *monitored* tenant is granted later via the
#                     Cortex onboarding URL, not here.
#
# Mode B — AppReg + UAMIs (--add-uamis):
#   All of Mode A, plus Azure RBAC for the UAMI side. Choose ONE of:
#
#     Easy option:
#       'Contributor' at subscription scope. Covers RG creation + UAMI CRUD,
#       but NOT role assignment - that always needs 'User Access Administrator'
#       (or 'Owner'). So pair Contributor with 'User Access Administrator'
#       at the same scope, OR use 'Owner' (which includes both).
#
#     Least-privilege option:
#       1. Pre-create the UAMI resource group out-of-band.
#       2. Grant the AD admin 'Managed Identity Contributor' on that RG.
#       3. Grant the AD admin 'User Access Administrator' on that RG (so the
#          script can grant the TF runner 'Managed Identity Contributor' on
#          the same RG).
#
# ──────────────────────────────────────────────────────────────────────────────
# PREREQUISITES
# ──────────────────────────────────────────────────────────────────────────────
#   - Azure CLI (az) installed and authenticated (az login) against the *Entra
#     tenant* where the AppReg will live. (For --add-uamis, that tenant must
#     also be the one homing the target subscription.)
#   - The logged-in identity must be allowed to create App Registrations in
#     that tenant. By default this is any user; if the tenant has set
#     "Users can register applications = No", an admin must grant the built-in
#     Entra role 'Application Developer' (see "Required permissions" above).
#   - You know the object_id of the identity (user or service principal) that
#     will execute `terraform apply`. Look it up with:
#       User:              az ad signed-in-user show --query id -o tsv
#       Service Principal: az ad sp show --id <sp-client-id> --query id -o tsv
#
# Usage (Mode A — AppReg only):
#   ./setup-byo-app-registration.sh \
#     --app-name <app-display-name> \
#     --tf-runner-object-id <object-id>
#
# Usage (Mode B — AppReg + UAMIs):
#   ./setup-byo-app-registration.sh \
#     --app-name <app-display-name> \
#     --tf-runner-object-id <object-id> \
#     --add-uamis \
#     --uami-subscription <SUB-ID> \
#     --uami-resource-group <RG-NAME> \
#     --uami-location <REGION> \
#     [--uami-name-prefix <PREFIX>]
#
# Examples:
#   # Mode A
#   ./setup-byo-app-registration.sh \
#     --app-name cortex-scan-platform-my-subscription \
#     --tf-runner-object-id 12345678-1234-1234-1234-123456789abc
#
#   # Mode B (with UAMIs)
#   ./setup-byo-app-registration.sh \
#     --app-name cortex-scan-platform-my-subscription \
#     --tf-runner-object-id 12345678-1234-1234-1234-123456789abc \
#     --add-uamis \
#     --uami-subscription 3ee44654-9e52-41a0-82ca-f5d5956452d6 \
#     --uami-resource-group cortex-outpost-rg \
#     --uami-location australiaeast
#
# Windows users: run via Git Bash (recommended) or WSL2:
#   - Git Bash: open "Git Bash" terminal, cd to the script directory, run as above
#   - WSL2:     open WSL terminal, install Azure CLI for Linux, then run as above
#
# After running, copy the output values into template_params.tfvars and run
# the main Terraform apply.
# ------------------------------------------------------------------------------

set -eu

# Built-in Azure role definition IDs (using IDs rather than display names avoids
# issues with localised role names on tenants in non-EN languages).
#
# Managed Identity Contributor — granted to the TF runner so it can attach the
# 5 UAMI self-FICs at `terraform apply` time.
MIC_ROLE_ID="e40ec5ca-96e0-45a2-b4ff-59039f2c2b59"
# Managed Identity Operator — granted to the BYO AppReg's Service Principal so
# the Cortex scanner-dispatcher can attach BYO UAMIs to scanner workload VMs at
# scan time. Without this, scan_task gets stuck in 'error' state with Azure
# 403 LinkedAuthorizationFailed on
# 'Microsoft.ManagedIdentity/userAssignedIdentities/assign/action'.
MIO_ROLE_ID="f1a07417-d97a-45cb-824c-7a7467783830"

ROLES="agentless dspm registry serverless proxy"

# -- Rollback bookkeeping -----------------------------------------------------
# Track which resources we've created so we can undo them if any later step
# fails (set -e + EXIT trap). Variables are populated as creation succeeds.
CREATED_APP_ID=""
CREATED_SP_OBJ_ID=""
# Newline-separated list of created UAMI ARM IDs, newest first (so iteration
# order = reverse-creation order for the rollback trap).
CREATED_UAMIS=""
# Role assignments we granted (so we can undo them on rollback). Newline-
# separated triples "scope|principal|role|principal-type", newest first so
# rollback iteration order = reverse-grant order. Empty when --add-uamis is
# not used or before any grants succeed.
GRANTED_RAS=""
ROLLBACK_DISABLED=0

rollback() {
  # Skip on success path (set ROLLBACK_DISABLED=1 before exit) and skip when
  # nothing was actually created yet (failed arg parsing, --help, etc.).
  [ "$ROLLBACK_DISABLED" -eq 1 ] && return 0
  if [ -z "$CREATED_APP_ID" ] && [ -z "$CREATED_UAMIS" ] && [ -z "$GRANTED_RAS" ]; then
    return 0
  fi
  echo >&2 ""
  echo >&2 "---------------------------------------------------------"
  echo >&2 "WARNING: Script failed - rolling back partial state ..."

  # Reverse-creation order:
  # 1. Role assignments (granted last in --add-uamis flow). May be multiple
  #    (Managed Identity Contributor on TF runner + Managed Identity Operator
  #    on BYO AppReg SP). Iterate newest-first.
  if [ -n "$GRANTED_RAS" ]; then
    printf '%s\n' "$GRANTED_RAS" | while IFS='|' read -r ra_scope ra_principal ra_role ra_ptype; do
      [ -z "$ra_scope" ] && continue
      echo >&2 "  Removing role assignment ($ra_role for $ra_principal on $ra_scope) ..."
      if az role assignment delete \
           --assignee-object-id "$ra_principal" \
           --assignee-principal-type "$ra_ptype" \
           --role "$ra_role" \
           --scope "$ra_scope" 2>/dev/null; then
        echo >&2 "  - removed"
      else
        # Retry without the principal-type hint (slower but more permissive).
        if az role assignment delete \
             --assignee "$ra_principal" \
             --role "$ra_role" \
             --scope "$ra_scope" 2>/dev/null; then
          echo >&2 "  - removed"
        else
          echo >&2 "  WARNING: could not remove role assignment - clean up manually:"
          echo >&2 "       az role assignment delete --assignee $ra_principal --role '$ra_role' --scope $ra_scope"
        fi
      fi
    done
  fi

  # 2. UAMIs (created mid-flow in --add-uamis). Iterate newest-first.
  if [ -n "$CREATED_UAMIS" ]; then
    printf '%s\n' "$CREATED_UAMIS" | while IFS= read -r uami_id; do
      [ -z "$uami_id" ] && continue
      uami_name=${uami_id##*/}
      echo >&2 "  Deleting UAMI ${uami_name} ..."
      if az identity delete --ids "$uami_id" 2>/dev/null; then
        echo >&2 "  - deleted"
      else
        echo >&2 "  WARNING: could not delete $uami_id - clean up manually:"
        echo >&2 "       az identity delete --ids $uami_id"
      fi
    done
  fi

  # 3. App Registration (created first). Deletion cascades to SP + owner edits.
  if [ -n "$CREATED_APP_ID" ]; then
    echo >&2 "  Deleting App Registration $CREATED_APP_ID ..."
    if az ad app delete --id "$CREATED_APP_ID" 2>/dev/null; then
      echo >&2 "  - deleted"
    else
      echo >&2 "  WARNING: could not delete App Registration $CREATED_APP_ID - clean up manually:"
      echo >&2 "       az ad app delete --id $CREATED_APP_ID"
      [ -n "$CREATED_SP_OBJ_ID" ] && \
        echo >&2 "       az ad sp delete --id $CREATED_SP_OBJ_ID"
    fi
  fi
  echo >&2 "---------------------------------------------------------"
}
trap rollback EXIT

# -- Helpers ------------------------------------------------------------------

# Validate a string is a GUID (8-4-4-4-12 hex pattern). POSIX has no =~ regex;
# use a case-glob with explicit hex character classes.
guid_is_valid() {
  # shellcheck disable=SC2254  # globbing is intentional for POSIX pattern match
  case "$1" in
    [0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]-[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]-[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]-[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]-[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])
      return 0 ;;
    *)
      return 1 ;;
  esac
}

# Per-role UAMI ARM IDs, populated as each `az identity create` succeeds.
ID_AGENTLESS=""
ID_DSPM=""
ID_REGISTRY=""
ID_SERVERLESS=""
ID_PROXY=""

get_id_for_role() {
  case "$1" in
    agentless)  printf '%s\n' "$ID_AGENTLESS" ;;
    dspm)       printf '%s\n' "$ID_DSPM" ;;
    registry)   printf '%s\n' "$ID_REGISTRY" ;;
    serverless) printf '%s\n' "$ID_SERVERLESS" ;;
    proxy)      printf '%s\n' "$ID_PROXY" ;;
    *) echo >&2 "INTERNAL: unknown role '$1'"; return 1 ;;
  esac
}

set_id_for_role() {
  case "$1" in
    agentless)  ID_AGENTLESS="$2" ;;
    dspm)       ID_DSPM="$2" ;;
    registry)   ID_REGISTRY="$2" ;;
    serverless) ID_SERVERLESS="$2" ;;
    proxy)      ID_PROXY="$2" ;;
    *) echo >&2 "INTERNAL: unknown role '$1'"; return 1 ;;
  esac
}

usage() {
  cat >&2 <<'USAGE'
Usage: setup-byo-app-registration.sh [OPTIONS]

Required options:
  --app-name <NAME>                 Display name of the new App Registration.
  --tf-runner-object-id <GUID>      Azure AD object_id (GUID) of the user OR service principal
                                    that will run 'terraform apply'. Added as owner of the AppReg
                                    so it can attach Federated Identity Credentials. With
                                    --add-uamis, ALSO granted 'Managed Identity Contributor' on
                                    the UAMI resource group so TF can attach UAMI self-FICs.

Other options:
  --copy-to-clipboard               Also copy the tfvars output to the system clipboard.
                                    Off by default - values are printed to stdout regardless.
  -h, --help                        Show this help and exit.

UAMI options (all four required together when --add-uamis is set):
  --add-uamis                       In addition to the AppReg, create the 5 scanner UAMIs
                                    (agentless, dspm, registry, serverless, proxy) and grant
                                    the TF runner 'Managed Identity Contributor' on the UAMI
                                    resource group. Sets uami_mode=customer_managed in output.
  --uami-subscription <GUID>        Azure subscription ID where the UAMIs will be created.
  --uami-resource-group <NAME>      Resource group that will hold the UAMIs.
                                    Will be created if it does not already exist.
  --uami-location <REGION>          Azure region for the resource group / UAMIs.
                                    Example: australiaeast, eastus.
  --uami-name-prefix <PREFIX>       Prefix for UAMI names. Default: cortex
                                    Each UAMI is named <prefix>-<role>, e.g. cortex-agentless.

Look up the tf-runner object_id with:
  User:              az ad signed-in-user show --query id -o tsv
  Service Principal: az ad sp show --id <sp-client-id> --query id -o tsv

Examples:
  # AppReg only
  setup-byo-app-registration.sh \
    --app-name cortex-scan-platform-my-subscription \
    --tf-runner-object-id 12345678-1234-1234-1234-123456789abc

  # AppReg + UAMIs (single AD-admin entrypoint, zero manual FIC steps)
  setup-byo-app-registration.sh \
    --app-name cortex-scan-platform-my-subscription \
    --tf-runner-object-id 12345678-1234-1234-1234-123456789abc \
    --add-uamis \
    --uami-subscription 3ee44654-9e52-41a0-82ca-f5d5956452d6 \
    --uami-resource-group cortex-outpost-rg \
    --uami-location australiaeast

Rollback (deletes AppReg, and if --add-uamis was used also delete the UAMIs):
  setup-byo-app-registration.sh --rollback --app-client-id <APP_ID> \
    [--uami-subscription <SUB> --uami-resource-group <RG> --uami-name-prefix <PREFIX>]
USAGE
}

# -- Pre-flight: dependency checks --------------------------------------------
# Fail fast with actionable errors before doing anything else.

if ! command -v az >/dev/null 2>&1; then
  echo >&2 "ERROR: Azure CLI 'az' not found in PATH."
  echo >&2 "   Install instructions: https://learn.microsoft.com/cli/azure/install-azure-cli"
  exit 1
fi

# Verify az login was performed against *some* tenant - actual tenant correctness
# is checked later via the tf-runner identity lookup.
if ! az account show >/dev/null 2>&1; then
  echo >&2 "ERROR: Not logged in to Azure CLI. Run 'az login' against the tenant where the AppReg will live, then retry."
  exit 1
fi

APP_NAME=""
TF_RUNNER_OBJECT_ID=""
COPY_TO_CLIPBOARD=0

# UAMI-mode inputs
ADD_UAMIS=0
UAMI_SUBSCRIPTION=""
UAMI_RESOURCE_GROUP=""
UAMI_LOCATION=""
UAMI_NAME_PREFIX="cortex"

# -- Manual rollback mode -----------------------------------------------------
# Triggered via `--rollback` as the FIRST argument. Tears down a previous run's
# AppReg (deletion cascades to SP) and, if UAMI args are supplied, deletes the
# 5 UAMIs too. Idempotent - missing resources are treated as success.
if [ "${1:-}" = "--rollback" ]; then
  ROLLBACK_DISABLED=1  # disable EXIT trap; we handle errors manually below
  shift
  RB_APP_ID=""
  RB_SUB=""
  RB_RG=""
  RB_PREFIX="cortex"
  while [ "$#" -gt 0 ]; do
    case "$1" in
      --app-client-id)
        [ "$#" -ge 2 ] || { echo >&2 "ERROR: --app-client-id requires a value"; exit 1; }
        RB_APP_ID="$2"; shift 2 ;;
      --uami-subscription)
        [ "$#" -ge 2 ] || { echo >&2 "ERROR: --uami-subscription requires a value"; exit 1; }
        RB_SUB="$2"; shift 2 ;;
      --uami-resource-group)
        [ "$#" -ge 2 ] || { echo >&2 "ERROR: --uami-resource-group requires a value"; exit 1; }
        RB_RG="$2"; shift 2 ;;
      --uami-name-prefix)
        [ "$#" -ge 2 ] || { echo >&2 "ERROR: --uami-name-prefix requires a value"; exit 1; }
        RB_PREFIX="$2"; shift 2 ;;
      *)
        echo >&2 "ERROR: Unknown rollback argument: '$1'"
        echo >&2 "Usage: setup-byo-app-registration.sh --rollback --app-client-id <APP_ID> [--uami-subscription <SUB> --uami-resource-group <RG> --uami-name-prefix <PREFIX>]"
        exit 1 ;;
    esac
  done
  if [ -z "$RB_APP_ID" ]; then
    echo >&2 "ERROR: --rollback requires --app-client-id <APP_ID>"
    echo >&2 "Find it in template_params.tfvars (customer_app_client_id) or the script's previous output."
    exit 1
  fi
  rb_failed=0

  # Tear down UAMIs first if requested (reverse-creation order). RG is left in
  # place because it may have been pre-existing and shared with other workloads.
  if [ -n "$RB_SUB" ] || [ -n "$RB_RG" ]; then
    if [ -z "$RB_SUB" ] || [ -z "$RB_RG" ]; then
      echo >&2 "ERROR: --rollback UAMI cleanup requires BOTH --uami-subscription and --uami-resource-group."
      exit 1
    fi
    if ! guid_is_valid "$RB_SUB"; then
      echo >&2 "ERROR: --uami-subscription is not a valid GUID: '$RB_SUB'"
      exit 1
    fi
    echo >&2 "Selecting subscription $RB_SUB ..."
    az account set --subscription "$RB_SUB"
    for role in $ROLES; do
      uami_name="${RB_PREFIX}-${role}"
      echo >&2 "Deleting UAMI '$uami_name' ..."
      if az identity delete --name "$uami_name" --resource-group "$RB_RG" 2>/dev/null; then
        echo >&2 "  - deleted"
      else
        if az identity show --name "$uami_name" --resource-group "$RB_RG" >/dev/null 2>&1; then
          echo >&2 "  WARNING: failed to delete (UAMI exists but delete errored) - clean up manually:"
          echo >&2 "       az identity delete --name $uami_name --resource-group $RB_RG"
          rb_failed=1
        else
          echo >&2 "  - already absent"
        fi
      fi
    done
  fi

  echo >&2 "Deleting App Registration $RB_APP_ID (and its Service Principal) ..."
  if az ad app delete --id "$RB_APP_ID"; then
    echo >&2 "OK: App Registration and Service Principal deleted."
  else
    echo >&2 "ERROR: Rollback failed - App Registration may not exist or you lack permissions to delete it."
    rb_failed=1
  fi

  if [ "$rb_failed" -eq 1 ]; then
    echo >&2 "ERROR: Rollback completed with errors - see messages above."
    exit 1
  fi
  echo >&2 "OK: Rollback complete."
  exit 0
fi

# Parse named flags. Reject positional args and unknown flags to prevent confusion.
while [ "$#" -gt 0 ]; do
  case "$1" in
    --app-name)
      [ "$#" -ge 2 ] || { echo >&2 "ERROR: --app-name requires a value"; usage; exit 1; }
      APP_NAME="$2"; shift 2 ;;
    --tf-runner-object-id)
      [ "$#" -ge 2 ] || { echo >&2 "ERROR: --tf-runner-object-id requires a value"; usage; exit 1; }
      TF_RUNNER_OBJECT_ID="$2"; shift 2 ;;
    --copy-to-clipboard)
      COPY_TO_CLIPBOARD=1; shift ;;
    --add-uamis)
      ADD_UAMIS=1; shift ;;
    --uami-subscription)
      [ "$#" -ge 2 ] || { echo >&2 "ERROR: --uami-subscription requires a value"; usage; exit 1; }
      UAMI_SUBSCRIPTION="$2"; shift 2 ;;
    --uami-resource-group)
      [ "$#" -ge 2 ] || { echo >&2 "ERROR: --uami-resource-group requires a value"; usage; exit 1; }
      UAMI_RESOURCE_GROUP="$2"; shift 2 ;;
    --uami-location)
      [ "$#" -ge 2 ] || { echo >&2 "ERROR: --uami-location requires a value"; usage; exit 1; }
      UAMI_LOCATION="$2"; shift 2 ;;
    --uami-name-prefix)
      [ "$#" -ge 2 ] || { echo >&2 "ERROR: --uami-name-prefix requires a value"; usage; exit 1; }
      UAMI_NAME_PREFIX="$2"; shift 2 ;;
    -h|--help)
      usage; exit 0 ;;
    *)
      echo >&2 "ERROR: Unknown argument: '$1'"
      usage; exit 1 ;;
  esac
done

if [ -z "$APP_NAME" ] || [ -z "$TF_RUNNER_OBJECT_ID" ]; then
  echo >&2 "ERROR: Missing required option(s):"
  [ -z "$APP_NAME" ] && echo >&2 "   --app-name"
  [ -z "$TF_RUNNER_OBJECT_ID" ] && echo >&2 "   --tf-runner-object-id"
  echo >&2 ""
  usage
  exit 1
fi

if ! guid_is_valid "$TF_RUNNER_OBJECT_ID"; then
  echo >&2 "ERROR: --tf-runner-object-id is not a valid GUID: '$TF_RUNNER_OBJECT_ID'"
  echo >&2 "   Expected format: 12345678-1234-1234-1234-123456789abc"
  exit 1
fi

# -- UAMI-mode arg validation -------------------------------------------------
if [ "$ADD_UAMIS" -eq 1 ]; then
  if [ -z "$UAMI_SUBSCRIPTION" ] || [ -z "$UAMI_RESOURCE_GROUP" ] || [ -z "$UAMI_LOCATION" ]; then
    echo >&2 "ERROR: --add-uamis requires --uami-subscription, --uami-resource-group, and --uami-location."
    [ -z "$UAMI_SUBSCRIPTION" ]   && echo >&2 "   --uami-subscription is missing"
    [ -z "$UAMI_RESOURCE_GROUP" ] && echo >&2 "   --uami-resource-group is missing"
    [ -z "$UAMI_LOCATION" ]       && echo >&2 "   --uami-location is missing"
    echo >&2 ""
    usage
    exit 1
  fi
  if ! guid_is_valid "$UAMI_SUBSCRIPTION"; then
    echo >&2 "ERROR: --uami-subscription is not a valid GUID: '$UAMI_SUBSCRIPTION'"
    exit 1
  fi
else
  # Catch the silent-failure mode where someone passes UAMI args but forgets
  # the --add-uamis switch.
  if [ -n "$UAMI_SUBSCRIPTION$UAMI_RESOURCE_GROUP$UAMI_LOCATION" ]; then
    echo >&2 "ERROR: --uami-* options require --add-uamis to also be set."
    exit 1
  fi
fi

# -- Pre-flight: verify tf-runner identity exists in Azure AD -----------------
# Cheap fail-fast check - avoids creating an AppReg + SP and then discovering at the
# 'az ad app owner add' step that the supplied object_id doesn't resolve to anyone.
# A GUID-shaped object_id can be either a User or a Service Principal; either is valid.
echo >&2 "Verifying tf-runner identity exists in Azure AD ..."
TF_RUNNER_KIND=""
if az ad user show --id "$TF_RUNNER_OBJECT_ID" --query id -o tsv >/dev/null 2>&1; then
  TF_RUNNER_KIND="user"
elif az ad sp show --id "$TF_RUNNER_OBJECT_ID" --query id -o tsv >/dev/null 2>&1; then
  TF_RUNNER_KIND="service principal"
else
  echo >&2 "ERROR: --tf-runner-object-id '$TF_RUNNER_OBJECT_ID' does not resolve to any user or service principal in this Azure AD tenant."
  echo >&2 "   Verify the object_id with one of:"
  echo >&2 "     az ad user show --id $TF_RUNNER_OBJECT_ID --query id -o tsv"
  echo >&2 "     az ad sp   show --id $TF_RUNNER_OBJECT_ID --query id -o tsv"
  echo >&2 "   Make sure 'az login' was done against the correct tenant (the tenant where the AppReg will live)."
  exit 1
fi
echo >&2 "  - tf-runner resolved as: $TF_RUNNER_KIND"

# -- Step 1: Create App Registration ------------------------------------------
echo >&2 "Creating App Registration: $APP_NAME ..."
APP_ID=$(az ad app create \
  --display-name "$APP_NAME" \
  --sign-in-audience AzureADMultipleOrgs \
  --query appId -o tsv)
CREATED_APP_ID="$APP_ID"  # record so the EXIT trap can roll back if a later step fails

# -- Step 2: Create Service Principal -----------------------------------------
echo >&2 "Creating Service Principal ..."
SP_OBJ_ID=$(az ad sp create --id "$APP_ID" --query id -o tsv)
CREATED_SP_OBJ_ID="$SP_OBJ_ID"

# -- Step 3: Add the TF runner as owner of the AppReg -------------------------
# Required so the runner can later add FICs via Terraform. Fail loudly if this
# step fails - silent failure here causes a confusing 'Insufficient privileges'
# error during `terraform apply`, far away from the root cause.
# az ad app owner add accepts the AppReg's client_id (--id) - no need to look up object_id.
echo >&2 "Adding TF runner ($TF_RUNNER_OBJECT_ID) as App Registration owner ..."
az ad app owner add --id "$APP_ID" --owner-object-id "$TF_RUNNER_OBJECT_ID"

# -- Step 4 (--add-uamis only): create UAMIs + delegate RBAC to TF runner -----
if [ "$ADD_UAMIS" -eq 1 ]; then
  echo >&2 ""
  echo >&2 "Selecting subscription $UAMI_SUBSCRIPTION ..."
  az account set --subscription "$UAMI_SUBSCRIPTION"

  echo >&2 "Ensuring resource group '$UAMI_RESOURCE_GROUP' exists in '$UAMI_LOCATION' ..."
  az group create --name "$UAMI_RESOURCE_GROUP" --location "$UAMI_LOCATION" --output none

  for role in $ROLES; do
    uami_name="${UAMI_NAME_PREFIX}-${role}"
    echo >&2 "Creating UAMI '$uami_name' ..."
    uami_id=$(az identity create \
      --name "$uami_name" \
      --resource-group "$UAMI_RESOURCE_GROUP" \
      --location "$UAMI_LOCATION" \
      --query id -o tsv)
    set_id_for_role "$role" "$uami_id"
    # Prepend so newest is first (rollback iterates top-down).
    if [ -z "$CREATED_UAMIS" ]; then
      CREATED_UAMIS="$uami_id"
    else
      CREATED_UAMIS="$uami_id
$CREATED_UAMIS"
    fi
  done

  # -- Step 5: Grant TF runner 'Managed Identity Contributor' on UAMI RG ------
  # This is the linchpin that lets a separate-persona TF runner attach the 5
  # UAMI self-FICs at `terraform apply` time without needing any Entra power.
  # The role grant is idempotent (`az role assignment create` returns 0 even if
  # the assignment already exists) but we still record it for rollback.
  RG_SCOPE="/subscriptions/${UAMI_SUBSCRIPTION}/resourceGroups/${UAMI_RESOURCE_GROUP}"
  TF_RUNNER_PTYPE=$([ "$TF_RUNNER_KIND" = "user" ] && echo User || echo ServicePrincipal)
  echo >&2 ""
  echo >&2 "Granting TF runner ($TF_RUNNER_OBJECT_ID) 'Managed Identity Contributor' on $RG_SCOPE ..."
  if az role assignment create \
       --assignee-object-id "$TF_RUNNER_OBJECT_ID" \
       --assignee-principal-type "$TF_RUNNER_PTYPE" \
       --role "$MIC_ROLE_ID" \
       --scope "$RG_SCOPE" \
       --output none 2>/dev/null; then
    # Prepend so newest is first (rollback iterates top-down).
    GRANTED_RAS="${RG_SCOPE}|${TF_RUNNER_OBJECT_ID}|${MIC_ROLE_ID}|${TF_RUNNER_PTYPE}"
    echo >&2 "  - granted"
  else
    # Re-run without --output none to surface the real error to the caller.
    echo >&2 "ERROR: failed to grant 'Managed Identity Contributor' to TF runner. Re-running to show the error:"
    az role assignment create \
      --assignee-object-id "$TF_RUNNER_OBJECT_ID" \
      --assignee-principal-type "$TF_RUNNER_PTYPE" \
      --role "$MIC_ROLE_ID" \
      --scope "$RG_SCOPE" || true
    echo >&2 "   You need 'User Access Administrator' or 'Owner' on the RG/subscription to grant role assignments."
    exit 1
  fi

  # -- Step 6: Grant BYO AppReg SP 'Managed Identity Operator' on UAMI RG -----
  # Required at scan-time, NOT apply-time: the Cortex scanner-dispatcher (acting
  # as the BYO AppReg SP) calls Microsoft.Compute/virtualMachines/write with the
  # scanner UAMI attached, which Azure validates via
  # Microsoft.ManagedIdentity/userAssignedIdentities/assign/action on the UAMI's
  # parent scope. For cortex-managed UAMIs in the outpost-owned RG, the SP gets
  # this implicitly through custom role definitions; for BYO UAMIs in a
  # customer-owned RG, it must be granted explicitly here.
  echo >&2 "Granting BYO AppReg SP ($SP_OBJ_ID) 'Managed Identity Operator' on $RG_SCOPE ..."
  if az role assignment create \
       --assignee-object-id "$SP_OBJ_ID" \
       --assignee-principal-type ServicePrincipal \
       --role "$MIO_ROLE_ID" \
       --scope "$RG_SCOPE" \
       --output none 2>/dev/null; then
    # Prepend so newest is first (rollback iterates top-down).
    GRANTED_RAS="${RG_SCOPE}|${SP_OBJ_ID}|${MIO_ROLE_ID}|ServicePrincipal
${GRANTED_RAS}"
    echo >&2 "  - granted"
  else
    echo >&2 "ERROR: failed to grant 'Managed Identity Operator' to BYO AppReg SP. Re-running to show the error:"
    az role assignment create \
      --assignee-object-id "$SP_OBJ_ID" \
      --assignee-principal-type ServicePrincipal \
      --role "$MIO_ROLE_ID" \
      --scope "$RG_SCOPE" || true
    echo >&2 "   You need 'User Access Administrator' or 'Owner' on the RG/subscription to grant role assignments."
    exit 1
  fi
fi

# All steps succeeded - disable the EXIT-trap rollback.
ROLLBACK_DISABLED=1

# -- Output -------------------------------------------------------------------
# AppReg side: customer supplies 2 IDs to Terraform; the AppReg's object_id is
# auto-derived at plan time via data.azuread_application.customer_app.
# UAMI side (when --add-uamis): also emit the 5 ARM IDs + the uami_mode setter.
if [ "$ADD_UAMIS" -eq 1 ]; then
  TFVARS=$(cat <<EOF
customer_app_client_id      = "$APP_ID"
customer_sp_object_id       = "$SP_OBJ_ID"
customer_uami_agentless_id  = "$(get_id_for_role agentless)"
customer_uami_dspm_id       = "$(get_id_for_role dspm)"
customer_uami_registry_id   = "$(get_id_for_role registry)"
customer_uami_serverless_id = "$(get_id_for_role serverless)"
customer_uami_proxy_id      = "$(get_id_for_role proxy)"
EOF
)
else
  TFVARS=$(cat <<EOF
customer_app_client_id = "$APP_ID"
customer_sp_object_id  = "$SP_OBJ_ID"
EOF
)
fi

echo >&2 ""
if [ "$ADD_UAMIS" -eq 1 ]; then
  echo >&2 "OK: BYO App Registration + 5 UAMIs created and configured successfully."
else
  echo >&2 "OK: BYO App Registration created and configured successfully."
fi
echo >&2 ""
echo >&2 "Copy the following lines into the UI or your template_params.tfvars:"
echo >&2 "---------------------------------------------------------"
printf '%s\n' "$TFVARS"
echo >&2 "---------------------------------------------------------"

if [ "$COPY_TO_CLIPBOARD" -eq 1 ]; then
  copied_via=""
  if command -v pbcopy >/dev/null 2>&1; then
    printf '%s\n' "$TFVARS" | pbcopy && copied_via="pbcopy (macOS)"
  elif command -v wl-copy >/dev/null 2>&1; then
    printf '%s\n' "$TFVARS" | wl-copy && copied_via="wl-copy (Wayland)"
  elif command -v xclip >/dev/null 2>&1; then
    printf '%s\n' "$TFVARS" | xclip -selection clipboard && copied_via="xclip (X11)"
  elif command -v xsel >/dev/null 2>&1; then
    printf '%s\n' "$TFVARS" | xsel --clipboard --input && copied_via="xsel (X11)"
  elif command -v clip.exe >/dev/null 2>&1; then
    printf '%s\n' "$TFVARS" | clip.exe && copied_via="clip.exe (Windows/WSL/Git Bash)"
  fi
  if [ -n "$copied_via" ]; then
    echo >&2 " Copied to system clipboard via $copied_via."
  else
    echo >&2 "WARNING: --copy-to-clipboard requested but no clipboard tool found in PATH (pbcopy / wl-copy / xclip / xsel / clip.exe)."
  fi
fi

echo >&2 ""
echo >&2 "Next steps:"
echo >&2 "  1. Paste the lines above into the UI or your template_params.tfvars."
echo >&2 "  2. Run the main Terraform apply:"
echo >&2 "       terraform apply -var-file=template_params.tfvars"
if [ "$ADD_UAMIS" -eq 1 ]; then
  echo >&2 "     TF will create all 9 Federated Identity Credentials (4 AppReg + 5 UAMI self-FICs)."
  echo >&2 "     No manual 'az federated-credential create' commands are required."
fi
echo >&2 ""
echo >&2 "To undo:"
if [ "$ADD_UAMIS" -eq 1 ]; then
  echo >&2 "  ./setup-byo-app-registration.sh --rollback \\"
  echo >&2 "    --app-client-id $APP_ID \\"
  echo >&2 "    --uami-subscription $UAMI_SUBSCRIPTION \\"
  echo >&2 "    --uami-resource-group $UAMI_RESOURCE_GROUP \\"
  echo >&2 "    --uami-name-prefix $UAMI_NAME_PREFIX"
else
  echo >&2 "  ./setup-byo-app-registration.sh --rollback --app-client-id $APP_ID"
fi
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://cortex-docs.paloaltonetworks.com/cortex-xsiam/configure-cortex-xsiam/cortex-xsiam-data-sources/cloud-service-provider-csp-onboarding/outpost-onboarding/working-with-bringing-your-own-azure-app-byoa-outposts/the-shell-script-for-azure-app-registration.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
