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 |