Configuring Azure Authentication
This guide covers how to securely configure Azure authentication for Controlinfra to scan your infrastructure.
Quick Setup Checklist
For Azure VM runners with Managed Identity, you need:
- Enable System Managed Identity on your Azure VM
- Assign these roles to the managed identity:
Readeron the subscription or resource groupStorage Blob Data Contributoron the Terraform state storage accountContributoron the resource group containing your infrastructure
- Configure environment variables in the runner service:
ARM_USE_MSI=true ARM_SUBSCRIPTION_ID=your-subscription-id ARM_TENANT_ID=your-tenant-id - Set
use_azuread_auth = truein your Terraform backend configuration
Overview
Controlinfra needs Azure credentials to:
- Access your Terraform state (if using Azure Storage backend)
- Run
terraform planto detect drift - Query Azure APIs to compare actual vs. desired state
Authentication Methods
Controlinfra supports three authentication methods for Azure:
| Method | Runner Type | Security Level | Best For |
|---|---|---|---|
| Service Principal | Cloud or Self-Hosted | Standard | Quick setup, CI/CD pipelines |
| Managed Identity | Self-Hosted Only | High | Azure VM runners with system-assigned identity |
| OIDC (Workload Identity Federation) | Self-Hosted Only | Highest | No secrets, short-lived tokens, production |
Option 1: Service Principal
Use an Azure AD Service Principal with client ID and secret.
When to Use
- Getting started quickly
- Cloud runner (Controlinfra-managed)
- CI/CD pipelines
- Development/testing environments
Setup Steps
Step 1: Create a Service Principal
# Login to Azure CLI
az login
# Create Service Principal with Reader role on your subscription
az ad sp create-for-rbac \
--name "controlinfra-scanner" \
--role "Reader" \
--scopes /subscriptions/YOUR_SUBSCRIPTION_ID \
--output jsonOutput:
{
"appId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", // Client ID
"displayName": "controlinfra-scanner",
"password": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", // Client Secret
"tenant": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" // Tenant ID
}Save the Password
The client secret is only shown once. Save it securely before continuing.
Step 2: Grant Additional Permissions
For Terraform state stored in Azure Storage:
# Get your storage account resource ID
STORAGE_ID=$(az storage account show \
--name YOUR_STORAGE_ACCOUNT \
--resource-group YOUR_RESOURCE_GROUP \
--query id -o tsv)
# Assign Storage Blob Data Contributor role
az role assignment create \
--assignee YOUR_APP_ID \
--role "Storage Blob Data Contributor" \
--scope $STORAGE_IDStep 3: Configure in Controlinfra
- Go to Add Repository → Cloud Provider → Azure
- Select "Service Principal" authentication
- Enter your credentials:
- Subscription ID: Your Azure subscription ID
- Tenant ID: Your Azure AD tenant ID
- Client ID: The
appIdfrom the service principal - Client Secret: The
passwordfrom the service principal
Security Considerations
- Credentials are encrypted at rest with AES-256-GCM
- Rotate client secrets regularly (recommended: every 90 days)
- Use least-privilege permissions
Option 2: Managed Identity Recommended
Use the managed identity attached to your Azure VM. No credentials to manage!
Self-Hosted Runner Required
This option is only available when using a self-hosted runner on an Azure VM.
When to Use
- Self-hosted runner on Azure VM
- Enhanced security requirements
- No credential rotation needed
- Production environments
How It Works
┌─────────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Controlinfra │────▶│ Self-Hosted │────▶│ Azure APIs │
│ Cloud │ │ Runner (VM) │ │ │
└─────────────────────┘ └──────────────────┘ └─────────────────┘
│
System Managed Identity
(automatic credentials)Setup Steps
Step 1: Create Azure VM with Managed Identity
# Create resource group
az group create \
--name controlinfra-runner-rg \
--location eastus
# Create VM with system-assigned managed identity
az vm create \
--resource-group controlinfra-runner-rg \
--name controlinfra-runner-vm \
--image Ubuntu2204 \
--size Standard_B2s \
--admin-username adminuser \
--generate-ssh-keys \
--assign-identityOr enable on an existing VM:
az vm identity assign \
--resource-group YOUR_RESOURCE_GROUP \
--name YOUR_VM_NAMEStep 2: Get the Managed Identity Principal ID
az vm identity show \
--resource-group controlinfra-runner-rg \
--name controlinfra-runner-vm \
--query principalId -o tsvStep 3: Assign Required Roles
The managed identity needs permissions to:
- Read Azure resources for drift detection
- Access Terraform state in Azure Storage
# Get the principal ID
PRINCIPAL_ID=$(az vm identity show \
--resource-group controlinfra-runner-rg \
--name controlinfra-runner-vm \
--query principalId -o tsv)
# Assign Reader role on the resource group containing your infrastructure
az role assignment create \
--assignee-object-id $PRINCIPAL_ID \
--assignee-principal-type ServicePrincipal \
--role "Reader" \
--scope /subscriptions/YOUR_SUBSCRIPTION_ID/resourceGroups/YOUR_INFRA_RG
# Assign Storage Blob Data Contributor on your Terraform state storage account
az role assignment create \
--assignee-object-id $PRINCIPAL_ID \
--assignee-principal-type ServicePrincipal \
--role "Storage Blob Data Contributor" \
--scope /subscriptions/YOUR_SUBSCRIPTION_ID/resourceGroups/YOUR_RG/providers/Microsoft.Storage/storageAccounts/YOUR_STORAGE_ACCOUNT
# For full infrastructure management, assign Contributor role
az role assignment create \
--assignee-object-id $PRINCIPAL_ID \
--assignee-principal-type ServicePrincipal \
--role "Contributor" \
--scope /subscriptions/YOUR_SUBSCRIPTION_ID/resourceGroups/YOUR_INFRA_RGRequired Permissions
All three roles are typically required for successful drift detection:
| Role | Scope | Purpose |
|---|---|---|
| Reader | Resource Group or Subscription | Basic read access to Azure resources |
| Storage Blob Data Contributor | Storage Account | Access Terraform state file |
| Contributor | Resource Group | Required for listKeys/action and full resource state access |
Why Contributor? Terraform needs Microsoft.Storage/storageAccounts/listKeys/action and similar permissions to read the full state of resources like Storage Accounts. Reader role alone will cause "AuthorizationFailed" errors during terraform plan.
Step 4: Configure the Runner Service
Create the systemd service file at /etc/systemd/system/controlinfra-runner.service:
[Unit]
Description=Controlinfra Self-Hosted Runner
After=network.target
[Service]
Type=simple
User=controlinfra
WorkingDirectory=/opt/controlinfra-runner
# Azure Managed Identity configuration
Environment="ARM_USE_MSI=true"
Environment="ARM_SUBSCRIPTION_ID=YOUR_SUBSCRIPTION_ID"
Environment="ARM_TENANT_ID=YOUR_TENANT_ID"
ExecStart=/opt/controlinfra-runner/bin/controlinfra-agent
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.targetEnable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable controlinfra-runner
sudo systemctl start controlinfra-runnerStep 5: Verify the Setup
SSH into your runner VM and verify:
# Check the managed identity is working
az login --identity
# Get current identity
az account show
# Test reading a resource
az vm list --resource-group YOUR_INFRA_RG --output tableBenefits
- No credentials stored - Azure SDK automatically uses VM metadata service
- No rotation needed - Azure manages credentials automatically
- Audit-friendly - All actions tracked under the managed identity
- Secure - Credentials never leave your Azure environment
When Managed Identity Won't Work
If your Terraform providers.tf hardcodes client_id from a variable (e.g., client_id = var.azurerm_sp.client_id), the provider will try to use that specific identity instead of the VM's system-assigned identity. In this case, use OIDC or Service Principal authentication instead.
Option 3: OIDC (Workload Identity Federation) Recommended
Use OpenID Connect federation to authenticate without storing any secrets. Controlinfra acts as an OIDC identity provider — it issues short-lived signed tokens that Azure exchanges for access tokens.
Self-Hosted Runner Required
This option is only available when using a self-hosted runner on an Azure VM.
When to Use
- Terraform provider hardcodes
client_idinproviders.tf - No client secrets to manage or rotate
- Short-lived tokens (10 minutes) for maximum security
- Compliance requirements that prohibit stored credentials
How It Works
┌─────────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Controlinfra │──1──│ Self-Hosted │──3──│ Azure AD │
│ (OIDC Provider) │ │ Runner (VM) │ │ Token Exchange │
└─────────────────────┘ └──────────────────┘ └─────────────────┘
│ │ │
1. Signs JWT token 2. Sets ARM_OIDC_TOKEN 3. Exchanges JWT
with repo context + ARM_USE_OIDC=true for access token
+ ARM_CLIENT_IDSetup Steps
Step 1: Create an Azure App Registration
If you don't already have one, create an app registration for Controlinfra:
az ad app create --display-name "controlinfra-oidc"Note the Application (client) ID from the output.
Step 2: Add a Federated Credential
This tells Azure to trust tokens issued by Controlinfra. You only need to do this once per organization — a single federated credential covers all repositories in your Controlinfra org.
az ad app federated-credential create \
--id YOUR_APP_CLIENT_ID \
--parameters '{
"name": "controlinfra",
"issuer": "https://api.controlinfra.com",
"subject": "org:YOUR_ORG_ID",
"audiences": ["api://AzureADTokenExchange"],
"description": "Controlinfra OIDC federation"
}'Finding Your Org ID
Your Org ID is shown in the Controlinfra UI at Settings → Organization, or in the URL when viewing your organization page: /orgs/{ORG_ID}.
Use https://api.controlinfra.com as the issuer for production, or https://api-stage.controlinfra.com for staging.
Subject Must Match Exactly
The subject field must match exactly what Controlinfra generates. The format is:
org:{orgId}If the subject doesn't match, Azure will return error AADSTS700211: No matching federated identity record found.
Step 3: Assign Required Roles
Grant the app registration access to your Azure resources:
# Get the app's service principal object ID
SP_OBJECT_ID=$(az ad sp show --id YOUR_APP_CLIENT_ID --query id -o tsv)
# Reader role on the subscription or resource group
az role assignment create \
--assignee-object-id $SP_OBJECT_ID \
--assignee-principal-type ServicePrincipal \
--role "Reader" \
--scope /subscriptions/YOUR_SUBSCRIPTION_ID
# Storage Blob Data Contributor for Terraform state
az role assignment create \
--assignee-object-id $SP_OBJECT_ID \
--assignee-principal-type ServicePrincipal \
--role "Storage Blob Data Contributor" \
--scope /subscriptions/YOUR_SUBSCRIPTION_ID/resourceGroups/YOUR_RG/providers/Microsoft.Storage/storageAccounts/YOUR_STORAGE_ACCOUNT
# Contributor for full infrastructure access (if needed)
az role assignment create \
--assignee-object-id $SP_OBJECT_ID \
--assignee-principal-type ServicePrincipal \
--role "Contributor" \
--scope /subscriptions/YOUR_SUBSCRIPTION_ID/resourceGroups/YOUR_INFRA_RGStep 4: Configure in Controlinfra
- Go to Add Repository (or edit existing) -> Cloud Provider -> Azure
- Select "OIDC" authentication
- Enter:
- Subscription ID: Your Azure subscription ID
- Tenant ID: Your Azure AD tenant ID
- Client ID: The app registration's Application (client) ID
No client secret is needed — Controlinfra handles the token exchange automatically.
Step 5: Verify OIDC Endpoints
Controlinfra exposes standard OIDC discovery endpoints that Azure uses to validate tokens:
- Discovery:
https://api.controlinfra.com/.well-known/openid-configuration - JWKS:
https://api.controlinfra.com/.well-known/jwks.json
You can verify these are accessible:
curl -s https://api.controlinfra.com/.well-known/openid-configuration | jq .Benefits
- No secrets stored - Only client ID, tenant ID, and subscription ID are saved
- Short-lived tokens - JWT tokens expire in 10 minutes
- No rotation needed - Tokens are generated fresh for each scan
- Audit-friendly - Each token includes workspace and repository context
- Works with hardcoded providers - Compatible with
providers.tfthat usesclient_idvariable
Troubleshooting OIDC
"No matching federated identity record found" (AADSTS700211)
AADSTS700211: No matching federated identity record found for presented assertion issuer- Check the issuer URL matches exactly —
https://api.controlinfra.com(production) orhttps://api-stage.controlinfra.com(staging) - Check the subject matches the format
workspace:{workspaceId}:repo:{repoConfigId} - Check the audience is
api://AzureADTokenExchange - Wait 1-2 minutes after creating the federated credential — propagation can take time
"Identity not found"
ManagedIdentityAuthorizer: Identity not foundThis means Terraform is trying managed identity instead of OIDC. Ensure:
- You selected OIDC (not Managed Identity) in the Controlinfra wizard
- The
ARM_USE_OIDC=trueandARM_OIDC_TOKENenv vars are being set by the runner
Required Azure Permissions
Controlinfra needs two types of permissions:
- Read-Only Access to Azure resources for drift detection
- Terraform State Access to read state from Azure Storage
Recommended: Built-in Roles
The simplest approach using Azure built-in roles:
| Role | Scope | Purpose |
|---|---|---|
| Reader | Resource Group or Subscription | Query Azure resources |
| Storage Blob Data Contributor | Storage Account | Read/write Terraform state |
# Reader role on the resource group
az role assignment create \
--assignee YOUR_PRINCIPAL_ID \
--role "Reader" \
--scope /subscriptions/SUB_ID/resourceGroups/RG_NAME
# Storage Blob Data Contributor on state storage
az role assignment create \
--assignee YOUR_PRINCIPAL_ID \
--role "Storage Blob Data Contributor" \
--scope /subscriptions/SUB_ID/resourceGroups/RG_NAME/providers/Microsoft.Storage/storageAccounts/STORAGE_NAMEAlternative: Custom Role Definition
For more restrictive access, create a custom role:
{
"Name": "Controlinfra Drift Scanner",
"Description": "Read-only access for drift detection",
"Actions": [
"Microsoft.Compute/virtualMachines/read",
"Microsoft.Compute/disks/read",
"Microsoft.Network/*/read",
"Microsoft.Storage/storageAccounts/read",
"Microsoft.Storage/storageAccounts/listKeys/action",
"Microsoft.Storage/storageAccounts/blobServices/containers/read",
"Microsoft.KeyVault/vaults/read",
"Microsoft.Sql/servers/read",
"Microsoft.Sql/servers/databases/read",
"Microsoft.Web/sites/read",
"Microsoft.ServiceBus/namespaces/read",
"Microsoft.Resources/subscriptions/resourceGroups/read"
],
"NotActions": [],
"AssignableScopes": [
"/subscriptions/YOUR_SUBSCRIPTION_ID"
]
}Create the custom role:
az role definition create --role-definition custom-role.jsonTerraform Backend Configuration
When using Azure Storage as your Terraform backend, ensure the backend block includes Azure AD authentication:
terraform {
backend "azurerm" {
resource_group_name = "terraform-state-rg"
storage_account_name = "tfstatestorage"
container_name = "tfstate"
key = "prod/terraform.tfstate"
use_azuread_auth = true # Required for Managed Identity
}
}Important
The use_azuread_auth = true setting is required when using Managed Identity or OIDC authentication. Without it, Terraform will attempt to use storage account keys which may not be available.
Environment Variables Reference
Service Principal Authentication
ARM_SUBSCRIPTION_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
ARM_TENANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
ARM_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
ARM_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxManaged Identity Authentication
ARM_USE_MSI=true
ARM_SUBSCRIPTION_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
ARM_TENANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxOIDC Authentication
ARM_USE_OIDC=true
ARM_OIDC_TOKEN=eyJhbGciOiJSUzI1NiIs... # Auto-generated by Controlinfra
ARM_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
ARM_TENANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
ARM_SUBSCRIPTION_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxINFO
ARM_OIDC_TOKEN is generated automatically by Controlinfra for each scan. You do not need to set this manually.
Optional Settings
# For Azure Government, China, or German clouds
ARM_ENVIRONMENT=public # or usgovernment, china, germanSecurity Best Practices
1. Use Managed Identity for Production
- Eliminates credential management
- Automatic rotation handled by Azure
- Better audit trail
2. Use Least Privilege
Only grant permissions for resources Terraform manages:
# Scope to specific resource group instead of subscription
az role assignment create \
--assignee $PRINCIPAL_ID \
--role "Reader" \
--scope /subscriptions/SUB_ID/resourceGroups/prod-infra-rg3. Monitor Credential Usage
Enable Azure Activity Log to track API calls:
# View recent activity for your service principal
az monitor activity-log list \
--caller YOUR_APP_ID \
--start-time 2024-01-01 \
--output table4. Rotate Service Principal Secrets
If using Service Principal, rotate secrets regularly:
# Create new secret (old one remains valid)
az ad sp credential reset \
--name controlinfra-scanner \
--credential-description "rotated-$(date +%Y%m%d)"
# Update Controlinfra with new secret
# Then delete old credentialTroubleshooting
"AuthorizationFailed" Error
The client does not have authorization to perform actionVerify the role is assigned:
bashaz role assignment list --assignee YOUR_PRINCIPAL_ID --output tableCheck the scope is correct (subscription vs resource group)
Wait a few minutes - role assignments can take up to 10 minutes to propagate
"AuthorizationPermissionMismatch" Error
This request is not authorized to perform this operation using this permission- The identity needs Storage Blob Data Contributor role on the storage account
- Reader role alone is not sufficient for blob access
"Azure CLI auth only supported as User" Error
Authenticating using the Azure CLI is only supported as a User (not a Service Principal)This occurs when running az login --identity but Terraform expects ARM environment variables.
Solution: Add ARM_USE_MSI to your runner configuration:
# In systemd service file or environment
Environment="ARM_USE_MSI=true"
Environment="ARM_SUBSCRIPTION_ID=your-subscription-id"
Environment="ARM_TENANT_ID=your-tenant-id"Managed Identity Not Found
ManagedIdentityCredential: IMDS endpoint unavailableVerify managed identity is enabled on the VM:
bashaz vm identity show --resource-group RG --name VM_NAMECheck from inside the VM:
bashcurl -s -H "Metadata: true" \ "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"
State File Access Issues
Error: Failed to get existing workspaces: containers.Client#ListBlobs: StatusCode=403- Verify Storage Blob Data Contributor role on the storage account
- Check
use_azuread_auth = truein Terraform backend config - Ensure the container exists and is accessible
Multi-Subscription Setup
For scanning infrastructure across multiple Azure subscriptions:
Option 1: Multiple Service Principals
Create separate service principals for each subscription and configure in Controlinfra per-repository.
Option 2: Cross-Subscription Managed Identity
Grant the managed identity roles in multiple subscriptions:
# Get principal ID
PRINCIPAL_ID=$(az vm identity show \
--resource-group runner-rg \
--name runner-vm \
--query principalId -o tsv)
# Assign in subscription 1
az role assignment create \
--assignee-object-id $PRINCIPAL_ID \
--assignee-principal-type ServicePrincipal \
--role "Reader" \
--scope /subscriptions/SUBSCRIPTION_1_ID
# Assign in subscription 2
az role assignment create \
--assignee-object-id $PRINCIPAL_ID \
--assignee-principal-type ServicePrincipal \
--role "Reader" \
--scope /subscriptions/SUBSCRIPTION_2_IDNext Steps
- Set Up Self-Hosted Runners - Deploy runners on Azure VMs
- Configure Terraform Backend - Backend settings
- Run Your First Scan - Start scanning