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.

Commercial cloud only. The relay and Calibrant scans currently support only Microsoft's commercial cloud. Microsoft 365 GCC, GCC High, DoD, China/21Vianet, and every other sovereign or national cloud are not supported.
What you need before starting:
  • 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 login

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

Save the API key. It's shown only once. If you lose it, you can regenerate it from the Connections page (which will invalidate the old one).

Step 3 — Create a resource group

az group create --name calibrant-relay-rg --location eastus

Pick 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!'
Upgrading an existing relay? Moving an existing relay to the Windows Server 2025 / Trusted Launch template requires deleting the old VM first — its disk and NIC are removed with it, while the Managed Identity and its permission grants survive — then redeploying. An in-place redeploy over the old VM is rejected by Azure.

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 pin

The 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 tsv
This adds cost to your Azure bill — roughly USD 35–45/month for the gateway hours, the Standard public IP, and data processed. It is off by default for that reason. The VM still has no public IP and remains inbound-unreachable either way; a NAT Gateway is outbound-only.

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

  1. Create (or pick) a Log Analytics workspace and a custom table ending _CL
  2. 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
  3. Install the Azure Monitor Agent on the VM and associate the rule with it
The relay logs the structure of each scan result — byte counts, section names, error counts — not its content, and error text has user names, IDs and customer domains removed. They are useful for diagnosing a failing scan and are not a second copy of your tenant configuration.

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 above

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

Read-only does not mean narrow. These are unattended, tenant-wide application permissions. 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.
The optional Power Platform registration is not read-only. A normal run does not create it. The script creates it only when you explicitly pass -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 DELETE against an environment and receiving 204 — a refusal would have been 403. 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 the DELETE command 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.
The full reasoning, including the measurements, is in Why Some Checks Are Manual. Declining is a supported configuration, not a broken install.

Download and run the permission grant script:

↓ Download grant-healthcheck-permissions.ps1

Verify the script before you run it. You are about to paste a downloaded script into a Global Administrator session, where it can assign directory roles. Check the digest first — it takes one command.
SHA-256: 2f8e3d45 ab154220 de884a39 8281fa5f 752a2c0e a5f170ca 910d5c54 c2cbf445
# Windows
Get-FileHash .\grant-healthcheck-permissions.ps1 -Algorithm SHA256

# Mac / Linux
shasum -a 256 grant-healthcheck-permissions.ps1

The 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.ps1

The 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.json

It 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 printedWhat it means
[OK] Registered with Power Platform BAP APIThe 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.
SKIPPEDNo 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.

Security group required. The Fabric admin APIs do not work with "The entire organization" — you must create an Entra ID security group, add the 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):

  1. Tenant settings > Developer settings > Service principals can call Fabric public APIs — enable, select "Specific security groups", and add your FabricApiAccess group
  2. 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.
Both settings are required. The first is under "Developer settings" and the second is under "Admin API settings" — a separate section further down the Tenant settings page. Missing either one will result in a 403 Forbidden error. Do not add any Power BI application permissions (e.g., Tenant.Read.All) to the service principal — per Microsoft, these are unnecessary and can cause errors.

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 + applicationsCustomScriptExtension.
  • 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.
    AppEnvironmentExtra replaces 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, because CALIBRANT_API_URL, AZURE_CLIENT_ID, and POLL_INTERVAL_MS are 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 tsv

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