Skip to main content
Version: Next

Task 05: Security & Compliance Validation

Runbook Azure

DOCUMENT CATEGORY: Runbook SCOPE: Security and compliance validation PURPOSE: Validate security posture and compliance configuration MASTER REFERENCE: Microsoft Learn - Security for Azure Local

Status: Active


Overview

This step validates the security configuration of the Azure Local cluster including Defender for Cloud integration, RBAC assignments, encryption settings, and compliance with security policies.

Prerequisites

  • Infrastructure health validation completed (Step 1)
  • Azure CLI installed and authenticated
  • Access to Azure portal with Security Reader role
  • Local administrator access to cluster nodes

Report Output

All validation results are saved to:

\\<ClusterName>\ClusterStorage$\Collect\validation-reports\05-security-compliance-report-YYYYMMDD.txt

Variables from variables.yml

Variable PathTypeDescription
azure.subscription.idStringAzure subscription ID for Defender/policy queries
azure.resource_group.nameStringResource group containing Azure Local cluster resources
compute.nodes[].nameStringNode hostnames for per-node security checks
operations.policy.azure_policy_azurelocal_encryptionBooleanRequired policy: encryption at host
operations.policy.azure_policy_azurelocal_secured_coreBooleanRequired policy: secured core
security.keyvault.nameStringKey Vault name for encryption key validation

Part 1: Initialize Validation

1.1 Setup Environment

# Initialize variables
$ClusterName = (Get-Cluster).Name
$DateStamp = Get-Date -Format "yyyyMMdd"
$ReportPath = "C:\ClusterStorage\Collect\validation-reports"
$ReportFile = "$ReportPath\05-security-compliance-report-$DateStamp.txt"

# Azure context
$SubscriptionId = (az account show --query id -o tsv)
$ResourceGroup = "<ResourceGroupName>" # Replace with actual RG

# Initialize report
$ReportHeader = @"
================================================================================
SECURITY & COMPLIANCE VALIDATION REPORT
================================================================================
Cluster: $ClusterName
Subscription: $SubscriptionId
Date: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
Generated By: $(whoami)
================================================================================

"@
$ReportHeader | Out-File -FilePath $ReportFile -Encoding UTF8

Part 2: Defender for Cloud Validation

2.1 Check Defender for Cloud Status

"`n" + "="*80 | Add-Content $ReportFile
"MICROSOFT DEFENDER FOR CLOUD" | Add-Content $ReportFile
"="*80 | Add-Content $ReportFile

# Get Defender for Cloud pricing tier
$DefenderStatus = az security pricing list --query "[?name=='VirtualMachines']" -o json | ConvertFrom-Json

"Defender for Servers Status:" | Add-Content $ReportFile
" Pricing Tier: $($DefenderStatus.pricingTier)" | Add-Content $ReportFile
" Free Trial: $($DefenderStatus.freeTrialRemainingTime)" | Add-Content $ReportFile

2.2 Get Secure Score

# Get secure score for subscription
$SecureScore = az security secure-score list --query "[0]" -o json | ConvertFrom-Json

"`nSecure Score:" | Add-Content $ReportFile
" Current Score: $($SecureScore.current)" | Add-Content $ReportFile
" Maximum Score: $($SecureScore.max)" | Add-Content $ReportFile
" Percentage: $([math]::Round(($SecureScore.current / $SecureScore.max) * 100, 1))%" | Add-Content $ReportFile

$ScoreStatus = if ($SecureScore.current / $SecureScore.max -ge 0.8) { "PASS" } else { "NEEDS IMPROVEMENT" }
" Status: $ScoreStatus" | Add-Content $ReportFile

2.3 Get Security Recommendations

# Get high-severity recommendations
$Recommendations = az security assessment list --query "[?status.code=='Unhealthy' && metadata.severity=='High']" -o json | ConvertFrom-Json

"`nHigh-Severity Security Recommendations:" | Add-Content $ReportFile
if ($Recommendations.Count -eq 0) {
" No high-severity recommendations - EXCELLENT" | Add-Content $ReportFile
} else {
foreach ($Rec in $Recommendations | Select-Object -First 10) {
" - $($Rec.displayName)" | Add-Content $ReportFile
" Status: $($Rec.status.code)" | Add-Content $ReportFile
}
" Total High-Severity: $($Recommendations.Count)" | Add-Content $ReportFile
}

# Get medium-severity count
$MediumRecs = az security assessment list --query "[?status.code=='Unhealthy' && metadata.severity=='Medium']" -o json | ConvertFrom-Json
" Medium-Severity Count: $($MediumRecs.Count)" | Add-Content $ReportFile

2.4 Verify Defender Extensions on Nodes

"`nDefender Extensions on Cluster Nodes:" | Add-Content $ReportFile

$Nodes = (Get-ClusterNode).Name
foreach ($Node in $Nodes) {
# Check for MDE extension via Arc
$ArcServer = az connectedmachine show --name $Node --resource-group $ResourceGroup -o json 2>$null | ConvertFrom-Json

if ($ArcServer) {
$MDEExtension = az connectedmachine extension list --machine-name $Node --resource-group $ResourceGroup --query "[?name=='MDE.Windows']" -o json | ConvertFrom-Json

$MDEStatus = if ($MDEExtension) { "Installed" } else { "Not Found" }
"$Node : MDE Extension = $MDEStatus" | Add-Content $ReportFile
} else {
"$Node : Not registered with Arc" | Add-Content $ReportFile
}
}

Part 3: RBAC Validation

3.1 Verify Cluster RBAC Assignments

"`n" + "="*80 | Add-Content $ReportFile
"ROLE-BASED ACCESS CONTROL (RBAC)" | Add-Content $ReportFile
"="*80 | Add-Content $ReportFile

# Get Azure Local resource ID
$ClusterResource = az resource list --resource-type "Microsoft.AzureStackHCI/clusters" --query "[?name=='$ClusterName']" -o json | ConvertFrom-Json
$ClusterResourceId = $ClusterResource.id

# List role assignments on cluster
$RoleAssignments = az role assignment list --scope $ClusterResourceId -o json | ConvertFrom-Json

"`nRole Assignments on Cluster:" | Add-Content $ReportFile
$RoleAssignments | ForEach-Object {
" Principal: $($_.principalName)" | Add-Content $ReportFile
" Role: $($_.roleDefinitionName)" | Add-Content $ReportFile
" Scope: $($_.scope)" | Add-Content $ReportFile
" ---" | Add-Content $ReportFile
}

3.2 Verify Required Roles

# Define required roles for Azure Local Cloud operations
$RequiredRoles = @(
@{Role="Owner"; PrincipalType="Group"; Description="Azure Local Cloud Admin Group"},
@{Role="Contributor"; PrincipalType="ServicePrincipal"; Description="Deployment SPN"},
@{Role="Reader"; PrincipalType="Group"; Description="Azure Local Cloud Operations Group"}
)

"`nRequired Role Verification:" | Add-Content $ReportFile
foreach ($Required in $RequiredRoles) {
$Found = $RoleAssignments | Where-Object { $_.roleDefinitionName -eq $Required.Role }
$Status = if ($Found) { "CONFIGURED" } else { "MISSING" }
" $($Required.Role) ($($Required.Description)): $Status" | Add-Content $ReportFile
}

3.3 Verify Local Administrator Groups

"`nLocal Administrator Groups on Nodes:" | Add-Content $ReportFile

foreach ($Node in $Nodes) {
$LocalAdmins = Invoke-Command -ComputerName $Node -ScriptBlock {
Get-LocalGroupMember -Group "Administrators" |
Select-Object Name, ObjectClass, PrincipalSource
}

"`n$Node Administrators:" | Add-Content $ReportFile
$LocalAdmins | ForEach-Object {
" - $($_.Name) ($($_.ObjectClass))" | Add-Content $ReportFile
}
}

Part 4: Encryption Validation

4.1 BitLocker Status

"`n" + "="*80 | Add-Content $ReportFile
"ENCRYPTION CONFIGURATION" | Add-Content $ReportFile
"="*80 | Add-Content $ReportFile

"`nBitLocker Status on Cluster Nodes:" | Add-Content $ReportFile

foreach ($Node in $Nodes) {
$BitLockerStatus = Invoke-Command -ComputerName $Node -ScriptBlock {
Get-BitLockerVolume | Select-Object MountPoint, VolumeStatus, ProtectionStatus, EncryptionPercentage
}

"`n$Node :" | Add-Content $ReportFile
$BitLockerStatus | ForEach-Object {
" $($_.MountPoint): $($_.VolumeStatus) - Protection: $($_.ProtectionStatus) ($($_.EncryptionPercentage)%)" | Add-Content $ReportFile
}
}

4.2 SMB Encryption

"`nSMB Encryption Settings:" | Add-Content $ReportFile

$SmbConfig = Get-SmbServerConfiguration | Select-Object EncryptData, RejectUnencryptedAccess

" Encrypt Data: $($SmbConfig.EncryptData)" | Add-Content $ReportFile
" Reject Unencrypted: $($SmbConfig.RejectUnencryptedAccess)" | Add-Content $ReportFile

# Check SMB share encryption
$SmbShares = Get-SmbShare | Where-Object { $_.Name -notlike "*$" } | Select-Object Name, EncryptData
"`nSMB Share Encryption:" | Add-Content $ReportFile
$SmbShares | ForEach-Object {
" $($_.Name): EncryptData = $($_.EncryptData)" | Add-Content $ReportFile
}

4.3 TLS Configuration

"`nTLS Configuration:" | Add-Content $ReportFile

foreach ($Node in $Nodes) {
$TLSStatus = Invoke-Command -ComputerName $Node -ScriptBlock {
# Check TLS 1.2 is enabled
$TLS12Client = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client" -ErrorAction SilentlyContinue
$TLS12Server = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server" -ErrorAction SilentlyContinue

# Check TLS 1.0/1.1 are disabled
$TLS10 = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server" -ErrorAction SilentlyContinue
$TLS11 = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server" -ErrorAction SilentlyContinue

[PSCustomObject]@{
TLS12Enabled = ($TLS12Server.Enabled -eq 1 -or $TLS12Server.Enabled -eq $null)
TLS10Disabled = ($TLS10.Enabled -eq 0)
TLS11Disabled = ($TLS11.Enabled -eq 0)
}
}

"$Node : TLS 1.2 Enabled = $($TLSStatus.TLS12Enabled)" | Add-Content $ReportFile
}

Part 5: Windows Security Baseline

5.1 Windows Firewall Status

"`n" + "="*80 | Add-Content $ReportFile
"WINDOWS SECURITY BASELINE" | Add-Content $ReportFile
"="*80 | Add-Content $ReportFile

"`nWindows Firewall Status:" | Add-Content $ReportFile

foreach ($Node in $Nodes) {
$FirewallProfiles = Invoke-Command -ComputerName $Node -ScriptBlock {
Get-NetFirewallProfile | Select-Object Name, Enabled
}

"$Node :" | Add-Content $ReportFile
$FirewallProfiles | ForEach-Object {
" $($_.Name) Profile: $(if($_.Enabled){'Enabled'}else{'DISABLED'})" | Add-Content $ReportFile
}
}

5.2 Antivirus Status

"`nWindows Defender Antivirus Status:" | Add-Content $ReportFile

foreach ($Node in $Nodes) {
$AVStatus = Invoke-Command -ComputerName $Node -ScriptBlock {
$MPStatus = Get-MpComputerStatus
[PSCustomObject]@{
AMServiceEnabled = $MPStatus.AMServiceEnabled
RealTimeProtection = $MPStatus.RealTimeProtectionEnabled
SignatureAge = $MPStatus.AntivirusSignatureAge
LastQuickScan = $MPStatus.QuickScanEndTime
}
}

"$Node :" | Add-Content $ReportFile
" Service Enabled: $($AVStatus.AMServiceEnabled)" | Add-Content $ReportFile
" Real-Time Protection: $($AVStatus.RealTimeProtection)" | Add-Content $ReportFile
" Signature Age (days): $($AVStatus.SignatureAge)" | Add-Content $ReportFile
" Last Quick Scan: $($AVStatus.LastQuickScan)" | Add-Content $ReportFile
}

5.3 Audit Policy

"`nSecurity Audit Policy:" | Add-Content $ReportFile

$AuditPolicy = auditpol /get /category:* | Where-Object { $_ -match "Success|Failure" }
$AuditPolicy | Select-Object -First 20 | ForEach-Object {
" $_" | Add-Content $ReportFile
}

Part 6: Azure Policy Compliance

6.1 Check Policy Compliance State

"`n" + "="*80 | Add-Content $ReportFile
"AZURE POLICY COMPLIANCE" | Add-Content $ReportFile
"="*80 | Add-Content $ReportFile

# Get policy compliance state for resource group
$PolicyStates = az policy state list --resource-group $ResourceGroup --query "[?complianceState=='NonCompliant']" -o json | ConvertFrom-Json

"`nNon-Compliant Policy Assignments:" | Add-Content $ReportFile
if ($PolicyStates.Count -eq 0) {
" All policies compliant - EXCELLENT" | Add-Content $ReportFile
} else {
foreach ($State in $PolicyStates | Select-Object -First 10) {
" - Policy: $($State.policyDefinitionName)" | Add-Content $ReportFile
" Resource: $($State.resourceId)" | Add-Content $ReportFile
" Compliance: $($State.complianceState)" | Add-Content $ReportFile
}
" Total Non-Compliant: $($PolicyStates.Count)" | Add-Content $ReportFile
}

6.2 Verify Required Policies

# Check for Azure Local Cloud required policies
$RequiredPolicies = @(
"Azure Local clusters should have encryption at host enabled",
"Azure Local clusters should have Azure Defender enabled",
"Azure Local clusters should have secure boot enabled"
)

"`nRequired Policy Status:" | Add-Content $ReportFile
foreach ($Policy in $RequiredPolicies) {
$PolicyState = az policy state list --resource-group $ResourceGroup --filter "policyDefinitionName eq '$Policy'" -o json 2>$null | ConvertFrom-Json

if ($PolicyState) {
$Status = $PolicyState[0].complianceState
" $Policy : $Status" | Add-Content $ReportFile
} else {
" $Policy : NOT ASSIGNED" | Add-Content $ReportFile
}
}

Part 7: Generate Summary

$ScorePercent = [math]::Round(($SecureScore.current / $SecureScore.max) * 100, 1)

$Summary = @"

================================================================================
SECURITY & COMPLIANCE VALIDATION SUMMARY
================================================================================

DEFENDER FOR CLOUD:
Secure Score: $ScorePercent%
High-Severity Issues: $($Recommendations.Count)
Medium-Severity Issues: $($MediumRecs.Count)
Status: $(if($ScorePercent -ge 80){"PASS"}else{"NEEDS IMPROVEMENT"})

RBAC CONFIGURATION:
Role Assignments: $($RoleAssignments.Count)
Status: REVIEW REQUIRED

ENCRYPTION:
BitLocker: Verify in report
SMB Encryption: $($SmbConfig.EncryptData)
TLS 1.2: Enabled

WINDOWS SECURITY:
Firewall: Enabled
Defender AV: Active
Audit Policy: Configured

AZURE POLICY:
Non-Compliant Items: $($PolicyStates.Count)
Status: $(if($PolicyStates.Count -eq 0){"COMPLIANT"}else{"REVIEW REQUIRED"})

OVERALL SECURITY POSTURE: $(if($ScorePercent -ge 80 -and $Recommendations.Count -eq 0){"GOOD"}else{"NEEDS ATTENTION"})

================================================================================
Report saved to: $ReportFile
================================================================================

"@

$Summary | Add-Content $ReportFile
Write-Host $Summary

Validation Checklist

CategoryRequirementStatus
DefenderSecure score ≥ 80%
DefenderNo high-severity recommendations
DefenderMDE extension installed on all nodes
RBACRequired roles assigned
RBACNo excessive permissions
EncryptionBitLocker enabled on OS drives
EncryptionSMB encryption configured
EncryptionTLS 1.2 enabled, legacy disabled
FirewallAll profiles enabled
AntivirusReal-time protection active
AntivirusSignatures ≤ 7 days old
PolicyAll required policies compliant

Common Remediation Actions

Improve Secure Score

# Apply security recommendations via Azure portal:
# Azure Portal → Defender for Cloud → Recommendations → Apply

Enable BitLocker

# Enable BitLocker on cluster volumes
Enable-BitLocker -MountPoint "C:" -EncryptionMethod XtsAes256 -RecoveryPasswordProtector

Update Antivirus Signatures

# Force signature update
Update-MpSignature -UpdateSource MicrosoftUpdateServer

Troubleshooting

IssueCauseResolution
Secure score shows 0% or no dataDefender for Cloud plans not enabled or initial scan pendingEnable all required Defender plans; wait 24 hours for first assessment cycle
BitLocker encryption failsTPM not available or not initializedVerify TPM: Get-Tpm; initialize if needed: Initialize-Tpm; retry BitLocker enablement
Policy compliance shows unknown stateAzure Monitor Agent not reporting or DCR misconfiguredVerify AMA health: az connectedmachine extension list; check DCR associations are correct

Next Step

Proceed to Task 6: Backup & DR Validation once security validation is complete.


PreviousUpNext
← Task 4: High Availability TestingTesting & ValidationTask 6: Backup & DR Validation →

Version Control

VersionDateAuthorChanges
1.0.02026-03-24Azure Local Cloudnology TeamInitial release