Skip to content

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:

  1. Enable System Managed Identity on your Azure VM
  2. Assign these roles to the managed identity:
    • Reader on the subscription or resource group
    • Storage Blob Data Contributor on the Terraform state storage account
    • Contributor on the resource group containing your infrastructure
  3. Configure environment variables in the runner service:
    ARM_USE_MSI=true
    ARM_SUBSCRIPTION_ID=your-subscription-id
    ARM_TENANT_ID=your-tenant-id
  4. Set use_azuread_auth = true in your Terraform backend configuration

Overview

Controlinfra needs Azure credentials to:

  1. Access your Terraform state (if using Azure Storage backend)
  2. Run terraform plan to detect drift
  3. 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:

MethodRunner TypeSecurity LevelBest For
Service PrincipalCloud or Self-HostedStandardQuick setup, CI/CD pipelines
Managed IdentitySelf-Hosted OnlyHighestAzure 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

bash
# 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 json

Output:

json
{
  "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:

bash
# 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_ID

Step 3: Configure in Controlinfra

  1. Go to Add RepositoryCloud ProviderAzure
  2. Select "Service Principal" authentication
  3. Enter your credentials:
    • Subscription ID: Your Azure subscription ID
    • Tenant ID: Your Azure AD tenant ID
    • Client ID: The appId from the service principal
    • Client Secret: The password from 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

bash
# 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-identity

Or enable on an existing VM:

bash
az vm identity assign \
  --resource-group YOUR_RESOURCE_GROUP \
  --name YOUR_VM_NAME

Step 2: Get the Managed Identity Principal ID

bash
az vm identity show \
  --resource-group controlinfra-runner-rg \
  --name controlinfra-runner-vm \
  --query principalId -o tsv

Step 3: Assign Required Roles

The managed identity needs permissions to:

  1. Read Azure resources for drift detection
  2. Access Terraform state in Azure Storage
bash
# 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_RG

Required Permissions

All three roles are typically required for successful drift detection:

RoleScopePurpose
ReaderResource Group or SubscriptionBasic read access to Azure resources
Storage Blob Data ContributorStorage AccountAccess Terraform state file
ContributorResource GroupRequired 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:

ini
[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.target

Enable and start the service:

bash
sudo systemctl daemon-reload
sudo systemctl enable controlinfra-runner
sudo systemctl start controlinfra-runner

Step 5: Verify the Setup

SSH into your runner VM and verify:

bash
# 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 table

Benefits

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

  1. Read-Only Access to Azure resources for drift detection
  2. Terraform State Access to read state from Azure Storage

The simplest approach using Azure built-in roles:

RoleScopePurpose
ReaderResource Group or SubscriptionQuery Azure resources
Storage Blob Data ContributorStorage AccountRead/write Terraform state
bash
# 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_NAME

Alternative: Custom Role Definition

For more restrictive access, create a custom role:

json
{
  "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:

bash
az role definition create --role-definition custom-role.json

Terraform Backend Configuration

When using Azure Storage as your Terraform backend, ensure the backend block includes Azure AD authentication:

hcl
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

bash
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=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Managed Identity Authentication

bash
ARM_USE_MSI=true
ARM_SUBSCRIPTION_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
ARM_TENANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

Optional Settings

bash
# For Azure Government, China, or German clouds
ARM_ENVIRONMENT=public  # or usgovernment, china, german

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

bash
# Scope to specific resource group instead of subscription
az role assignment create \
  --assignee $PRINCIPAL_ID \
  --role "Reader" \
  --scope /subscriptions/SUB_ID/resourceGroups/prod-infra-rg

3. Monitor Credential Usage

Enable Azure Activity Log to track API calls:

bash
# View recent activity for your service principal
az monitor activity-log list \
  --caller YOUR_APP_ID \
  --start-time 2024-01-01 \
  --output table

4. Rotate Service Principal Secrets

If using Service Principal, rotate secrets regularly:

bash
# 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 credential

Troubleshooting

"AuthorizationFailed" Error

The client does not have authorization to perform action
  1. Verify the role is assigned:

    bash
    az role assignment list --assignee YOUR_PRINCIPAL_ID --output table
  2. Check the scope is correct (subscription vs resource group)

  3. 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:

bash
# 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 unavailable
  1. Verify managed identity is enabled on the VM:

    bash
    az vm identity show --resource-group RG --name VM_NAME
  2. Check from inside the VM:

    bash
    curl -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
  1. Verify Storage Blob Data Contributor role on the storage account
  2. Check use_azuread_auth = true in Terraform backend config
  3. 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:

bash
# 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_ID

Next Steps

AI-powered infrastructure drift detection