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:
| Mode | Flag | What it creates | What it delegates to the Terraform runner |
|---|---|---|---|
| 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.
| Principal | Permissions needed | Granted by | 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.
| Principal | How to look it up |
|---|---|
| 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 |
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.
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):
| Layer | Role | Why |
|---|---|---|
| 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.
| Grant | Recipient | Scope | Why it's needed | Symptom if missing |
|---|---|---|---|---|
| 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:
# 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.
./setup-byo-app-registration.sh \ --app-name <APP_DISPLAY_NAME> \ --tf-runner-object-id <GUID> \ [--copy-to-clipboard]
Example:
./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.
./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:
./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.
| Flag | Description |
|---|---|
--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:
app_registration_mode = "customer_managed" customer_app_client_id = "<APP_ID>" customer_sp_object_id = "<SP_OBJECT_ID>"
Mode B:
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:
# 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
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.
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_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:
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:
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):
./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:
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:
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.
Important! Because if you modify this script to suit your organization's needs, Palo Alto Networks won't be able to offer additional support.
#!/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