Set up an Active Directory lab (part 2)

The previous post introduced the use case for an Azure-hosted Active Directory lab. Part 1 talked about Azure CLI and Cloud Shell and covered:

  • Creating the basics - resource group, storage account & VNet
  • Creating VM#0 - Bastion Host
  • Creating VM#1 - AD Domain Controller for the forest & domain 'corp.local'
  • Creating VM#2 - AD Domain Controller for the domain 'contoso.internal', part of 'corp.local' forest

This post will cover:

  • Creating VM#3 - Member Server - IIS web server running AuthPage
  • Creating VM#4 - Member Server - Azure AD Connect
  • Creating VM#5 - Windows 10 Enterprise client

Continuity...

If you're picking up from Part 1 on a different day, remember to set the same baseline re-usable variables before continuing on below.

Create VM#3 - App01 - Web Server

  • VM-A-APP01
  • Unmanaged OS disk
  • Windows Server 2019
  • Static IP address
  • Install IIS and deploy AuthPage

Create NSG

$nsg3Name = "jr-trustlab-nsg-a-app"

az network nsg create -n $nsg3Name -g $rgName
az network vnet subnet update -g $rgName -n $subnet2Name --vnet-name $vnetName --network-security-group $nsg3Name
az network nsg rule create --nsg-name $nsg3Name -g $rgName -n "Allow_RDP" --priority 100 --access "allow" --source-address-prefixes $subnet99Range --destination-address-prefixes $subnet2Range --destination-port-ranges "3389" --protocol "TCP" --description "Allow RDP from Bastion"
az network nsg rule create --nsg-name $nsg3Name -g $rgName -n "Allow_HTTP" --priority 110 --access "allow" --source-address-prefixes $subnet1Range $subnet2Range $subnet3Range --destination-address-prefixes $subnet2Range --destination-port-ranges 80 443 --protocol "TCP" --description "Allow HTTP traffic"
az network nsg rule create --nsg-name $nsg3Name -g $rgName -n "Deny_Inbound" --priority 4000 --access "deny" --source-address-prefixes "*" --destination-address-prefixes $subnet2Range --destination-port-ranges "*" --protocol "*" --description "Deny inbound traffic"

$logWsName = "jr-trustlab-logs"
$flow2LogName = "jr-trustlab-flow-a-app"
az network watcher flow-log create -n $flow2LogName -g $rgName --enabled true --nsg $nsg3Name --storage-account $storageName --location $region --format JSON --log-version 2 --retention 7 --traffic-analytics --workspace $logWsName

Create VM

$vm3Image = "Win2019Datacenter"
$vm3User = "lcladmin"
$vm3Pass = $vmPass
$vm3Size = "Standard_B2ms"
$vm3DiskGuid = [guid]::NewGuid().ToString().Replace("-","").Substring(0,10)

az vm create -n $vm3Name -g $rgName --image $vm3Image --admin-username $vm3User --admin-password $vm3Pass --computer-name $vm3Name --size $vm3Size --vnet-name $vnetName --subnet $subnet2Name --private-ip-address $vm3IPAddress --storage-account $storageName --use-unmanaged-disk --os-disk-name "$($vm3Name)-OSdisk-$($vm3DiskGuid)" --nsg '""' --public-ip-address '""'

Install IIS

  • Set static IP address
  • Join domain 'contoso.internal'
$scriptAAPP01_1 = @"
Set-TimeZone -id 'E. Australia Standard Time'
`$IP = ""$vm3IPAddress""
`$MaskBits = 24
`$Gateway = ""$($vm3IPAddress -split "\d{1,3}$" -join "1")""
`$DNS = ""$vm2IPAddress""
`$IPType = ""IPv4""
`$adapter = Get-NetAdapter | ? {`$_.Status -eq ""up""}
`$interface = `$adapter | Get-NetIPInterface -AddressFamily `$IPType
If (`$interface.Dhcp -eq ""Enabled"") {
  Get-NetAdapterBinding -ComponentID ms_tcpip6 | Disable-NetAdapterBinding
  If ((`$adapter | Get-NetIPConfiguration).IPv4Address.IPAddress) {
    `$adapter | Remove-NetIPAddress -AddressFamily `$IPType -Confirm:`$false
  }
  If ((`$adapter | Get-NetIPConfiguration).Ipv4DefaultGateway) {
    `$adapter | Remove-NetRoute -AddressFamily `$IPType -Confirm:`$false
  }
  `$adapter | New-NetIPAddress -AddressFamily `$IPType -IPAddress `$IP -PrefixLength `$MaskBits -DefaultGateway `$Gateway
  `$adapter | Set-DnsClientServerAddress -ServerAddresses `$DNS
}
Start-Sleep -Seconds 15 # Wait for DNS changes

Add-Computer -DomainName ""$dcA2Domain"" -Credential (New-Object System.Management.Automation.PSCredential(""$dcA2Domain\$vm3User"",(ConvertTo-SecureString '$vm3Pass' -AsPlainText -Force))) -OUPath ""OU=S_SERVERS,$dcA2DomainDN"" -Restart

"@

az vm run-command invoke --command-id RunPowerShellScript --name $vm3Name -g $rgName --scripts $scriptAAPP01_1
  • Install IIS
  • Deploy AuthPage
  • Configure IIS for Windows Authentication
$scriptAAPP01_2 = @"
`$installPath = ""c:\install""
If ((Test-Path ""c:\inetpub\wwwroot"") -eq `$false) {
  `$IISFeatures = ""Web-WebServer"",""Web-Common-Http"",""Web-Default-Doc"",""Web-Http-Errors"",""Web-Http-Redirect"",""Web-Health"",""Web-Http-Logging"",""Web-Security"",""Web-Filtering"",""Web-Basic-Auth"",""Web-Client-Auth"",""Web-IP-Security"",""Web-Windows-Auth"",""Web-Net-Ext"",""Web-Net-Ext45"",""Web-Asp-Net"",""Web-Asp-Net45"",""Web-ISAPI-Ext"",""Web-ISAPI-Filter"",""Web-Mgmt-Tools"",""Web-Mgmt-Console""
  Install-WindowsFeature -Name `$IISFeatures -ErrorAction SilentlyContinue
} 

If ((Test-Path ""`$installPath\authpage\authpage\default.aspx"") -eq `$false) {
  mkdir `$installPath -Force
  Invoke-WebRequest -Uri ""https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Components.PostAttachments/00/10/38/31/92/Authpage.zip"" -OutFile ""`$installPath\authpage.zip""
  Expand-Archive -LiteralPath ""`$installPath\authpage.zip"" -DestinationPath ""`$installPath\authpage""
}

If (Test-Path ""`$installPath\authpage\authpage\default.aspx"") {
  If (Test-Path ""c:\inetpub\wwwroot\iisstart.htm"") {
    Get-ChildItem -Path ""`$installPath\authpage\authpage"" -Recurse | Copy-Item -Destination ""c:\inetpub\wwwroot""
    Remove-Item ""c:\inetpub\wwwroot\iisstart.htm"" -ErrorAction SilentlyContinue
  }
} Else {
  Write-Output ""Extract of AuthPage failed""
}
If ((Test-Path ""c:\install\installer.flag"") -eq `$false) {
  Set-WebConfigurationProperty -Filter ""/system.webServer/security/authentication/anonymousAuthentication"" -Name Enabled -Value False -PSPath IIS:\ -location ""Default Web Site""
  Set-WebConfigurationProperty -Filter ""/system.webServer/security/authentication/windowsAuthentication"" -Name Enabled -Value True -PSPath IIS:\ -location ""Default Web Site""
  & `$ENV:windir\system32\inetsrv\appcmd.exe set AppPool DefaultAppPool -""processModel.identityType:NetworkService""
  `$Acl = (Get-Item ""c:\inetpub\wwwroot"").GetAccessControl(""Access"")
  `$Ar = New-Object System.Security.AccessControl.FileSystemAccessRule(""NETWORK SERVICE"", ""ReadAndExecute"", ""ContainerInherit,ObjectInherit"", ""None"", ""Allow"")
  `$Acl.SetAccessRule(`$Ar)
  Set-Acl ""c:\inetpub\wwwroot"" `$Acl
  ""Install complete"" | Out-File ""c:\install\installer.flag""
} Else { Write-Output ""Install flag found - skipping setup tasks"" }
"@

az vm run-command invoke --command-id RunPowerShellScript --name $vm3Name -g $rgName --scripts $scriptAAPP01_2

Create VM#4 - App02 - Azure AD Connect

  • VM-A-APP02
  • Unmanaged OS disk
  • Windows Server 2019
  • Static IP address
  • Download Azure AD Connect install
  • 💔 Cannot perform an unattended install of AADC

Re-use NSG

Nothing to do. This VM lives in the same subnet as VM-A-APP01 and the NSG has already been created & configured.

Create VM

$vm4Image = "Win2019Datacenter"
$vm4User = "lcladmin"
$vm4Pass = $vmPass
$vm4Size = "Standard_B2ms"
$vm4DiskGuid = [guid]::NewGuid().ToString().Replace("-","").Substring(0,10)

az vm create -n $vm4Name -g $rgName --image $vm4Image --admin-username $vm4User --admin-password $vm4Pass --computer-name $vm4Name --size $vm4Size --vnet-name $vnetName --subnet $subnet2Name --private-ip-address $vm4IPAddress --storage-account $storageName --use-unmanaged-disk --os-disk-name "$($vm4Name)-OSdisk-$($vm4DiskGuid)" --nsg '""' --public-ip-address '""'

Install Azure AD Connect

  • Set static IP address
  • Join domain 'contoso.internal'
$scriptAAPP02_1 = @"
Set-TimeZone -id 'E. Australia Standard Time'
`$IP = ""$vm4IPAddress""
`$MaskBits = 24
`$Gateway = ""$($vm4IPAddress -split "\d{1,3}$" -join "1")""
`$DNS = ""$vm2IPAddress""
`$IPType = ""IPv4""
`$adapter = Get-NetAdapter | ? {`$_.Status -eq ""up""}
`$interface = `$adapter | Get-NetIPInterface -AddressFamily `$IPType
If (`$interface.Dhcp -eq ""Enabled"") {
  Get-NetAdapterBinding -ComponentID ms_tcpip6 | Disable-NetAdapterBinding
  If ((`$adapter | Get-NetIPConfiguration).IPv4Address.IPAddress) {
    `$adapter | Remove-NetIPAddress -AddressFamily `$IPType -Confirm:`$false
  }
  If ((`$adapter | Get-NetIPConfiguration).Ipv4DefaultGateway) {
    `$adapter | Remove-NetRoute -AddressFamily `$IPType -Confirm:`$false
  }
  `$adapter | New-NetIPAddress -AddressFamily `$IPType -IPAddress `$IP -PrefixLength `$MaskBits -DefaultGateway `$Gateway
  `$adapter | Set-DnsClientServerAddress -ServerAddresses `$DNS
}
Start-Sleep -Seconds 15 # Wait for DNS changes

Add-Computer -DomainName ""$dcA2Domain"" -Credential (New-Object System.Management.Automation.PSCredential(""$dcA2Domain\$vm4User"",(ConvertTo-SecureString '$vm4Pass' -AsPlainText -Force))) -OUPath ""OU=S_SERVERS,$dcA2DomainDN"" -Restart

"@

az vm run-command invoke --command-id RunPowerShellScript --name $vm4Name -g $rgName --scripts $scriptAAPP02_1
  • Install RSAT
  • Download AAD Connect installer
  • Set permissions for AAD Connect service account
$scriptAAPP02_2 = @"
Install-WindowsFeature rsat-adds -IncludeAllSubFeature
try{
  Import-Module ActiveDirectory -ErrorAction Stop
}catch{
  throw ""Module ActiveDirectory not installed""
}
`$installPath = ""c:\install""
If ((Test-Path `$installPath) -eq `$false) {mkdir `$installPath -Force}

If ((Test-Path ""`$installPath\AzureADConnect.msi"") -eq `$false) {
  try {
    `$URi = ""https://www.microsoft.com/en-us/download/confirmation.aspx?id=47594""
    `$downloadPage = Invoke-WebRequest -Uri `$URi  -usebasicparsing
    `$fileUri = (`$downloadPage.RawContent.Split('""') -like ""https://*AzureADConnect.msi"")[0]
    Invoke-WebRequest -Uri `$fileUri -OutFile ""`$installPath\AzureADConnect.msi""
    Write-Output (""File {0} size: {1}"" -f (gci ""`$installPath\AzureADConnect.msi"").Name, ((gci ""`$installPath\AzureADConnect.msi"").Length / 1MB).ToString("".""))
  } catch {
    Write-Host `$(`$_.Exception.Message)
    throw 'ERROR: Could not download file'
  }
}

If ((Test-Path ""`$env:ProgramFiles\Microsoft Azure Active Directory Connect\AzureADConnect.exe"") -eq `$false) {
  & ""`$ENV:windir\system32\msiexec.exe"" /i ""`$installPath\AzureADConnect.msi"" /qb-
}

try {
  `$URi = ""https://www.powershellgallery.com/api/v2/package/AADConnectPermissions/7.3""
  Invoke-WebRequest -Uri `$URi  -OutFile ""`$installPath\AADConnectPermissions.zip""
  If ((Test-Path ""`$installPath\AADCP\AADConnectPermissions.ps1"") -eq `$false) { Expand-Archive -Path ""`$installPath\AADConnectPermissions.zip"" -DestinationPath ""`$installPath\AADCP"" }
  & ""`$installPath\AADCP\AADConnectPermissions.ps1"" -User svc-azuresync -PasswordHashSync
  & ""`$installPath\AADCP\AADConnectPermissions.ps1"" -User svc-azuresync -msDsConsistencyGuid -ExchangeHybridWriteBackOUs ""OU=Staff,OU=S_USERS,$dcA2DomainDN""
  & ""`$installPath\AADCP\AADConnectPermissions.ps1"" -User svc-azuresync -PasswordWriteBack -ExchangeHybridWriteBackOUs ""OU=Staff,OU=S_USERS,$dcA2DomainDN""
} catch {
  Write-Host `$(`$_.Exception.Message)
  throw 'ERROR: Could not download file'
}

"@
az vm run-command invoke --command-id RunPowerShellScript --name $vm4Name -g $rgName --scripts $scriptAAPP02_2
  • AAD Connect needs to be manually installed and configured

Create VM#5 - Windows 10 Client

  • VM-A-CLIENT01
  • Unmanaged OS disk
  • Windows 10 Enterprise (licensed under a trial M365 environment with E5 licensing)

Create NSG

$nsg4Name = "jr-trustlab-nsg-a-client"

az network nsg create -n $nsg4Name -g $rgName
az network vnet subnet update -g $rgName -n $subnet3Name --vnet-name $vnetName --network-security-group $nsg4Name
az network nsg rule create --nsg-name $nsg4Name -g $rgName -n "Allow_RDP" --priority 100 --access "allow" --source-address-prefixes $subnet99Range --destination-address-prefixes $subnet3Range --destination-port-ranges "3389" --protocol "TCP" --description "Allow RDP from Bastion"
az network nsg rule create --nsg-name $nsg4Name -g $rgName -n "Deny_Inbound" --priority 4000 --access "deny" --source-address-prefixes "*" --destination-address-prefixes $subnet3Range --destination-port-ranges "*" --protocol "*" --description "Deny inbound traffic"

Create VM

$vm5Image = "MicrosoftWindowsDesktop:Windows-10:20h2-ent:19042.867.2103051748"
$vm5User = "lcladmin"
$vm5Pass = $vmPass
$vm5Size = "Standard_B2ms"
$vm5DiskGuid = [guid]::NewGuid().ToString().Replace("-","").Substring(0,10)

az vm create -n $vm5Name -g $rgName --image $vm5Image --admin-username $vm5User --admin-password $vm5Pass --computer-name $vm5Name --size $vm5Size --vnet-name $vnetName --subnet $subnet3Name --private-ip-address $vm5IPAddress --storage-account $storageName --use-unmanaged-disk --os-disk-name "$($vm5Name)-OSdisk-$($vm5DiskGuid)" --nsg '""' --public-ip-address '""'
  • Set static IP and DNS server IP to VM-A-DC02
  • Join domain
  • Add "Grp_AllStaff" AD group to local "Remote Desktop Users" group to allow test user accounts access to RDP
$scriptACLIENT01_1 = @"
Set-TimeZone -id 'E. Australia Standard Time'
`$IP = ""$vm5IPAddress""
`$MaskBits = 24
`$Gateway = ""$($vm5IPAddress -split "\d{1,3}$" -join "1")""
`$DNS = ""$vm2IPAddress""
`$IPType = ""IPv4""
`$adapter = Get-NetAdapter | ? {`$_.Status -eq ""up""}
`$interface = `$adapter | Get-NetIPInterface -AddressFamily `$IPType
If (`$interface.Dhcp -eq ""Enabled"") {
  Get-NetAdapterBinding -ComponentID ms_tcpip6 | Disable-NetAdapterBinding
  If ((`$adapter | Get-NetIPConfiguration).IPv4Address.IPAddress) {
    `$adapter | Remove-NetIPAddress -AddressFamily `$IPType -Confirm:`$false
  }
  If ((`$adapter | Get-NetIPConfiguration).Ipv4DefaultGateway) {
    `$adapter | Remove-NetRoute -AddressFamily `$IPType -Confirm:`$false
  }
  `$adapter | New-NetIPAddress -AddressFamily `$IPType -IPAddress `$IP -PrefixLength `$MaskBits -DefaultGateway `$Gateway
  `$adapter | Set-DnsClientServerAddress -ServerAddresses `$DNS
}
Start-Sleep -Seconds 15 # Wait for DNS changes

Add-Computer -DomainName ""$dcA2Domain"" -Credential (New-Object System.Management.Automation.PSCredential(""$dcA2Domain\$vm5User"",(ConvertTo-SecureString '$vm5Pass' -AsPlainText -Force))) -OUPath ""OU=S_WORKSTATIONS,$dcA2DomainDN"" -Restart
"@

az vm run-command invoke --command-id RunPowerShellScript --name $vm5Name -g $rgName --scripts $scriptACLIENT01_1
$scriptACLIENT01_2 = @"
Add-LocalGroupMember -Group ""Remote Desktop Users"" -Member ""$dcA2Domain\Grp_AllStaff""
"@

az vm run-command invoke --command-id RunPowerShellScript --name $vm5Name -g $rgName --scripts $scriptACLIENT01_2

Wrap up

Part 1 set the foundations. Part 2 covered the creation of the two application servers and the Win10 client. Part 3 will describe the steps to create resources for Domain B.

Appendix - More about IIS and Windows Authentication

While we're all here - here's a few references for Keberos authentication and IIS.

Title Location
Setting up kerberos authentication for a website in IIS https://techcommunity.microsoft.com/t5/iis-support-blog/setting-up-kerberos-authentication-for-a-website-in-iis/ba-p/347882
Windows authentication HTTP request flow in IIS https://techcommunity.microsoft.com/t5/iis-support-blog/windows-authentication-http-request-flow-in-iis/ba-p/324645
Kerberos configuration manager for IIS https://docs.microsoft.com/en-us/archive/blogs/surajdixit/kerberos-configuration-manager-for-internet-information-services-server
Explaining integrated windows authentication in SharePoint https://docs.microsoft.com/en-us/archive/blogs/vivek/part1-explaining-integrated-windows-authentication-in-sharepoint-and-how-ntlm-fails-but-kerberos-works-in-double-hop-authentication
ASP.NET authentication test page https://docs.microsoft.com/en-us/archive/blogs/friis/asp-net-authentication-test-page