Monday Cloud Tips feature graphic showing an orange Monday coffee mug and six outline icons representing security auditing and configuration management on an Azure-blue background.

Azure Machine Configuration: Audit VM Settings Without Agents

The Problem

You’re managing hundreds of Azure VMs but have no visibility into what’s actually configured inside them. Are Windows servers allowing weak passwords? Is SSH configured securely on Linux? Which VMs have configuration drift from your security baseline? Without installing agents on every VM and building custom monitoring, you’re completely blind to the actual OS configuration – and auditors aren’t impressed by “we think they’re configured correctly.”

The Solution

Azure Machine Configuration (formerly Guest Configuration) audits settings inside your VMs using Azure Policy’s built-in extension. It checks OS configurations, password policies, security baselines, and installed software without additional agents or performance overhead. The extension is automatically deployed via policy, connects to Azure’s guest configuration service, and reports compliance directly into Azure Policy – giving you automated, continuous compliance auditing across your entire VM estate.

Essential Machine Configuration Implementations

1. Mass Deployment Script

# Deploy Machine Configuration across subscription
# Includes prerequisites and common audit policies

param(
    [Parameter(Mandatory=$true)]
    [string]$SubscriptionId,
    
    [Parameter(Mandatory=$false)]
    [string]$ManagementGroupId,
    
    [Parameter(Mandatory=$false)]
    [switch]$IncludeArcServers
)

Connect-AzAccount
Set-AzContext -SubscriptionId $SubscriptionId

$scope = if ($ManagementGroupId) {
    "/providers/Microsoft.Management/managementGroups/$ManagementGroupId"
} else {
    "/subscriptions/$SubscriptionId"
}

Write-Host "šŸš€ Deploying Machine Configuration to: $scope"

# 1. Deploy prerequisite policy (creates managed identities + extension)
Write-Host "`nšŸ“¦ Deploying prerequisites..."

$prerequisiteInit = Get-AzPolicySetDefinition | Where-Object {
    $_.Properties.DisplayName -like "*Deploy prerequisites to enable*Guest Configuration*"
}

$prereqAssignment = New-AzPolicyAssignment `
    -Name "mc-prerequisites-$(Get-Date -Format 'yyyyMMdd')" `
    -DisplayName "Machine Configuration Prerequisites" `
    -Scope $scope `
    -PolicySetDefinition $prerequisiteInit `
    -Location "uksouth" `
    -AssignIdentity `
    -IdentityType "SystemAssigned"

# Grant Contributor to managed identity for extension deployment
Start-Sleep -Seconds 10
$roleDefinition = Get-AzRoleDefinition -Name "Contributor"
New-AzRoleAssignment `
    -ObjectId $prereqAssignment.Identity.PrincipalId `
    -RoleDefinitionId $roleDefinition.Id `
    -Scope $scope `
    -ErrorAction SilentlyContinue

Write-Host "āœ… Prerequisites deployed. Extensions will install within 30 minutes."

# 2. Deploy Windows security baseline audit
Write-Host "`nšŸ” Deploying Windows security baseline audit..."

$windowsBaseline = Get-AzPolicySetDefinition | Where-Object {
    $_.Properties.DisplayName -like "*Windows machines should meet*Azure compute security baseline*"
}

New-AzPolicyAssignment `
    -Name "audit-windows-baseline" `
    -DisplayName "Audit: Windows Security Baseline" `
    -Scope $scope `
    -PolicySetDefinition $windowsBaseline `
    -Location "uksouth" | Out-Null

# 3. Deploy password security audit
Write-Host "šŸ”‘ Deploying password security audit..."

$passwordPolicy = Get-AzPolicySetDefinition | Where-Object {
    $_.Properties.DisplayName -like "*insecure password*"
}

$passwordParams = @{
    IncludeArcMachines = if ($IncludeArcServers) { "true" } else { "false" }
}

New-AzPolicyAssignment `
    -Name "audit-password-security" `
    -DisplayName "Audit: Insecure Password Settings" `
    -Scope $scope `
    -PolicySetDefinition $passwordPolicy `
    -PolicyParameterObject $passwordParams `
    -Location "uksouth" | Out-Null

# 4. Deploy Linux baseline audit
Write-Host "🐧 Deploying Linux security baseline audit..."

$linuxBaseline = Get-AzPolicySetDefinition | Where-Object {
    $_.Properties.DisplayName -like "*Linux machines should meet*"
}

if ($linuxBaseline) {
    New-AzPolicyAssignment `
        -Name "audit-linux-baseline" `
        -DisplayName "Audit: Linux Security Baseline" `
        -Scope $scope `
        -PolicySetDefinition $linuxBaseline `
        -Location "uksouth" | Out-Null
}

Write-Host "`nāœ… All policies deployed successfully"
Write-Host "ā° First compliance scan: 30-60 minutes"
Write-Host "ā° Ongoing scans: Every 15 minutes"
Write-Host "`nšŸ“Š Run compliance report after 1 hour: .\Get-MachineConfigCompliance.ps1"

2. Compliance Reporting and Remediation Tracking

# Get detailed Machine Configuration compliance report
# Exports non-compliant VMs with specific violations

param(
    [Parameter(Mandatory=$true)]
    [string]$SubscriptionId,
    
    [Parameter(Mandatory=$false)]
    [string]$OutputPath = ".\machine-config-report.csv",
    
    [Parameter(Mandatory=$false)]
    [switch]$OnlyNonCompliant
)

Set-AzContext -SubscriptionId $SubscriptionId

Write-Host "šŸ“Š Gathering Machine Configuration compliance data..."

# Query all Guest Configuration policy states
$policyStates = Get-AzPolicyState -SubscriptionId $SubscriptionId -Filter "PolicyDefinitionCategory eq 'Guest Configuration'"

if ($OnlyNonCompliant) {
    $policyStates = $policyStates | Where-Object { $_.ComplianceState -eq "NonCompliant" }
}

Write-Host "Found $($policyStates.Count) policy state records"

# Enrich with resource details
$complianceReport = $policyStates | ForEach-Object {
    $resourceId = $_.ResourceId
    $resourceName = ($resourceId -split '/')[-1]
    $resourceGroup = ($resourceId -split '/')[4]
    
    [PSCustomObject]@{
        VMName = $resourceName
        ResourceGroup = $resourceGroup
        Location = $_.ResourceLocation
        ComplianceState = $_.ComplianceState
        PolicyName = $_.PolicyDefinitionName
        PolicyDisplayName = $_.PolicyDefinitionDisplayName
        ComplianceReasonCode = $_.ComplianceReasonCode
        LastEvaluated = $_.Timestamp
        ResourceId = $resourceId
    }
}

# Export to CSV
$complianceReport | Export-Csv -Path $OutputPath -NoTypeInformation
Write-Host "āœ… Report exported to: $OutputPath"

# Console summary
Write-Host "`nšŸ“ˆ Compliance Summary:"
$total = ($complianceReport | Select-Object -Unique VMName).Count
$compliant = ($complianceReport | Where-Object ComplianceState -eq 'Compliant' | Select-Object -Unique VMName).Count
$nonCompliant = ($complianceReport | Where-Object ComplianceState -eq 'NonCompliant' | Select-Object -Unique VMName).Count

Write-Host "Total VMs evaluated: $total"
Write-Host "Compliant: $compliant ($([math]::Round($compliant/$total*100,1))%)"
Write-Host "Non-Compliant: $nonCompliant ($([math]::Round($nonCompliant/$total*100,1))%)"

# Top violations
Write-Host "`nšŸ”„ Top 5 Policy Violations:"
$complianceReport | 
    Where-Object ComplianceState -eq "NonCompliant" |
    Group-Object PolicyDisplayName |
    Sort-Object Count -Descending |
    Select-Object -First 5 |
    ForEach-Object {
        Write-Host "  $($_.Count) VMs: $($_.Name)"
    }

# VMs missing extension
Write-Host "`nāš ļø  Checking for VMs without Machine Configuration extension..."
$allVMs = Get-AzVM
$vmsWithExtension = $complianceReport | Select-Object -Unique VMName
$missingExtension = $allVMs | Where-Object { $_.Name -notin $vmsWithExtension.VMName }

if ($missingExtension.Count -gt 0) {
    Write-Host "VMs missing extension: $($missingExtension.Count)"
    $missingExtension | Select-Object Name, ResourceGroupName, Location | Format-Table
} else {
    Write-Host "āœ… All VMs have Machine Configuration extension"
}

3. Azure Resource Graph Queries

// Query 1: All non-compliant VMs with detailed reasons
policyresources
| where type == "microsoft.policyinsights/policystates"
| where properties.policyDefinitionCategory == "Guest Configuration"
| where properties.complianceState == "NonCompliant"
| extend 
    vmName = tostring(split(properties.resourceId, "/")[8]),
    resourceGroup = tostring(split(properties.resourceId, "/")[4]),
    policyName = tostring(properties.policyDefinitionName),
    policyDisplay = tostring(properties.policyDefinitionDisplayName),
    reason = tostring(properties.complianceReasonCode),
    evaluated = todatetime(properties.timestamp)
| project vmName, resourceGroup, policyDisplay, reason, evaluated
| order by evaluated desc

// Query 2: Compliance percentage by policy
policyresources
| where type == "microsoft.policyinsights/policystates"
| where properties.policyDefinitionCategory == "Guest Configuration"
| summarize 
    Total = count(),
    Compliant = countif(properties.complianceState == "Compliant"),
    NonCompliant = countif(properties.complianceState == "NonCompliant")
    by PolicyName = tostring(properties.policyDefinitionDisplayName)
| extend CompliancePercentage = round((Compliant * 100.0) / Total, 1)
| project PolicyName, Total, Compliant, NonCompliant, CompliancePercentage
| order by CompliancePercentage asc

// Query 3: VMs without Machine Configuration extension
resources
| where type == "microsoft.compute/virtualmachines"
| where properties.storageProfile.osDisk.osType in ("Windows", "Linux")
| project vmId = tolower(id), vmName = name, resourceGroup, location, 
    osType = tostring(properties.storageProfile.osDisk.osType)
| join kind=leftouter (
    resources
    | where type == "microsoft.compute/virtualmachines/extensions"
    | where name in ("AzurePolicyforWindows", "AzurePolicyforLinux")
    | project vmId = tolower(substring(id, 0, indexof(id, "/extensions"))), hasExtension = 1
) on vmId
| where isnull(hasExtension)
| project vmName, resourceGroup, osType, Status = "Missing Extension"
| order by vmName asc

// Query 4: Recently failed compliance checks (last 24 hours)
policyresources
| where type == "microsoft.policyinsights/policystates"
| where properties.policyDefinitionCategory == "Guest Configuration"
| where properties.complianceState == "NonCompliant"
| where todatetime(properties.timestamp) > ago(24h)
| extend 
    vmName = tostring(split(properties.resourceId, "/")[8]),
    policyDisplay = tostring(properties.policyDefinitionDisplayName),
    evaluated = todatetime(properties.timestamp)
| summarize 
    FailureCount = count(),
    LatestFailure = max(evaluated)
    by vmName, policyDisplay
| order by FailureCount desc

// Query 5: Compliance trend by resource group
policyresources
| where type == "microsoft.policyinsights/policystates"
| where properties.policyDefinitionCategory == "Guest Configuration"
| extend 
    resourceGroup = tostring(split(properties.resourceId, "/")[4]),
    compliant = iff(properties.complianceState == "Compliant", 1, 0)
| summarize 
    TotalChecks = count(),
    CompliantChecks = sum(compliant)
    by resourceGroup
| extend CompliancePercentage = round((CompliantChecks * 100.0) / TotalChecks, 1)
| project resourceGroup, TotalChecks, CompliancePercentage
| order by CompliancePercentage asc

4. Automated Remediation Script

# Trigger remediation tasks for non-compliant VMs
# Creates remediation tasks for policies with DeployIfNotExists effect

param(
    [Parameter(Mandatory=$true)]
    [string]$SubscriptionId,
    
    [Parameter(Mandatory=$false)]
    [string]$PolicyAssignmentName,
    
    [Parameter(Mandatory=$false)]
    [string]$ResourceGroupName
)

Set-AzContext -SubscriptionId $SubscriptionId

$scope = if ($ResourceGroupName) {
    "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName"
} else {
    "/subscriptions/$SubscriptionId"
}

Write-Host "šŸ”§ Creating remediation tasks for Machine Configuration policies..."

# Get all Machine Configuration policy assignments
$assignments = Get-AzPolicyAssignment -Scope $scope | Where-Object {
    $_.Properties.PolicyDefinitionId -like "*Guest*Configuration*" -or
    $_.Properties.PolicyDefinitionId -like "*Machine*Configuration*"
}

if ($PolicyAssignmentName) {
    $assignments = $assignments | Where-Object { $_.Name -eq $PolicyAssignmentName }
}

Write-Host "Found $($assignments.Count) policy assignments"

foreach ($assignment in $assignments) {
    Write-Host "`nProcessing: $($assignment.Properties.DisplayName)"
    
    # Get non-compliant resources
    $nonCompliant = Get-AzPolicyState -PolicyAssignmentName $assignment.Name -Filter "ComplianceState eq 'NonCompliant'"
    
    if ($nonCompliant.Count -eq 0) {
        Write-Host "  āœ… All resources compliant - no remediation needed"
        continue
    }
    
    Write-Host "  āš ļø  Non-compliant resources: $($nonCompliant.Count)"
    
    # Create remediation task
    try {
        $remediationName = "remediate-$(Get-Date -Format 'yyyyMMddHHmm')"
        $remediation = Start-AzPolicyRemediation `
            -Name $remediationName `
            -PolicyAssignmentId $assignment.PolicyAssignmentId `
            -Scope $scope `
            -AsJob
        
        Write-Host "  šŸš€ Remediation task created: $remediationName"
    } catch {
        Write-Host "  āŒ Error creating remediation: $($_.Exception.Message)"
    }
}

Write-Host "`nāœ… Remediation tasks created. Check status with:"
Write-Host "Get-AzPolicyRemediation -Scope '$scope'"

5. Extension Health Check Script

# Check Machine Configuration extension health across VMs
# Identifies VMs with missing, failed, or outdated extensions

param(
    [Parameter(Mandatory=$true)]
    [string]$SubscriptionId,
    
    [Parameter(Mandatory=$false)]
    [string]$ResourceGroupName
)

Set-AzContext -SubscriptionId $SubscriptionId

Write-Host "šŸ” Checking Machine Configuration extension health..."

$vms = if ($ResourceGroupName) {
    Get-AzVM -ResourceGroupName $ResourceGroupName
} else {
    Get-AzVM
}

Write-Host "Analyzing $($vms.Count) VMs..."

$results = foreach ($vm in $vms) {
    $osType = $vm.StorageProfile.OsDisk.OsType
    $expectedExtension = if ($osType -eq "Windows") { 
        "AzurePolicyforWindows" 
    } else { 
        "AzurePolicyforLinux" 
    }
    
    # Get extension status
    $extension = Get-AzVMExtension `
        -ResourceGroupName $vm.ResourceGroupName `
        -VMName $vm.Name `
        -Name $expectedExtension `
        -ErrorAction SilentlyContinue
    
    $status = if (-not $extension) {
        "Missing"
    } elseif ($extension.ProvisioningState -ne "Succeeded") {
        "Failed: $($extension.ProvisioningState)"
    } elseif ($extension.TypeHandlerVersion -lt "1.29") {
        "Outdated: v$($extension.TypeHandlerVersion)"
    } else {
        "Healthy: v$($extension.TypeHandlerVersion)"
    }
    
    [PSCustomObject]@{
        VMName = $vm.Name
        ResourceGroup = $vm.ResourceGroupName
        OSType = $osType
        ExtensionStatus = $status
        LastModified = $extension.SubStatus[0].Time
    }
}

# Summary
$missing = $results | Where-Object { $_.ExtensionStatus -eq "Missing" }
$failed = $results | Where-Object { $_.ExtensionStatus -like "Failed*" }
$outdated = $results | Where-Object { $_.ExtensionStatus -like "Outdated*" }
$healthy = $results | Where-Object { $_.ExtensionStatus -like "Healthy*" }

Write-Host "`nšŸ“Š Extension Health Summary:"
Write-Host "Healthy: $($healthy.Count)"
Write-Host "Missing: $($missing.Count)"
Write-Host "Failed: $($failed.Count)"
Write-Host "Outdated: $($outdated.Count)"

if ($missing.Count -gt 0) {
    Write-Host "`nāš ļø  VMs Missing Extension:"
    $missing | Select-Object VMName, ResourceGroup, OSType | Format-Table
    Write-Host "Fix: Re-run prerequisite policy assignment or manually install extension"
}

if ($failed.Count -gt 0) {
    Write-Host "`nāŒ VMs with Failed Extension:"
    $failed | Select-Object VMName, ResourceGroup, ExtensionStatus | Format-Table
    Write-Host "Fix: Check VM logs and reinstall extension"
}

if ($outdated.Count -gt 0) {
    Write-Host "`nāš ļø  VMs with Outdated Extension:"
    $outdated | Select-Object VMName, ResourceGroup, ExtensionStatus | Format-Table
    Write-Host "Fix: Extensions auto-update, but you can force update if needed"
}

# Export full results
$outputFile = "machine-config-extension-health.csv"
$results | Export-Csv -Path $outputFile -NoTypeInformation
Write-Host "`nāœ… Full report exported to: $outputFile"

Why It Matters

  • Security posture visibility: Know exactly what’s configured inside your VMs without logging into each one
  • Automated compliance: Auditors get continuous compliance reports instead of point-in-time manual checks
  • Configuration drift detection: Catch when someone manually changes security settings that violate policy
  • No performance overhead: Extension runs checks every 15 minutes with minimal CPU/memory usage
  • Enterprise scale: Works across thousands of VMs in multiple subscriptions and management groups
  • Free for Azure VMs: No additional cost beyond Azure Policy (Arc servers: Ā£6/month)

Try This Week

  1. Deploy prerequisites and audit policies – Run the mass deployment script
  2. Generate compliance report – After 1 hour, run the compliance reporting script
  3. Query via Resource Graph – Use the KQL queries to analyze compliance trends
  4. Check extension health – Verify all VMs have the extension installed correctly
  5. Set up remediation – Create automatic remediation tasks for fixable violations

Common Machine Configuration Mistakes

  • Not granting Contributor role to policy managed identity: Extension deployment fails silently
  • Blocking guest configuration service endpoints: VMs can’t report compliance (allow: *.guestconfiguration.azure.com)
  • Expecting instant results: First scan takes 30-60 minutes, then every 15 minutes
  • Using DeployIfNotExists at Management Group level: Not supported – use AuditIfNotExists instead
  • Not checking extension status: VMs might have failed extension provisioning but policy shows “Not Started”
  • Forgetting Arc servers cost money: Each Arc-enabled server costs Ā£6/month for guest configuration

Advanced Patterns

Custom configuration packages: Create your own guest configuration packages using PowerShell DSC (Windows) or Chef InSpec (Linux) to audit your specific requirements

Automated response: Trigger Azure Automation runbooks when VMs become non-compliant to automatically remediate

Compliance dashboards: Export policy states to Log Analytics and build workbooks showing compliance trends over time

Multi-tenant management: Use Azure Lighthouse to manage guest configuration across customer tenants from a single console

Pro Tip

Start with just the password security audit policy. It catches the most common security misconfigurations (weak passwords, password never expires, etc.) and has the highest violation rate – typically 30-40% of Windows VMs are non-compliant on first scan. This gives you quick wins and demonstrates value before rolling out the full security baseline.