Calibrant Relay Setup
The Calibrant Relay is a small service you deploy in your own Azure subscription. It handles two things for you:
- Tenant Healthcheck — run M365 configuration audits using PowerShell against your tenant via Managed Identity
- Security Baseline — M365 Security Baseline, CISA SCuBA, and ACSC Essential Eight assessments with automated M365 configuration checks
The relay is a Windows VM running Windows Server 2025 Core (no desktop GUI, minimal attack surface). It uses a Managed Identity for all M365 authentication — no passwords, no service account credentials, no domain join required. The VM makes outbound-only HTTPS connections to Calibrant and Microsoft APIs. No inbound ports are open.
Most deployment time is spent waiting for Azure to provision the VM and install software. Timing varies by Azure region and service availability.
- An Azure subscription where you can create resources (Contributor role or higher)
- A Calibrant account
- The Azure CLI installed on your local machine (you'll run a few commands)
Step 1 — Install the Azure CLI
If you already have the Azure CLI installed, skip to Step 2.
# Windows (PowerShell or Command Prompt)
winget install Microsoft.AzureCLI
# Mac
brew install azure-cli
# After installing, sign in:
az loginVerify you're pointed at the right subscription:
az account show --query "{name:name, id:id}" -o table
# Switch subscriptions if needed:
az account set --subscription "Your Subscription Name"Step 2 — Create a relay in Calibrant
Go to Connections and scroll to the Calibrant Relay section. Click Add Relay, give it a name (e.g. "Production Relay"), and click Create Relay.
You'll see a Relay API Key — copy it now.
Step 3 — Create a resource group
az group create --name calibrant-relay-rg --location eastusPick the Azure region closest to your users — eastus, westeurope,australiaeast, etc.
Step 4 — Deploy the relay VM
Download the Bicep template and run the deployment command. Replace cal_YourKeyHere with the API key you copied in Step 2, and set a secure password (you'll almost never need it — it's just required by Azure for the VM admin account).
# Download the Bicep template
curl -O https://www.calibrant.ai/relay/main.bicep
# Deploy (Azure provisioning time varies)
az deployment group create \
--resource-group calibrant-relay-rg \
--template-file main.bicep \
--parameters calibrantApiKey='cal_YourKeyHere' adminPassword='YourSecurePassword123!'Optional: verify the template before you deploy it. The template names the exact provisioning script it will run, by SHA-256, and the VM refuses to execute anything else — if the downloaded script does not match, provisioning aborts before it runs. You can check that chain yourself:
# The digest the template pins
grep bootstrapSha256 main.bicep
# The script the template points at, and its actual digest
grep bootstrapUrl main.bicep
curl -sO https://www.calibrant.ai/relay/bootstrap-<sha>.ps1
shasum -a 256 bootstrap-<sha>.ps1 # must equal both the filename and the pinThe script URL is content-addressed — the digest is in the filename — so a template you keep and reuse always resolves to the same reviewed script, even after we publish newer versions. Note this verifies internal consistency: that the script the VM runs is the one the template names. It is not independent third-party attestation, since both the template and the script are served by us.
Optional: give the relay a stable outbound IP
By default the relay uses Azure's default outbound access — free, but the egress IP is Microsoft-owned, shared, and, in Microsoft's words, "can change without notice". If you need a fixed IP to allowlist on a firewall or a Conditional Access named location, deploy with a NAT Gateway:
az deployment group create \
--resource-group calibrant-relay-rg \
--template-file main.bicep \
--parameters calibrantApiKey='cal_YourKeyHere' adminPassword='YourSecurePassword123!' \
enableNatGateway=true
# Then read the IP you now own:
az network public-ip show --resource-group calibrant-relay-rg \
--name calibrant-relay-nat-pip --query ipAddress -o tsvMicrosoft is retiring default outbound access for virtual networks created with Network API versions released after 31 March 2026. This template sets the subnet's outbound behaviour explicitly rather than relying on the API version's default, so whichever default applies is irrelevant here and your existing deployments will not change — but new deployments elsewhere in your estate may behave differently.
Optional: send relay logs to Log Analytics
The relay keeps 60 days of logs on the VM at C:\calibrant-relay\, readable only by SYSTEM and Administrators. No workspace or monitoring agent is deployed — earlier versions created both. If your relay dates from one of those versions, the monitoring agent goes away with the old VM when you upgrade (delete the VM, then redeploy — see the caveat above; an in-place redeploy is rejected by Azure), and the workspace is a separate resource you can delete yourself.
If you want those logs centralised, add a Data Collection Rule against C:\calibrant-relay\*.log in your own workspace. That is standard Azure Monitor and it keeps the choice, the cost and the retention with you:
- Create (or pick) a Log Analytics workspace and a custom table ending
_CL - Create a Data Collection Rule with a Custom Text Logs data source pointing at
C:\calibrant-relay\*.log. A Data Collection Endpoint is required for this data source - Install the Azure Monitor Agent on the VM and associate the rule with it
When the deployment finishes, you'll see output including a managedIdentityPrincipalId. Copy this value — you'll need it in Step 5 to grant M365 permissions.
What gets deployed:
- A Windows Server 2025 Core VM (no GUI, Standard_B2ls_v2 — 2 vCPU, 4 GiB RAM, Trusted Launch)
- A User-assigned Managed Identity (for M365 authentication)
- A Virtual Network with a Network Security Group (outbound-only, all inbound denied)
- A Custom Script Extension that automatically installs Node.js, PowerShell 7, all M365 modules, and the relay service
View main.bicep template
// Calibrant Relay — Windows VM deployment with Managed Identity
// No domain join required. No inbound ports. Outbound HTTPS only.
// Full source: https://github.com/fusedad/calibrant/blob/main/relay/deploy/main.bicep
@secure()
param calibrantApiKey string
@secure()
@minLength(12)
param adminPassword string
param calibrantApiUrl string = 'https://www.calibrant.ai'
param location string = resourceGroup().location
param pollIntervalMs string = '600000'
param adminUsername string = 'calibrant-admin'
param vmSize string = 'Standard_B2ls_v2'
param relayZipUrl string = 'https://www.calibrant.ai/relay/calibrant-relay.zip'
resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = {
name: 'calibrant-relay-identity'
location: location
}
// ... (NSG, VNet, NIC, VM, Extensions)
// Full template at the URL aboveStep 5 — Grant M365 permissions to the Managed Identity
The relay's Managed Identity needs permission to read your M365 tenant configuration. This is a one-time setup. The script grants 18 Microsoft Graph application roles plus Exchange.ManageAsApp. The Graph roles are read-only; the Exchange grant enables app authentication and its authority is limited by the one Entra ID directory role — Global Reader, also read-only. One optional registration is not read-only. Read the next callout before you run it.
AccessReview.Read.All can read all reviews, reviewers, decisions, and settings; the Office installation role can read organization installation settings; and the Backup role can read backup configuration and protected resource lists. Calibrant's compiled collectors return only aggregate access-review coverage, the update channel, and backup-policy counts. That data minimization does not reduce Microsoft's grant, so treat the relay as a privileged reader. Review the exact permission list and security architecture before consenting.-EnablePowerPlatform, so read this before choosing that switch.- What it is. Microsoft exposes the Power Platform admin API to service principals through a management-application registration, keyed by the identity's client ID. It is not an Entra ID role and does not appear in your directory role list.
- What it permits. The whole admin API, reads and writes. We measured an identity holding only this registration issuing a
DELETEagainst an environment and receiving204— a refusal would have been403. It could in principle delete an environment or rewrite DLP policy. Microsoft offers no read-only variant. - What Calibrant does with it. Only
GETs, from a fixed catalogue compiled into the relay build. The portal cannot send the relay different code. That constrains Calibrant; it does not constrain the credential. - What it buys. One baseline control (CAL-DAT-002) and four CISA SCuBA controls.
- It is off unless you ask for it. Running the script normally grants only read-only permissions and prints
Power Platform registration: SKIPPED. To enable it, re-run with-EnablePowerPlatform. If you enabled it and changed your mind, remove the registration with theDELETEcommand in Removing the Relay — the relay does not need redeploying. Declining costs you the Power Platform checks, which report not evaluated; no other category is affected.
Download and run the permission grant script:
↓ Download grant-healthcheck-permissions.ps1
# Windows
Get-FileHash .\grant-healthcheck-permissions.ps1 -Algorithm SHA256
# Mac / Linux
shasum -a 256 grant-healthcheck-permissions.ps1The digest is generated from the published file at build time, so it always matches what this site serves. It catches a corrupted or modified download — it is not independent attestation, since the digest and the file come from the same origin. The script is short and readable; for a Global Administrator session, reading it is the strongest check available.
# In a pwsh 7 terminal on your local machine (not the VM):
./grant-healthcheck-permissions.ps1The script will open a browser for sign-in (requires a Global Administrator account), grant all necessary permissions, and print a validation report showing every permission as [OK].
Optional: grant the Graph app roles with Bicep instead
If you prefer declarative grants, download grants.bicep and its Bicep config (which pulls in the Microsoft Graph Bicep extension). The config is served as grants-bicepconfig.json but must be saved as bicepconfig.json — az bicep only reads a file with exactly that name in the working directory, which the second command below does for you:
curl -O https://www.calibrant.ai/relay/grants.bicep
curl -o bicepconfig.json https://www.calibrant.ai/relay/grants-bicepconfig.jsonIt covers only the 18 Microsoft Graph app roles plus Exchange.ManageAsApp — all 19 app-role grants. The 18 Graph roles are explicit read permissions. The Exchange grant enables app authentication; Exchange access becomes read-only when you separately assign only Global Reader. The Global Reader directory role and the Power Platform BAP management-application registration still require the PowerShell script (without that registration, every Power Platform check reports not evaluated). The deployer needs Global Administrator (the script) or at minimum Privileged Role Administrator (the Bicep path), and the template does not perform the legacy-role cleanup the script does on re-run.
One useful property of this path: the template cannot create the Power Platform registration, so a Bicep-only grant is read-only by construction. You would then add Global Reader by hand rather than running the script at all.
az deployment group create \
--resource-group calibrant-relay-rg \
--template-file grants.bicep \
--parameters managedIdentityPrincipalId=<value from Step 4 output>For the full list of what the script grants and why, see Permission details below.
Step 6 — Confirm the Power Platform decision (optional checks)
There is nothing new to grant here. The grant script in Step 5 either created the Power Platform management-application registration or skipped it, and this step is where you confirm which happened matches what you intended in the Step 5 callout.
| What the script printed | What it means |
|---|---|
[OK] Registered with Power Platform BAP API | The registration exists. Power Platform checks will run. The identity now holds a credential whose admin API also permits writes — if that was not what you wanted, remove it with the DELETE command here. |
SKIPPED | No registration was created, and every permission the relay holds is read-only. This is what you get unless you pass -EnablePowerPlatform. To enable it later, run az login and re-run the script with that switch. |
[SKIP] or [WARN] | You asked for the registration but the script could not obtain a Power Platform token. Run az login and re-run with -EnablePowerPlatform, or use the two manual commands the script prints. |
Declining costs one baseline control (CAL-DAT-002) and four CISA SCuBA controls; those checks report not evaluated and nothing else changes. The measurements behind this trade are in Why Some Checks Are Manual.
Step 7 — Enable Fabric / Power BI checks (optional)
Skip this and the Fabric checks report not evaluated. Enabling it unlocks twelve baseline controls and eight CISA SCuBA controls. Unlike the rest of setup, this cannot be scripted — the Fabric admin portal has no API for these settings, so it is a few clicks by hand.
Fabric tenant settings checks (the Fabric & Power BI baseline area) use the Fabric admin REST API (https://api.fabric.microsoft.com/v1/admin/tenantsettings) via Managed Identity. No Entra ID directory role is required — access comes from the security group and portal settings below. You must configure these in the Fabric admin portal.
calibrant-relay-identity service principal to it, and specify that group in each setting below.Step 1: Create a security group in Microsoft Entra ID (e.g. FabricApiAccess) and add the calibrant-relay-identity service principal as a member.
Step 2: In the Fabric admin portal, enable these two settings (they are in different sections of Tenant settings):
- Tenant settings > Developer settings > Service principals can call Fabric public APIs — enable, select "Specific security groups", and add your
FabricApiAccessgroup - Tenant settings > Admin API settings > Service principals can access read-only admin APIs — enable, select "Specific security groups", and add the same group
Enable only those two. Leave the neighbouring service-principal settings alone.
Which neighbouring settings to leave off, and why
Three settings sit next to the two above and look like they might help. None is needed by the scan, and two of them are settings this product's own baseline expects to find switched off — so enabling them to make setup work would make your own assessment fail.
- Allow service principals to create and use profiles — CAL-FAB-011 / CIS 9.1.11. Profiles can impersonate users.
- Service principals can create workspaces, connections, and deployment pipelines — CAL-FAB-012.
- Service principals can access admin APIs used for updates — write access; the relay only reads.
Settings take up to 15 minutes to propagate. Fabric checks will show as "Not Evaluated" until the settings take effect.
Step 8 — Verify the relay is connected
Go to Connections in Calibrant. Within 30 seconds of the bootstrap completing, the relay should show as Online with a green indicator.
If it's still showing offline after a minute, check the relay logs:
az vm run-command invoke \
--name calibrant-relay-vm \
--resource-group calibrant-relay-rg \
--command-id RunPowerShellScript \
--scripts "Get-Content C:\calibrant-relay\relay.log -Tail 30"You're done
The relay is running and scanning. Everything below is there when you need it — nothing further is required to use the product.
- Relay reference — exact permissions granted and why, logs, environment variables, pre-installed tools, and who can reach your tenant through the relay
- Relay updates — how new versions are approved and installed
- Removing the relay — full offboarding checklist
Troubleshooting
- Relay shows Offline after deployment — the bootstrap script may still be running. Azure can take several minutes to install all software. Check progress in the Azure portal: go to your VM → Extensions + applications → CustomScriptExtension.
- Bootstrap failed — check the extension logs in the portal or run:
az vm run-command invoke --name calibrant-relay-vm --resource-group calibrant-relay-rg --command-id RunPowerShellScript --scripts "Get-Content 'C:\WindowsAzure\Logs\Plugins\Microsoft.Compute.CustomScriptExtension\1.10\CustomScriptHandler.log' -Tail 50" - PowerShell cmdlets return Access Denied during healthcheck — the Managed Identity permissions may not have propagated yet. Wait 10 minutes after running the grant script and try again.
- Service starts but shows no agents in logs — check that the API key is correct. Re-run the deployment with the correct key, or update it via Run Command.
AppEnvironmentExtrareplaces the whole extra block rather than merging into it, so anything you do not pass back is dropped from it. The relay keeps working regardless, becauseCALIBRANT_API_URL,AZURE_CLIENT_ID, andPOLL_INTERVAL_MSare also set at machine scope and the service inherits them. The API key is the one value that lives only here — so passing every variable back is the habit to keep.First get the Managed Identity client ID:
az identity show --name calibrant-relay-identity --resource-group calibrant-relay-rg --query clientId -o tsvThen set the whole block at once:
az vm run-command invoke --name calibrant-relay-vm --resource-group calibrant-relay-rg --command-id RunPowerShellScript --scripts "nssm set CalibrantRelay AppEnvironmentExtra 'CALIBRANT_API_URL=https://www.calibrant.ai' 'CALIBRANT_API_KEY=cal_NewKey' 'AZURE_CLIENT_ID=<client-id>' 'POLL_INTERVAL_MS=600000'; nssm set CalibrantRelayUpdater AppEnvironmentExtra 'CALIBRANT_API_URL=https://www.calibrant.ai' 'CALIBRANT_API_KEY=cal_NewKey'; Restart-Service CalibrantRelay -Force; Restart-Service CalibrantRelayUpdater -Force"The key lives only in the service environment, which only the service account can read. Do not also set it as a machine-wide environment variable — that copy is readable by any local user via the registry.
Removing the relay
When you are done with the relay, see Removing the Relay for the full offboarding runbook and a read-only script that verifies nothing was left behind. Note that deleting the VM alone is not enough — the Managed Identity is a separate resource with its own lifecycle, and it is the thing that holds the permissions.