FSLogix Profile Container with Azure Files and AzureAD

I have already written a post about how to deploy AVD with AzureAD joined VMs
(https://tech-guys.blog/2021/11/10/azure-virtual-desktop-and-azuread-joined-vm/)

Addition to AzureAD joined VMs we need an Azure Storage Account, an Azure File share as well to save our FSLogix Profiles. This post will guide you to all steps.

At time of writing AzureFiles and AzureAD Kerberos functionality is still in preview (https://docs.microsoft.com/en-us/azure/virtual-desktop/create-profile-container-azure-ad)

Requirements:

  • User must be a hybrid user identities!!!!!!
  1. you must have an Azure Subscription and ADConnect already installed and configured
  2. create a storage account
    I used the following settings and leaved all other settings by default…

3. create azure file share

for my lab I only use SKU standard but for prod environments I always use and recommend premium storage. Microsoft created insights on sizing and designing fslogix solutions for enterprises here: https://docs.microsoft.com/en-us/azure/architecture/example-scenario/wvd/windows-virtual-desktop-fslogix

4. enable Azure AD authorization

Connect-AzAccount -Tenant $tenantId -SubscriptionId $subscriptionId
$Uri = ('https://management.azure.com/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Storage/storageAccounts/{2}?api-version=2021-04-01' -f $subscriptionId, $resourceGroupName, $storageAccountName);
$json = @{properties=@{azureFilesIdentityBasedAuthentication=@{directoryServiceOptions="AADKERB"}}};
$json = $json | ConvertTo-Json -Depth 99
$token = $(Get-AzAccessToken).Token
$headers = @{ Authorization="Bearer $token" }
try {
Invoke-RestMethod -Uri $Uri -ContentType 'application/json' -Method PATCH -Headers $Headers -Body $json;
} catch {
Write-Host $_.Exception.ToString()
Write-Error -Message "Caught exception setting Storage Account directoryServiceOptions=AADKERB: $_" -ErrorAction Stop
}
New-AzStorageAccountKey -ResourceGroupName $resourceGroupName -Name $storageAccountName -KeyName kerb1 -ErrorAction Stop

5. create an Azure AD application that represent the storage account (during preview only via Powershell)

Install-Module -Name Az.StorageInstall-Module -Name AzureAD
$tenantid = "yourid"
$subscriptionid = "yourid"
$resourceGroupName = "rg"
$storageAccountName = "name"
#set password between storage account and azuread
$kerbKey1 = Get-AzStorageAccountKey -ResourceGroupName $resourceGroupName -Name $storageAccountName -ListKerbKey | Where-Object { $_.KeyName -like "kerb1" }
$aadPasswordBuffer = [System.Linq.Enumerable]::Take([System.Convert]::FromBase64String($kerbKey1.Value), 32);
$password = "kk:" + [System.Convert]::ToBase64String($aadPasswordBuffer);
#connect to tennant

Connect-AzureAD
Set-AzContext -Subscription $subscriptionid
$azureAdTenantDetail = Get-AzureADTenantDetail;
$azureAdTenantId = $azureAdTenantDetail.ObjectId
$azureAdPrimaryDomain = ($azureAdTenantDetail.VerifiedDomains | Where-Object {$_._Default -eq $true}).Name

#generate service principal names

$servicePrincipalNames = New-Object string[] 3
$servicePrincipalNames[0] = 'HTTP/{0}.file.core.windows.net' -f $storageAccountName
$servicePrincipalNames[1] = 'CIFS/{0}.file.core.windows.net' -f $storageAccountName
$servicePrincipalNames[2] = 'HOST/{0}.file.core.windows.net' -f $storageAccountName

#create app

$application = New-AzureADApplication -DisplayName $storageAccountName -IdentifierUris $servicePrincipalNames -GroupMembershipClaims "All";

#create service

$servicePrincipal = New-AzureADServicePrincipal -AccountEnabled $true -AppId $application.AppId -ServicePrincipalType "Application";

$Token = ([Microsoft.Open.Azure.AD.CommonLibrary.AzureSession]::AccessTokens['AccessToken']).AccessToken
$Uri = ('https://graph.windows.net/{0}/{1}/{2}?api-version=1.6' -f $azureAdPrimaryDomain, 'servicePrincipals', $servicePrincipal.ObjectId)
$json = @'
{
"passwordCredentials": [
{
"customKeyIdentifier": null,
"endDate": "",
"value": "",
"startDate": ""
}]
}
'@
$now = [DateTime]::UtcNow
$json = $json -replace "", $now.AddDays(-1).ToString("s")
$json = $json -replace "", $now.AddMonths(6).ToString("s")
$json = $json -replace "", $password
$Headers = @{'authorization' = "Bearer $($Token)"}
try {
Invoke-RestMethod -Uri $Uri -ContentType 'application/json' -Method Patch -Headers $Headers -Body $json
Write-Host "Success: Password is set for $storageAccountName"
} catch {
Write-Host $_.Exception.ToString()
Write-Host "StatusCode: " $_.Exception.Response.StatusCode.value
Write-Host "StatusDescription: " $_.Exception.Response.StatusDescription
}

Result is an app with the name of our storage account:

and you can see on your storage account a hint that kerberos is enabled.

6. API Permissions

now you can assign API permissions to that app:

1st we need openid permissions

  • openid – Sign user in
  • profile – View users basic profile

2nd we need User permissions

  • User.Read – Sign in and read user profile

after adding it is important to click “Grant admin consent”. For this you need right permissions within AzureAD.

7. Assign SMB Permissions

To allow an User to create profile container we need to assign RBAC Roles to the Storage account.

Storage File Data SMB Share Contributor for all AVD Users and “Elevated Contributor” to a Group or Users that need Admin Permission.

8. Assign directory level access permissions

For me it was quiet confusing where I have to set these permisions and which system I have to use. In my Lab I have one Windows VM with AD DS and ADConnect installed that I used to run that scripts.

From MS Documentation I found:

The system you use to configure the permissions must meet the following requirements:

  • The version of Windows meets the supported OS requirements defined in the Prerequisites section.
  • Is Azure AD-joined or Hybrid Azure AD-joined to the same Azure AD tenant as the storage account.
  • Has line-of-sight to the domain controller.
  • Is domain-joined to your Active Directory (Windows Explorer method only).
Connect-AzAccount -Tenant $tenantId -SubscriptionId $subscriptionId
$AdModule = Get-Module ActiveDirectory;
if ($null -eq $AdModule) {
Write-Error "Please install and/or import the ActiveDirectory PowerShell module." -ErrorAction Stop;
}
$domainInformation = Get-ADDomain
$Domain = $domainInformation.DnsRoot
$domainGuid = $domainInformation.ObjectGUID.ToString()
$domainName = $domainInformation.DnsRoot
$domainSid = $domainInformation.DomainSID.Value
$forestName = $domainInformation.Forest
$netBiosDomainName = $domainInformation.DnsRoot
$azureStorageSid = $domainSid + "-123454321";
Write-Verbose "Setting AD properties on $storageAccountName in $resourceGroupName : EnableActiveDirectoryDomainServicesForFile=$true, ActiveDirectoryDomainName=$domainName,
ActiveDirectoryNetBiosDomainName=$netBiosDomainName, ActiveDirectoryForestName=$($domainInformation.Forest) ActiveDirectoryDomainGuid=$domainGuid, ActiveDirectoryDomainSid=$domainSid,
ActiveDirectoryAzureStorageSid=$azureStorageSid"
$Uri = ('https://management.azure.com/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Storage/storageAccounts/{2}?api-version=2021-04-01' -f $subscriptionId, $resourceGroupName, $storageAccountName);
$json=
@{
properties=
@{azureFilesIdentityBasedAuthentication=
@{directoryServiceOptions="AADKERB";
activeDirectoryProperties=@{domainName="$($domainName)";
netBiosDomainName="$($netBiosDomainName)";
forestName="$($forestName)";
domainGuid="$($domainGuid)";
domainSid="$($domainSid)";
azureStorageSid="$($azureStorageSid)"}
}
}
};
$json = $json | ConvertTo-Json -Depth 99
$token = $(Get-AzAccessToken).Token
$headers = @{ Authorization="Bearer $token" }
try {
Invoke-RestMethod -Uri $Uri -ContentType 'application/json' -Method PATCH -Headers $Headers -Body $json
} catch {
Write-Host $_.Exception.ToString()
Write-Host "Error setting Storage Account AD properties. StatusCode:" $_.Exception.Response.StatusCode.value__
Write-Host "Error setting Storage Account AD properties. StatusDescription:" $_.Exception.Response.StatusDescription
Write-Error -Message "Caught exception setting Storage Account AD properties: $_" -ErrorAction Stop
}

9. Enable AzureAD functionality via Registry

reg add HKLM\SYSTEM\CurrentControlSet\Control\Lsa\Kerberos\Parameters /v CloudKerberosTicketRetrievalEnabled /t REG_DWORD /d 1

10. Configure SessionHost

On my Sessionhost I configured FSLogix like this:

Because I used that host already with my test account I already had a local profile. Within my first test it was not working, but with allowing FSLogix to delete lcoal profile than I was able to login and my profile container was created.

To be honest I had some trouble to get this working. Maybe it was one of my reboot to solve this 😉

Thanks for reading