Azure App Service Environments (ASEs) and AD Integration
Recently I had to look at a case where there was a requirement to communicate with an Active Directory Domain Controller from a Azure Web App. We were looking to use App Service Environments, looking at the documentation published here https://docs.microsoft.com/en-us/azure/app-service-web/web-sites-integrate-with-vnet,it stated:
This caused some confusion as it appeared to suggest you could not communicate with domain controllers but it appears this is actually more in reference to domain joining.
Furthermore, there is a Microsoft blog post on how to load a LDAP module for PHP with an Azure Web App - which indicates that it is a supported scenario.
You can relatively easily verify this by deploying an Azure Web App with VNET integration or in ASE. I used a modified version of the template published here https://github.com/Azure/azure-quickstart-templates/tree/master/201-web-app-ase-create to create a Web App in an ASE.
I then created a domain controller via PowerShell in this Gist:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Select a subscritpion | |
Set-AzureRmContext -SubscriptionName (Get-AzureRmSubscription | Out-GridView -PassThru).SubscriptionName | |
$Location = 'North Europe' | |
# Create a resource group for the VM and storage account | |
$CoreInfraResourceGroupName = 'VM-RG-0001' | |
New-AzureRmResourceGroup -Name $CoreInfraResourceGroupName -Location $Location | |
$CoreInfraVMStorageAccount = '0048vhdstorage' | |
New-AzureRmStorageAccount -ResourceGroupName $CoreInfraResourceGroupName -Name $CoreInfraVMStorageAccount -Type Standard_LRS -Location $Location | |
# Use existing VNET | |
$VNETResourceGroupName = 'VNET-RG-001' | |
$VNETName = 'VNET-001' | |
$AccountName = 'my-azure-admin' | |
$vmpwd = 'ReplaceWithAStrongPassword' | |
$VMSize = 'Standard_A2' | |
# Get Windows Server Image Details | |
$offer = (Get-AzureRmVMImageOffer -Location $Location -PublisherName $publisher | Where-Object Offer -eq 'WindowsServer' ).Offer | |
$sku = (Get-AzureRmVMImageSku -Location $Location -Offer $offer -PublisherName $publisher | Where-Object Skus -eq '2008-R2-SP1').Skus | |
$imageid = (Get-AzureRmVMImage -Location $Location -Offer $offer -PublisherName $publisher -Skus $sku | Sort-Object Version -Descending)[0].Id | |
$version = (Get-AzureRmVMImage -Location $Location -Offer $offer -PublisherName $publisher -Skus $sku | Sort-Object Version -Descending)[0].Version | |
$VMName = 'AZ-DC-001' | |
$ComputerName = $VMName | |
$OSDiskName = $VMName + 'osDisk' | |
$subnetname = 'SN-MGT-01' | |
$InterfaceName = 'nic-' + $VMName; | |
# Get the storage account | |
$StorageAccount = Get-AzureRMStorageAccount -Name $CoreInfraVMStorageAccount -ResourceGroupName $CoreInfraResourceGroupName | |
# Configure vnet, subnet and nic | |
$vnet = Get-AzureRmVirtualNetwork -Name $VNETName -ResourceGroupName $VNETResourceGroupName | |
$subnet = Get-AzureRmVirtualNetworkSubnetConfig -Name $subnetname -VirtualNetwork $vnet | |
$Interface = New-AzureRmNetworkInterface -Name $InterfaceName -ResourceGroupName $CoreInfraResourceGroupName -Location $Location -SubnetId $subnet.Id | |
# Create a credential object | |
$SecurePassword = ConvertTo-SecureString $vmpwd -AsPlainText -Force | |
$Credential = New-Object System.Management.Automation.PSCredential ($AccountName, $SecurePassword); | |
# Set VM configuration | |
$VirtualMachine = New-AzureRmVMConfig -VMName $VMName -VMSize $VMSize | |
$VirtualMachine = Set-AzureRmVMOperatingSystem -VM $VirtualMachine -Windows -ComputerName $ComputerName -Credential $Credential ` | |
-ProvisionVMAgent -EnableAutoUpdate | |
$VirtualMachine = Set-AzureRmVMSourceImage -VM $VirtualMachine -PublisherName $publisher -Offer $offer -Skus $sku -Version 'latest' | |
$VirtualMachine = Add-AzureRmVMNetworkInterface -VM $VirtualMachine -Id $Interface.Id | |
$OSDiskUri = $StorageAccount.PrimaryEndpoints.Blob.ToString() + 'vhds/' + $OSDiskName + '.vhd' | |
$VirtualMachine = Set-AzureRmVMOSDisk -VM $VirtualMachine -Name $OSDiskName -VhdUri $OSDiskUri -CreateOption FromImage | |
## Create the VM | |
New-AzureRmVM -ResourceGroupName $CoreInfraResourceGroupName -Location $Location -VM $VirtualMachine | |
Then I used the PowerShell code in this Gist to install AD related roles and promoted the server to a Domain Controller via an answer file - change the forest/domain functional level and other settings to suit your needs.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$AccountName = 'my-admin' | |
$vmpwd = 'ReplaceWithAStrongPassword' | |
$SecurePassword = ConvertTo-SecureString $vmpwd -AsPlainText -Force | |
$Credential = New-Object System.Management.Automation.PSCredential ($AccountName, $SecurePassword); | |
Import-Module ServerManager | |
Add-WindowsFeature GPMC, Backup-Features, Backup, Backup-Tools,DNS,WINS-Server -Verbose | |
Add-WindowsFeature AS-NET-Framework -Verbose | |
Add-WindowsFeature AD-Domain-Services, ADDS-Domain-Controller -Verbose | |
$Domain = 'acme.local' | |
$NetBiosDomainName = 'acme' | |
$ADSite = 'acme-site1' | |
<# | |
This entry specifies the forest functional level when a new domain is created in a new forest as follows: | |
0 = Windows 2000 Server | |
2 = Windows Server 2003 | |
3 = Windows Server 2008 | |
#> | |
$ForestLevel = 2 | |
<# | |
This entry specifies the domain functional level. This entry is based on the levels that exist in the forest when a new domain is created in an existing forest. Value descriptions are as follows: | |
0 = Windows 2000 Server native mode | |
2 = Windows Server 2003 | |
3 = Windows Server 2008 | |
#> | |
$Domainlevel = 2 | |
$DSSafeModePassword = $vmpwd | |
$DCPromoFile = @" | |
[DCINSTALL] | |
InstallDNS=yes | |
NewDomain=forest | |
NewDomainDNSName=$Domain | |
DomainNetBiosName=$NetBiosDomainName | |
SiteName=$ADSite | |
ReplicaorNewDomain=domain | |
ForestLevel=$ForestLevel | |
DomainLevel=$Domainlevel | |
ConfirmGC=Yes | |
SafeModeAdminPassword=$DSSafeModePassword | |
RebootonCompletion=Yes | |
"@ | |
# Output config file to text file | |
$DCPromoFile | out-file c:\dcpromoanswerfile.txt -Force | |
# Run DCPromo with the correct config | |
& 'C:\Windows\System32\dcpromo.exe' /unattend:c:\dcpromoanswerfile.txt |
At this point you can perform a rudimentary test of AD integration via Kudu/SCM PowerShell console.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$username = 'aduser' | |
$password = 'replaceWithPassword' | |
$DomainControllerIpAddress = '192.168.0.21' | |
$LdapDn = 'dc=acme,dc=local' | |
$dn = New-Object System.DirectoryServices.DirectoryEntry ("LDAP://$($DomainControllerIpAddress):389/$LdapDn",$username,$password) | |
# Here look for a user | |
$ds = new-object System.DirectoryServices.DirectorySearcher($dn) | |
$ds.filter = "((userPrincipalName=*))" | |
$ds.SearchScope = "subtree" | |
$ds.PropertiesToLoad.Add("distinguishedName") | |
$ds.PropertiesToLoad.Add("sAMAccountName") | |
$ds.PropertiesToLoad.Add("lastLogon") | |
$ds.PropertiesToLoad.Add("telephoneNumber") | |
$ds.PropertiesToLoad.Add("memberOf") | |
$ds.PropertiesToLoad.Add("distinguishedname") | |
$ds.PropertiesToLoad.Add("otherHomePhone"); | |
$ds.FindAll() |
If you wish to test using PHP, you will need to download the PHP binaries from http://windows.php.net/download/, and extracted them on my computer, in the ext directory you will find the php_ldap.dll file. Note the version you downloads needs to match the version of PHP you have configured your Web App with, which in my case was 5.6.
Next from Kudu / SCM I created a directory named bin under /site/wwwroot, in that directory. Then using FTPS (I used FileZilla, but you will need to create a deployment account first) to upload the php_ldap.dll file.
Then create a file named ldap-test.php with the following php code:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
$domain = 'acme.local'; | |
$username = 'aduser'; | |
$password = 'replaceWithPassword'; | |
$ldapconfig['host'] = '192.168.21'; | |
$ldapconfig['port'] = 389; | |
$ldapconfig['basedn'] = 'dc=acme,dc=local'; | |
$ldap_dn = "DC=acme,DC=local"; | |
//print_r($ldapconfig); | |
$ds=ldap_connect($ldapconfig['host'], $ldapconfig['port']); | |
ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3); | |
ldap_set_option($ds, LDAP_OPT_REFERRALS, 0); | |
if ($ds) { | |
echo("ldap connect completed\n"); | |
ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3); | |
ldap_set_option($ds, LDAP_OPT_REFERRALS, 0); | |
$bind=ldap_bind($ds, $username.'@'.$domain, $password); | |
if ($bind) { | |
echo "LDAP bind successful...$bind <br />"; | |
$attributes= array( "sn", "givenname", "mail", "samaccountname"); | |
$filter = '(&(objectCategory=person)(samaccountname=*))'; | |
$results = ldap_search($ds, $ldap_dn, $filter, $attributes); | |
if ($retval) { | |
echo("Login correct <br />"); | |
$info = ldap_get_entries($ds, $results); | |
print_r($info); | |
for ($i=0; $i<$entries["count"]; $i++) | |
{ | |
echo $entries[$i]["displayname"] | |
[0]."(".$entries[$i]["l"][0].")<br />"; | |
} | |
} else { | |
echo("Login incorrect <br />".ldap_error($retval)); | |
} | |
}else { | |
echo "ERROR: LDAP bind failed...<br />"; | |
} | |
ldap_unbind($bind); | |
} | |
?> |
Comments
Post a Comment