Public/Test-AadkerbReadiness.ps1
|
function Test-AadkerbReadiness { <# .SYNOPSIS Diagnoses Microsoft Entra Kerberos (AADKERB) authentication readiness for Azure Files. .DESCRIPTION Performs comprehensive checks to identify issues preventing Azure Files SMB mounts using Entra Kerberos authentication with cloud-only identities. Validates: - Storage account AADKERB configuration and SMB security settings - Auto-generated Entra app registration and admin consent - Conditional Access policies that may block the kerberos/1.0 client - Client-side registry, services, Kerberos tickets, and device join state - RBAC role assignments for SMB share access .PARAMETER StorageAccountName The name of the Azure Storage Account configured with AADKERB. .PARAMETER ResourceGroupName The resource group containing the storage account. .PARAMETER SubscriptionId Azure subscription ID. If omitted the current az CLI context is used. .PARAMETER SkipClientChecks Skip client-side checks (useful from a pipeline or jump-box). .PARAMETER SkipConditionalAccessChecks Skip Conditional Access policy checks. .EXAMPLE Test-AadkerbReadiness -StorageAccountName 'mystorageaccount' -ResourceGroupName 'rg-myproject-01' #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$StorageAccountName, [Parameter(Mandatory)] [string]$ResourceGroupName, [Parameter()] [string]$SubscriptionId, [Parameter()] [switch]$SkipClientChecks, [Parameter()] [switch]$SkipConditionalAccessChecks ) Set-StrictMode -Version Latest #region Helpers function Write-ColorInfo { param( [Parameter(Position = 0)] [string]$Message, [System.ConsoleColor]$ForegroundColor ) $params = @{} if ($PSBoundParameters.ContainsKey('ForegroundColor')) { $params['ForegroundColor'] = $ForegroundColor } Write-Host $Message @params } $passCount = 0 $warnCount = 0 $failCount = 0 function Write-Check { param( [ValidateSet('PASS', 'WARN', 'FAIL', 'INFO')] [string]$Status, [string]$Message ) switch ($Status) { 'PASS' { Write-ColorInfo " [PASS] $Message" -ForegroundColor Green; Set-Variable -Name passCount -Value ($passCount + 1) -Scope 1 } 'WARN' { Write-ColorInfo " [WARN] $Message" -ForegroundColor Yellow; Set-Variable -Name warnCount -Value ($warnCount + 1) -Scope 1 } 'FAIL' { Write-ColorInfo " [FAIL] $Message" -ForegroundColor Red; Set-Variable -Name failCount -Value ($failCount + 1) -Scope 1 } 'INFO' { Write-ColorInfo " [INFO] $Message" -ForegroundColor Cyan } } } function Write-Section { param([string]$Title) Write-ColorInfo '' Write-ColorInfo '=====================================================================' -ForegroundColor White Write-ColorInfo " $Title" -ForegroundColor White Write-ColorInfo '=====================================================================' -ForegroundColor White } function Invoke-GraphRequest { param( [string]$Uri, [string]$Method = 'GET' ) try { $response = az rest --method $Method --url $Uri --headers 'Content-Type=application/json' 2>&1 if ($LASTEXITCODE -ne 0) { Write-Check -Status WARN -Message "Graph API call failed: $response" return $null } return ($response | ConvertFrom-Json) } catch { Write-Check -Status WARN -Message "Graph API call exception: $_" return $null } } #endregion Helpers # ── Pre-flight ─────────────────────────────────────────────────────────── Assert-AzCliLogin | Out-Null Write-ColorInfo '' Write-ColorInfo '╔═══════════════════════════════════════════════════════════════════════╗' -ForegroundColor Cyan Write-ColorInfo '║ AADKERB Readiness Diagnostic — Azure Files + Entra Kerberos ║' -ForegroundColor Cyan Write-ColorInfo '╚═══════════════════════════════════════════════════════════════════════╝' -ForegroundColor Cyan Write-ColorInfo '' Write-ColorInfo " Storage Account : $StorageAccountName" Write-ColorInfo " Resource Group : $ResourceGroupName" Write-ColorInfo " Timestamp : $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" #region 1 — Storage Account Configuration Write-Section '1. Storage Account — AADKERB Configuration' $subArgs = if ($SubscriptionId) { @('--subscription', $SubscriptionId) } else { @() } try { $sa = az storage account show --name $StorageAccountName --resource-group $ResourceGroupName @subArgs 2>&1 | ConvertFrom-Json } catch { Write-Check -Status FAIL -Message "Unable to retrieve storage account '$StorageAccountName'. Verify az CLI login and permissions." Write-ColorInfo "`nDiagnostic aborted — cannot continue without storage account access." -ForegroundColor Red return } $directoryType = $sa.azureFilesIdentityBasedAuthentication.directoryServiceOptions if ($directoryType -eq 'AADKERB') { Write-Check -Status PASS -Message "Directory service type is 'AADKERB'." } elseif ($null -eq $directoryType -or $directoryType -eq 'None') { Write-Check -Status FAIL -Message "Directory service type is '$directoryType'. Expected 'AADKERB'." } else { Write-Check -Status WARN -Message "Directory service type is '$directoryType' (AD or AADDS). This script diagnoses AADKERB only." } $adProps = $sa.azureFilesIdentityBasedAuthentication.activeDirectoryProperties if ($directoryType -eq 'AADKERB') { if ($null -eq $adProps -or [string]::IsNullOrWhiteSpace($adProps.domainName) -or [string]::IsNullOrWhiteSpace($adProps.domainGuid)) { Write-Check -Status FAIL -Message "activeDirectoryProperties is not set. The SMB server cannot validate Kerberos tickets without domainName/domainGuid." } else { Write-Check -Status PASS -Message "activeDirectoryProperties set — domainName='$($adProps.domainName)', domainGuid='$($adProps.domainGuid)'" } } $defaultPerm = $sa.azureFilesIdentityBasedAuthentication.defaultSharePermission if ($defaultPerm -and $defaultPerm -ne 'None') { Write-Check -Status PASS -Message "Default share-level permission: $defaultPerm" } else { Write-Check -Status FAIL -Message "Default share-level permission is '$defaultPerm'. Cloud-only identities require a default permission." } $smbVersions = $sa.fileServiceProperties.protocolSettings.smb.versions $smbAuthTypes = $sa.fileServiceProperties.protocolSettings.smb.authenticationMethods $smbKerbEnc = $sa.fileServiceProperties.protocolSettings.smb.kerberosTicketEncryption $smbChanEnc = $sa.fileServiceProperties.protocolSettings.smb.channelEncryption Write-Check -Status INFO -Message "SMB versions : $($smbVersions -join ', ')" Write-Check -Status INFO -Message "SMB authentication types : $($smbAuthTypes -join ', ')" Write-Check -Status INFO -Message "SMB Kerberos encryption : $($smbKerbEnc -join ', ')" Write-Check -Status INFO -Message "SMB channel encryption : $($smbChanEnc -join ', ')" if ($smbAuthTypes -and ($smbAuthTypes -match 'Kerberos')) { Write-Check -Status PASS -Message 'Kerberos is listed as an SMB authentication type.' } else { Write-Check -Status WARN -Message 'Kerberos is not explicitly listed in SMB authentication types.' } #endregion #region 2 — Entra App Registration & Admin Consent Write-Section '2. Entra ID — Storage Account App Registration & Admin Consent' $appDisplayName = "[Storage Account] $StorageAccountName.file.core.windows.net" Write-Check -Status INFO -Message "Looking up app registration: $appDisplayName" $spResult = Invoke-GraphRequest -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=displayName eq '$appDisplayName'&`$select=id,appId,displayName" $storageSpObjectId = $null $storageAppId = $null if ($null -eq $spResult -or $spResult.value.Count -eq 0) { Write-Check -Status FAIL -Message "Service principal '$appDisplayName' not found." } else { $storageSp = $spResult.value[0] $storageSpObjectId = $storageSp.id $storageAppId = $storageSp.appId Write-Check -Status PASS -Message "Service principal found — Object ID: $storageSpObjectId, App ID: $storageAppId" $grantsResult = Invoke-GraphRequest -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$storageSpObjectId/oauth2PermissionGrants?`$select=scope,consentType,resourceId" if ($null -eq $grantsResult -or $grantsResult.value.Count -eq 0) { Write-Check -Status FAIL -Message 'No admin consent grants found. Admin consent for openid profile User.Read is required.' } else { $allScopes = ($grantsResult.value | ForEach-Object { $_.scope }) -join ' ' Write-Check -Status INFO -Message "Granted scopes: $allScopes" foreach ($scope in @('openid', 'profile', 'User.Read')) { if ($allScopes -match [regex]::Escape($scope)) { Write-Check -Status PASS -Message "Required scope '$scope' is granted." } else { Write-Check -Status FAIL -Message "Required scope '$scope' is NOT granted." } } } $appObjResult = Invoke-GraphRequest -Uri "https://graph.microsoft.com/v1.0/applications?`$filter=appId eq '$storageAppId'&`$select=id,tags" if ($null -ne $appObjResult -and $appObjResult.value.Count -gt 0) { $appTags = $appObjResult.value[0].tags if ($appTags -contains 'kdc_enable_cloud_group_sids') { Write-Check -Status PASS -Message "Application manifest tag 'kdc_enable_cloud_group_sids' is present." } else { Write-Check -Status FAIL -Message "Application manifest tag 'kdc_enable_cloud_group_sids' is MISSING." } } else { Write-Check -Status WARN -Message "Unable to look up application registration by appId '$storageAppId'." } } #endregion #region 3 — Conditional Access Policies if (-not $SkipConditionalAccessChecks) { Write-Section '3. Conditional Access — Policies That May Block kerberos/1.0' if (-not $storageAppId) { Write-Check -Status WARN -Message 'Skipping CA checks — storage account app ID could not be determined.' } else { $caResult = Invoke-GraphRequest -Uri 'https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies' if ($null -eq $caResult) { Write-Check -Status WARN -Message 'Unable to read Conditional Access policies.' } else { $enabledPolicies = @($caResult.value | Where-Object { $_.state -eq 'enabled' }) Write-Check -Status INFO -Message "Total enabled CA policies: $($enabledPolicies.Count)" $blockingPolicies = @() foreach ($policy in $enabledPolicies) { $includeApps = $policy.conditions.applications.includeApplications $excludeApps = $policy.conditions.applications.excludeApplications $appInScope = ($includeApps -contains 'All') -or ($includeApps -contains $storageAppId) $appExcluded = ($excludeApps -contains $storageAppId) if (-not $appInScope -or $appExcluded) { continue } $grant = $policy.grantControls $isBlocking = $false $blockReasons = @() if ($grant.builtInControls -contains 'mfa') { $isBlocking = $true; $blockReasons += 'Requires MFA' } if ($grant.authenticationStrength) { $isBlocking = $true; $blockReasons += "Requires authentication strength: $($grant.authenticationStrength.displayName ?? $grant.authenticationStrength.id)" } if ($grant.termsOfUse -and $grant.termsOfUse.Count -gt 0) { $isBlocking = $true; $blockReasons += 'Requires Terms of Use acceptance' } if ($isBlocking) { $blockingPolicies += [PSCustomObject]@{ PolicyId = $policy.id; DisplayName = $policy.displayName; Reasons = ($blockReasons -join '; ') } } } if ($blockingPolicies.Count -eq 0) { Write-Check -Status PASS -Message "No CA policies found that would block AADKERB for app '$storageAppId'." } else { Write-Check -Status FAIL -Message "$($blockingPolicies.Count) CA policy/policies will block AADKERB authentication:" foreach ($bp in $blockingPolicies) { Write-Check -Status FAIL -Message " → [$($bp.PolicyId)] $($bp.DisplayName) — $($bp.Reasons)" } } } } } else { Write-Section '3. Conditional Access — SKIPPED' } #endregion #region 4 — Sign-in Log Analysis Write-Section '4. Entra ID — Recent Sign-in Failures for Storage Account App' if ($storageAppId) { $signInFilter = "appId eq '$storageAppId' and status/errorCode ne 0" $signInUrl = "https://graph.microsoft.com/v1.0/auditLogs/signIns?`$filter=$signInFilter&`$top=10&`$orderby=createdDateTime desc&`$select=createdDateTime,userPrincipalName,appDisplayName,status,conditionalAccessStatus,clientAppUsed" $signInResult = Invoke-GraphRequest -Uri $signInUrl if ($null -eq $signInResult -or $signInResult.value.Count -eq 0) { Write-Check -Status INFO -Message 'No recent sign-in failures found for the storage account app.' } else { Write-Check -Status WARN -Message "$($signInResult.value.Count) recent sign-in failure(s) found:" foreach ($entry in $signInResult.value) { Write-ColorInfo " $($entry.createdDateTime) | $($entry.userPrincipalName) | Error: $($entry.status.errorCode) | $($entry.status.failureReason) | CA: $($entry.conditionalAccessStatus) | Client: $($entry.clientAppUsed)" -ForegroundColor DarkYellow } } } else { Write-Check -Status WARN -Message 'Skipping sign-in log analysis — storage account app ID not determined.' } #endregion #region 5 — Client-side Checks if (-not $SkipClientChecks) { Write-Section '5. Client Device — Configuration & Readiness' $os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue if ($os) { Write-Check -Status INFO -Message "OS: $($os.Caption) (Build $($os.BuildNumber))" if ([int]$os.BuildNumber -ge 22000) { Write-Check -Status PASS -Message "OS build $($os.BuildNumber) is supported for cloud-only AADKERB." } else { Write-Check -Status WARN -Message "OS build $($os.BuildNumber) may not support cloud-only identities." } } $dsregOutput = dsregcmd /status 2>&1 if ($dsregOutput -match 'AzureAdJoined\s*:\s*YES') { Write-Check -Status PASS -Message 'Device is Microsoft Entra joined.' } else { Write-Check -Status FAIL -Message 'Device does not appear to be Entra joined or hybrid joined.' } $regPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\Kerberos\Parameters' $regValue = Get-ItemProperty -Path $regPath -Name 'CloudKerberosTicketRetrievalEnabled' -ErrorAction SilentlyContinue if ($regValue -and $regValue.CloudKerberosTicketRetrievalEnabled -eq 1) { Write-Check -Status PASS -Message 'CloudKerberosTicketRetrievalEnabled = 1 (enabled).' } else { Write-Check -Status FAIL -Message 'CloudKerberosTicketRetrievalEnabled registry value not found or not set to 1.' } foreach ($svc in @(@{Name = 'WinHttpAutoProxySvc'; Display = 'WinHTTP Web Proxy Auto-Discovery'}, @{Name = 'iphlpsvc'; Display = 'IP Helper'})) { $service = Get-Service -Name $svc.Name -ErrorAction SilentlyContinue if ($service -and $service.Status -eq 'Running') { Write-Check -Status PASS -Message "Service '$($svc.Display)' ($($svc.Name)) is running." } else { Write-Check -Status WARN -Message "Service '$($svc.Display)' ($($svc.Name)) is not running." } } $klistString = (klist 2>&1) -join "`n" if ($klistString -match 'krbtgt/KERBEROS\.MICROSOFTONLINE\.COM') { Write-Check -Status PASS -Message 'Entra Kerberos TGT found.' } else { Write-Check -Status WARN -Message 'No Entra Kerberos TGT found.' } $cifsPattern = "cifs/$StorageAccountName\.file\.core\.windows\.net" if ($klistString -match $cifsPattern) { Write-Check -Status PASS -Message "CIFS service ticket found for $StorageAccountName." } else { Write-Check -Status INFO -Message "No CIFS service ticket cached yet for $StorageAccountName." } Write-Check -Status INFO -Message "Testing TCP 445 to $StorageAccountName.file.core.windows.net..." $tcpTest = Test-NetConnection -ComputerName "$StorageAccountName.file.core.windows.net" -Port 445 -WarningAction SilentlyContinue -ErrorAction SilentlyContinue if ($tcpTest.TcpTestSucceeded) { Write-Check -Status PASS -Message "TCP 445 is reachable to $StorageAccountName.file.core.windows.net." } else { Write-Check -Status FAIL -Message 'TCP 445 is NOT reachable.' } } else { Write-Section '5. Client Device — SKIPPED' } #endregion #region 6 — RBAC Role Assignments Write-Section '6. Storage Account — RBAC Role Assignments' try { $roleAssignments = @(az role assignment list ` --scope "/subscriptions/$($sa.id.Split('/')[2])/resourceGroups/$ResourceGroupName/providers/Microsoft.Storage/storageAccounts/$StorageAccountName" ` --query "[?contains(roleDefinitionName, 'Storage File Data SMB Share')]" 2>&1 | ConvertFrom-Json) if ($roleAssignments.Count -eq 0) { Write-Check -Status WARN -Message "No 'Storage File Data SMB Share *' role assignments found." } else { Write-Check -Status PASS -Message "$($roleAssignments.Count) SMB Share role assignment(s) found:" foreach ($ra in $roleAssignments) { Write-Check -Status INFO -Message " → $($ra.roleDefinitionName) — Principal: $($ra.principalName) ($($ra.principalType))" } } } catch { Write-Check -Status WARN -Message "Unable to list RBAC role assignments. Error: $_" } #endregion #region Summary Write-ColorInfo '' Write-ColorInfo '=====================================================================' -ForegroundColor White Write-ColorInfo ' SUMMARY' -ForegroundColor White Write-ColorInfo '=====================================================================' -ForegroundColor White Write-ColorInfo '' Write-ColorInfo " PASS : $passCount" -ForegroundColor Green Write-ColorInfo " WARN : $warnCount" -ForegroundColor Yellow Write-ColorInfo " FAIL : $failCount" -ForegroundColor Red Write-ColorInfo '' if ($failCount -eq 0 -and $warnCount -eq 0) { Write-ColorInfo ' ✅ All checks passed. AADKERB should be fully operational.' -ForegroundColor Green } elseif ($failCount -eq 0) { Write-ColorInfo " ⚠️ No failures but $warnCount warning(s). Review the items above." -ForegroundColor Yellow } else { Write-ColorInfo " ❌ $failCount failure(s) detected. Address the items above before testing Azure Files mount." -ForegroundColor Red } Write-ColorInfo '' #endregion } |