Command Channel Provisioning — Admin Out-of-Band Actions
This runbook covers the one-time Azure and Entra ID actions that the deploy SPN
(sp-fabric-data-worker) cannot perform, because it deliberately lacks
Microsoft.Authorization/roleAssignments/write and Key Vault Crypto permissions.
The deploy SPN stays low-privilege; these grants are created once, out-of-band, by an
admin who is Owner + User Access Administrator on rg-fabric-dbt-platform.
This is the repo's sanctioned exception to the IaC-only rule — the same pattern already
used for the mi-fabric-functions Key Vault grant and the audit_db storage-key access.
Terraform does not manage the signing key or any of these role assignments.
Status — DEV: COMPLETED on 2026-06-05. The signing key and all five role assignments below already exist in DEV. This runbook is the procedure to reproduce them in UAT/PROD (or to re-create them in DEV if the resources are ever lost). The exported public key is committed at
scripts/fabric_ui/keys/devportal-command-signing.pub.pem.
Prerequisites
- A fresh interactive
az loginas an admin who is Owner + User Access Administrator onrg-fabric-dbt-platform(a headless/cached-token session failsMissingSubscriptionon scopedaz role assignmentwrites — use an interactive login in your own terminal, or the Azure Portal UI). - Terraform Phase 0 (
infra-deploy) completed — namespacesb-geris-devportaland itsagent-commands/agent-resultsqueues must exist. - Always pass
--subscription dfcc3a1c-fc89-4f3b-ab97-66464c60f9d8explicitly on every scoped write.
Step 1 — Create RSA-2048 Signing Key in Key Vault
The admin who holds only Key Vault Secrets Officer must first self-grant Key Vault Crypto Officer (Keys plane) before the key can be created:
ADMIN_OID="b7cc889d-ba72-4ff6-b557-bd14d56f38c9" # geris_fabric_admin@geris.nl
KV_ID="/subscriptions/dfcc3a1c-fc89-4f3b-ab97-66464c60f9d8/resourceGroups/rg-fabric-dbt-platform/providers/Microsoft.KeyVault/vaults/kv-fabric-dbt-keys"
az role assignment create \
--subscription dfcc3a1c-fc89-4f3b-ab97-66464c60f9d8 \
--assignee-object-id "$ADMIN_OID" --assignee-principal-type User \
--role "Key Vault Crypto Officer" --scope "$KV_ID"
# wait ~60s for RBAC propagation, then:
# Create RSA-2048 key; non-exportable private key stays in KV for RS256 signing.
az keyvault key create \
--vault-name kv-fabric-dbt-keys \
--name devportal-command-signing \
--kty RSA --size 2048 --ops sign verify
# Export the PUBLIC key in PEM format (bundled into the laptop agent binary).
az keyvault key download \
--vault-name kv-fabric-dbt-keys \
--name devportal-command-signing \
--encoding PEM \
--file devportal-command-signing.pub.pem
echo "Public key exported — commit this file at:"
echo " scripts/fabric_ui/keys/devportal-command-signing.pub.pem"
The matching private key never leaves Key Vault (non-exportable, sign+verify only). The agent embeds only the public PEM to verify RS256 signatures from the cloud.
Step 2 — Grant platform-admin SPN Key Vault Crypto User
Key Vault Crypto User lets the SWA backend call sign(RS256) via CryptographyClient
on the private key (and verify) — without ever exporting it.
PLATFORM_ADMIN_OID="37c4e058-c9ba-427c-867a-54aeb6ee01cc" # sp-fabric-platform-admin
KV_ID="/subscriptions/dfcc3a1c-fc89-4f3b-ab97-66464c60f9d8/resourceGroups/rg-fabric-dbt-platform/providers/Microsoft.KeyVault/vaults/kv-fabric-dbt-keys"
az role assignment create \
--subscription dfcc3a1c-fc89-4f3b-ab97-66464c60f9d8 \
--assignee-object-id "$PLATFORM_ADMIN_OID" --assignee-principal-type ServicePrincipal \
--role "Key Vault Crypto User" --scope "$KV_ID"
Step 3 — Service Bus RBAC (the authorization model)
The deploy SPN has no roleAssignments/write, so all five Service Bus grants are admin
actions. The developer-group Data Receiver on agent-commands is the structural
authorization gate — only members of the developer Entra group can receive (and therefore
execute) commands. This is why the backend no longer performs a Microsoft Graph
group-membership check: queue RBAC enforces it natively.
SB_NS_ID="/subscriptions/dfcc3a1c-fc89-4f3b-ab97-66464c60f9d8/resourceGroups/rg-fabric-dbt-platform/providers/Microsoft.ServiceBus/namespaces/sb-geris-devportal"
PLATFORM_ADMIN_OID="37c4e058-c9ba-427c-867a-54aeb6ee01cc" # sp-fabric-platform-admin
MI_FUNCTIONS_OID="e54bbe5b-5e0e-444d-8025-8a064bd9455c" # mi-fabric-functions
DEVELOPER_GROUP_OID="41489f76-7dbb-4fbc-a54b-ac07e673392b" # FP GER Fabric Developers (incl. B2B guests)
# platform-admin SPN: Data Sender on agent-commands (mints + enqueues commands)
az role assignment create \
--subscription dfcc3a1c-fc89-4f3b-ab97-66464c60f9d8 \
--assignee-object-id "$PLATFORM_ADMIN_OID" --assignee-principal-type ServicePrincipal \
--role "Azure Service Bus Data Sender" \
--scope "${SB_NS_ID}/queues/agent-commands"
# developer group: Data Receiver on agent-commands — THE structural gate
# (each laptop agent receives its own session; non-members cannot receive)
az role assignment create \
--subscription dfcc3a1c-fc89-4f3b-ab97-66464c60f9d8 \
--assignee-object-id "$DEVELOPER_GROUP_OID" --assignee-principal-type Group \
--role "Azure Service Bus Data Receiver" \
--scope "${SB_NS_ID}/queues/agent-commands"
# developer group: Data Sender on agent-results (laptop agent posts execution results)
az role assignment create \
--subscription dfcc3a1c-fc89-4f3b-ab97-66464c60f9d8 \
--assignee-object-id "$DEVELOPER_GROUP_OID" --assignee-principal-type Group \
--role "Azure Service Bus Data Sender" \
--scope "${SB_NS_ID}/queues/agent-results"
# mi-fabric-functions: Data Receiver on agent-results (triggers the serviceBusTrigger consumer)
az role assignment create \
--subscription dfcc3a1c-fc89-4f3b-ab97-66464c60f9d8 \
--assignee-object-id "$MI_FUNCTIONS_OID" --assignee-principal-type ServicePrincipal \
--role "Azure Service Bus Data Receiver" \
--scope "${SB_NS_ID}/queues/agent-results"
Use
--assignee-object-id+ explicit--assignee-principal-type(not--assignee) so the call skips the Graph name-lookup — the robust form for SPN / managed-identity / group assignees.
Step 4 — Conditional Access Baseline (IT / Entra — Daan lacks rights)
Apply an MFA conditional access policy scoped to the developer Entra group
(41489f76-7dbb-4fbc-a54b-ac07e673392b):
- Require MFA for all sign-ins by group members
- No device compliance requirement (supports unmanaged external-consultant devices)
- B2B guests in the group are included (allowed per design)
Step 5 — Azure Trusted Signing (code signing for agent binary)
The build-tray-installer ADO pipeline signs the PyInstaller .exe using Azure Trusted Signing.
- Confirm the ADO build SPN has the Trusted Signing Certificate Profile Signer role on the
Trusted Signing account (
codesign-gerisor equivalent) - Ensure the certificate profile
geris-devportal-agentexists in the account - After the first successful signed build, verify the signature:
sigcheck.exe -a agent-tray.exe
fabric-dev:// Retirement Notice
As of P-SB5 (Task Group F), the fabric-dev:// URL scheme is no longer an
authority-bearing command path. The following actions have been removed from the
URI handler and now flow exclusively over the signed Service Bus channel:
| Retired URI action | Replacement |
|---|---|
fabric-dev://dbt-build | POST /api/commands → CommandType.DBT_BUILD_LOCAL |
fabric-dev://teardown-local | POST /api/commands → CommandType.TEARDOWN_LOCAL |
fabric-dev://sync-gold-local | POST /api/commands → CommandType.SYNC_GOLD_LOCAL |
fabric-dev://refresh-bronze-local | POST /api/commands → CommandType.REFRESH_BRONZE_LOCAL |
fabric-dev://start-claude | POST /api/commands → CommandType.PROVISION_LOCAL |
fabric-dev://pr | POST /api/commands → (PR is a portal workflow, not a command) |
fabric-dev://provision | POST /api/commands → CommandType.PROVISION_LOCAL |
The scheme now handles two no-authority nudges only:
fabric-dev://open-feature?slug=<slug>— ensures the local worktree exists and opens the editor (no cloud delegation, no privileged child process).fabric-dev://show-job?job_id=<id>— focuses the local tray UI job view.
No security or RBAC changes are needed for this retirement. The HKCU
Software\Classes\fabric-dev registry key remains registered so existing
bookmarks continue to wake the tray — they simply no longer execute jobs.
Verification
After completing all steps, confirm:
SUB="dfcc3a1c-fc89-4f3b-ab97-66464c60f9d8"
SB_NS_ID="/subscriptions/$SUB/resourceGroups/rg-fabric-dbt-platform/providers/Microsoft.ServiceBus/namespaces/sb-geris-devportal"
# Namespace + queues exist
az servicebus queue list \
--namespace-name sb-geris-devportal \
-g rg-fabric-dbt-platform \
--query "[].{name:name,session:properties.requiresSession}" -o table
# Key exists
az keyvault key show --vault-name kv-fabric-dbt-keys --name devportal-command-signing \
--query "{name:name,kty:key.kty}" -o json
# RBAC on each queue
az role assignment list --subscription "$SUB" --scope "${SB_NS_ID}/queues/agent-commands" \
--query "[].{principal:principalId,role:roleDefinitionName}" -o table
az role assignment list --subscription "$SUB" --scope "${SB_NS_ID}/queues/agent-results" \
--query "[].{principal:principalId,role:roleDefinitionName}" -o table