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 two authentication methods for Azure, each suited for different environments and security requirements:
| Method | Runner Type | Security Level | Best For |
|---|---|---|---|
| Service Principal | Cloud or Self-Hosted | Standard | Quick setup, CI/CD pipelines |
| Managed Identity | Self-Hosted Only | Highest | Azure VM runners, 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
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 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-xxxxxxxxxxxxOptional 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