Complex Azure Template Odyssey Part Three: ADFS Server

Part One of this series covered the project itself and the overall template structure. Part Two went through how I deploy the Domain Controller in depth. This post will focus on the next server in the chain: The ADFS server that is required to enable authentication in the application which will eventually be installed on this environment.

The Template

The nested deployment template for the ADFS server differs little from my DC template. If anything, it’s even simpler because we don’t have to reconfigure the virtual network after deploying the VM. The screenshot below shots the JSON outline for the template.

adfs template json

You can see that it follows the same pattern as the DC template in part two. I have a VM, a NIC that it depends on and which is attached to our virtual network, and I have VM extensions within the VM itself to enable diagnostics, push a DSC configuration to the VM and execute a custom PowerShell script.

I went through the template construction in detail with the DC, so here I’ll simply show the resources code for you. The VM uses the same Windows Server base image as the DC but doesn’t need the extra disk that we attached to the DC.

"resources": [   {     "apiVersion": "2015-05-01-preview",     "dependsOn": [     ],     "location": "[parameters('resourceLocation')]",     "name": "[variables('vmADFSNicName')]",     "properties": {       "ipConfigurations": [         {           "name": "ipconfig1",           "properties": {             "privateIPAllocationMethod": "Static",             "privateIPAddress": "[variables('vmADFSIPAddress')]",             "subnet": {               "id": "[variables('vmADFSSubnetRef')]"             }           }         }       ]     },     "tags": {       "displayName": "vmADFSNic"     },     "type": "Microsoft.Network/networkInterfaces"   },   {     "name": "[variables('vmADFSName')]",     "type": "Microsoft.Compute/virtualMachines",     "location": "[parameters('resourceLocation')]",     "apiVersion": "2015-05-01-preview",     "dependsOn": [       "[concat('Microsoft.Network/networkInterfaces/', variables('vmADFSNicName'))]",     ],     "tags": {       "displayName": "vmADFS"     },     "properties": {       "hardwareProfile": {         "vmSize": "[variables('vmADFSVmSize')]"       },       "osProfile": {         "computername": "[variables('vmADFSName')]",         "adminUsername": "[parameters('adminUsername')]",         "adminPassword": "[parameters('adminPassword')]"       },       "storageProfile": {         "imageReference": {           "publisher": "[variables('windowsImagePublisher')]",           "offer": "[variables('windowsImageOffer')]",           "sku": "[variables('windowsImageSKU')]",           "version": "latest"         },         "osDisk": {           "name": "[concat(variables('vmADFSName'), '-os-disk')]",           "vhd": {             "uri": "[concat('http://', variables('storageAccountName'), '.blob.core.windows.net/', variables('vmStorageAccountContainerName'), '/', variables('vmADFSName'), 'os.vhd')]"           },           "caching": "ReadWrite",           "createOption": "FromImage"         }       },       "networkProfile": {         "networkInterfaces": [           {             "id": "[resourceId('Microsoft.Network/networkInterfaces', variables('vmADFSNicName'))]"           }         ]       }     },     "resources": [       {         "type": "extensions",         "name": "IaaSDiagnostics",         "apiVersion": "2015-06-15",         "location": "[parameters('resourceLocation')]",         "dependsOn": [           "[concat('Microsoft.Compute/virtualMachines/', variables('vmADFSName'))]"         ],         "tags": {           "displayName": "[concat(variables('vmADFSName'),'/vmDiagnostics')]"         },         "properties": {           "publisher": "Microsoft.Azure.Diagnostics",           "type": "IaaSDiagnostics",           "typeHandlerVersion": "1.4",           "autoUpgradeMinorVersion": "true",           "settings": {             "xmlCfg": "[base64(variables('wadcfgx'))]",             "StorageAccount": "[variables('storageAccountName')]"           },           "protectedSettings": {             "storageAccountName": "[variables('storageAccountName')]",             "storageAccountKey": "[listKeys(variables('storageAccountid'),'2015-05-01-preview').key1]",             "storageAccountEndPoint": "https://core.windows.net/"           }         }       },       {         "type": "Microsoft.Compute/virtualMachines/extensions",         "name": "[concat(variables('vmADFSName'),'/ADFSserver')]",         "apiVersion": "2015-05-01-preview",         "location": "[parameters('resourceLocation')]",         "dependsOn": [           "[resourceId('Microsoft.Compute/virtualMachines', variables('vmADFSName'))]",           "[concat('Microsoft.Compute/virtualMachines/', variables('vmADFSName'),'/extensions/IaaSDiagnostics')]"         ],         "properties": {           "publisher": "Microsoft.Powershell",           "type": "DSC",           "typeHandlerVersion": "1.7",           "settings": {             "modulesURL": "[concat(variables('vmDSCmoduleUrl'), parameters('_artifactsLocationSasToken'))]",             "configurationFunction": "[variables('vmADFSConfigurationFunction')]",             "properties": {               "domainName": "[variables('domainName')]",               "vmDCName": "[variables('vmDCName')]",               "adminCreds": {                 "userName": "[parameters('adminUsername')]",                 "password": "PrivateSettingsRef:adminPassword"               }             }           },           "protectedSettings": {             "items": {               "adminPassword": "[parameters('adminPassword')]"             }           }         }       },       {         "type": "Microsoft.Compute/virtualMachines/extensions",         "name": "[concat(variables('vmADFSName'),'/adfsScript')]",         "apiVersion": "2015-05-01-preview",         "location": "[parameters('resourceLocation')]",         "dependsOn": [           "[concat('Microsoft.Compute/virtualMachines/', variables('vmADFSName'))]",           "[concat('Microsoft.Compute/virtualMachines/', variables('vmADFSName'),'/extensions/ADFSserver')]"         ],         "properties": {           "publisher": "Microsoft.Compute",           "type": "CustomScriptExtension",           "typeHandlerVersion": "1.4",           "settings": {             "fileUris": [               "[concat(parameters('_artifactsLocation'),'/AdfsServer.ps1', parameters('_artifactsLocationSasToken'))]",               "[concat(parameters('_artifactsLocation'),'/PSPKI.zip', parameters('_artifactsLocationSasToken'))]",               "[concat(parameters('_artifactsLocation'),'/tuServDeployFunctions.ps1', parameters('_artifactsLocationSasToken'))]"             ],             "commandToExecute": "[concat('powershell.exe -file AdfsServer.ps1',' -vmAdminUsername ',parameters('adminUsername'),' -vmAdminPassword ',parameters('adminPassword'),' -fsServiceName ',variables('vmWAPpublicipDnsName'),' -vmDCname ',variables('vmDCName'), ' -resourceLocation \"', parameters('resourceLocation'),'\"')]"           }         }       }     ]   },   ]

The DSC Modules

All the DSC modules I need get zipped into the same archive file which is deployed by each DSC extension to the VMs. I showed you that in part one. For the ADFS server, the extension calls the configuration module DSCvmConfigs.ps1\\ADFSserver (note the escaped slash) – the ADFSserver configuration within my single DSCvmConfigs.ps1 file that holds all my configurations. As with the DC configuration, this is based on stuff held in the SharePoint farm template on GitHub.

configuration ADFSserver {     param     (         [Parameter(Mandatory)]         [String]$DomainName,          [Parameter(Mandatory)]         [String]$vmDCName,          [Parameter(Mandatory)]         [System.Management.Automation.PSCredential]$Admincreds,          [Int]$RetryCount=20,         [Int]$RetryIntervalSec=30     )      Import-DscResource -ModuleName xComputerManagement,xActiveDirectory      Node localhost     {         WindowsFeature ADFSInstall          {              Ensure = "Present"              Name = "ADFS-Federation"         }           WindowsFeature ADPS         {             Name = "RSAT-AD-PowerShell"             Ensure = "Present"          }          xWaitForADDomain DscForestWait          {              DomainName = $DomainName              DomainUserCredential= $Admincreds             RetryCount = $RetryCount              RetryIntervalSec = $RetryIntervalSec              DependsOn = "[WindowsFeature]ADPS"               }         xComputer DomainJoin         {             Name = $env:COMPUTERNAME             DomainName = $DomainName             Credential = New-Object System.Management.Automation.PSCredential ("${DomainName}\$($Admincreds.UserName)", $Admincreds.Password)             DependsOn = "[xWaitForADDomain]DscForestWait"         }           LocalConfigurationManager          {             DebugMode = $true                 RebootNodeIfNeeded = $true         }     }      }

The DSC for my ADFS server does much less than that of the DC. It installs the Windows features I need (the RSAT-AD-PowerShell tools are needed by the xWaitForADDomain config), makes sure our domain is contactable and joins the server to it. Unfortunately there are no DSC resources around to configure our ADFS server at the moment and whilst I’m happy writing scripts to to that work, I’m less comfortable writing DSC modules right now!

The Custom Scripts

Once our DSC extension has joined the domain and added our features, it’s over to the customscript extension to configure the ADFS service. As with the DC, I copy down the script itself, a file with my own functions in and the PSPKI module.

# # AdfsServer.ps1 # param (     $vmAdminUsername,     $vmAdminPassword,     $fsServiceName,     $vmDCname,     $resourceLocation )  $password =  ConvertTo-SecureString $vmAdminPassword -AsPlainText -Force $credential = New-Object System.Management.Automation.PSCredential("$env:USERDOMAIN\$vmAdminUsername", $password)  Write-Verbose -Verbose "Entering Domain Controller Script" Write-Verbose -verbose "Script path: $PSScriptRoot" Write-Verbose -Verbose "vmAdminUsername: $vmAdminUsername" Write-Verbose -Verbose "vmAdminPassword: $vmAdminPassword" Write-Verbose -Verbose "fsServiceName: $fsServiceName" Write-Verbose -Verbose "env:UserDomain: $env:USERDOMAIN" Write-Verbose -Verbose "resourceLocation: $resourceLocation" Write-Verbose -Verbose "==================================="      # Write an event to the event log to say that the script has executed.     $event = New-Object System.Diagnostics.EventLog("Application")     $event.Source = "tuServEnvironment"     $info_event = [System.Diagnostics.EventLogEntryType]::Information     $event.WriteEntry("ADFSserver Script Executed", $info_event, 5001)       $srcPath = "\\"+ $vmDCname + "\src"     $fsCertificateSubject = $fsServiceName + "."+($resourceLocation.Replace(" ",[System.String]::Empty)).ToLower()+".cloudapp.azure.com"     $fsCertFileName = $fsCertificateSubject+".pfx"     $certPath = $srcPath + "\" + $fsCertFileName      #Copy cert from DC     write-verbose -Verbose "Copying $certpath to $PSScriptRoot" #        $powershellCommand = "& {copy-item '" + $certPath + "' '" + $workingDir + "'}" #        Write-Verbose -Verbose $powershellCommand #        $bytes = [System.Text.Encoding]::Unicode.GetBytes($powershellCommand) #        $encodedCommand = [Convert]::ToBase64String($bytes)  #        Start-Process -wait "powershell.exe" -ArgumentList "-encodedcommand $encodedCommand"         copy-item $certPath -Destination $PSScriptRoot -Verbose  Invoke-Command  -Credential $credential -ComputerName $env:COMPUTERNAME -ScriptBlock {      param (         $workingDir,         $vmAdminPassword,         $domainCredential,         $fsServiceName,         $vmDCname,         $resourceLocation     )     # Working variables      Write-Verbose -Verbose "Entering ADFS Script"     Write-Verbose -verbose "workingDir: $workingDir"     Write-Verbose -Verbose "vmAdminPassword: $vmAdminPassword"     Write-Verbose -Verbose "fsServiceName: $fsServiceName"     Write-Verbose -Verbose "env:UserDomain: $env:USERDOMAIN"     Write-Verbose -Verbose "env:UserDNSDomain: $env:USERDNSDOMAIN"     Write-Verbose -Verbose "env:ComputerName: $env:COMPUTERNAME"     Write-Verbose -Verbose "resourceLocation: $resourceLocation"     Write-Verbose -Verbose "==================================="      # Write an event to the event log to say that the script has executed.     $event = New-Object System.Diagnostics.EventLog("Application")     $event.Source = "tuServEnvironment"     $info_event = [System.Diagnostics.EventLogEntryType]::Information     $event.WriteEntry("In ADFSserver scriptblock", $info_event, 5001)      #go to our packages scripts folder     Set-Location $workingDir          $zipfile = $workingDir + "\PSPKI.zip"     $destination = $workingDir     [System.Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem") | Out-Null     [System.IO.Compression.ZipFile]::ExtractToDirectory($zipfile, $destination)          Write-Verbose -Verbose "Importing PSPKI"     Import-Module .\tuServDeployFunctions.ps1       $fsCertificateSubject = $fsServiceName + "."+($resourceLocation.Replace(" ","")).ToLower()+".cloudapp.azure.com"     $fsCertFileName = $workingDir + "\" + $fsCertificateSubject+".pfx"      Write-Verbose -Verbose "Importing sslcert $fsCertFileName"     Import-SSLCertificate -certificateFileName $fsCertFileName -certificatePassword $vmAdminPassword       $adfsServiceAccount = $env:USERDOMAIN+"\"+"svc_adfs"     $adfsPassword = ConvertTo-SecureString $vmAdminPassword -AsPlainText -Force      $adfsCredentials = New-Object System.Management.Automation.PSCredential ($adfsServiceAccount, $adfsPassword)      $adfsDisplayName = "ADFS Service"      Write-Verbose -Verbose "Creating ADFS Farm"     Create-ADFSFarm -domainCredential $domainCredential -adfsName $fsCertificateSubject -adfsDisplayName $adfsDisplayName -adfsCredentials $adfsCredentials -certificateSubject $fsCertificateSubject   } -ArgumentList $PSScriptRoot, $vmAdminPassword, $credential, $fsServiceName, $vmDCname, $resourceLocation

The script starts by copying the certificate files from the DC. The script extension shells the script as the local system account, so it connects to the share on the DC as the computer account. I copy the files before I execute an invoke-command block that run as the domain admin. I do this because once I’m in that invoke-command block, network access becomes a real pain!

As you can see, this script doesn’t do a huge amount. Once in the invoke-command it unzips the PSPKI modules, imports the certificate it needs into the computer cert store and then calls a function to configure the ADFS service. The functions called by the script are below:

function Import-SSLCertificate {     [CmdletBinding()]     param     (         $certificateFileName,         $certificatePassword     )              Write-Verbose -Verbose "Importing cert $certificateFileName with password $certificatePassword"         Write-Verbose -Verbose "---"          Import-Module .\PSPKI\pspki.psm1          Write-Verbose -Verbose "Attempting to import certificate" $certificateFileName         # import it         $password = ConvertTo-SecureString $certificatePassword -AsPlainText -Force         Import-PfxCertificate –FilePath ($certificateFileName) cert:\localMachine\my -Password $password  }  function Create-ADFSFarm { [CmdletBinding()] param ( $domainCredential, $adfsName,  $adfsDisplayName,  $adfsCredentials, $certificateSubject )      Write-Verbose -Verbose "In Function Create-ADFS Farm"     Write-Verbose -Verbose "Parameters:"     Write-Verbose -Verbose "adfsName: $adfsName"     Write-Verbose -Verbose "certificateSubject: $certificateSubject"     Write-Verbose -Verbose "adfsDisplayName: $adfsDisplayName"     Write-Verbose -Verbose "adfsCredentials: $adfsCredentials"     Write-Verbose -Verbose "============================================"      Write-Verbose -Verbose "Importing Module"     Import-Module ADFS     Write-Verbose -Verbose "Getting Thumbprint"     $certificateThumbprint = (get-childitem Cert:\LocalMachine\My | where {$_.subject -match $certificateSubject} | Sort-Object -Descending NotBefore)[0].thumbprint     Write-Verbose -Verbose "Thumprint is $certificateThumbprint"     Write-Verbose -Verbose "Install ADFS Farm"      Write-Verbose -Verbose "Echo command:"     Write-Verbose -Verbose "Install-AdfsFarm -credential $domainCredential -CertificateThumbprint $certificateThumbprint -FederationServiceDisplayName '$adfsDisplayName' -FederationServiceName $adfsName -ServiceAccountCredential $adfsCredentials"     Install-AdfsFarm -credential $domainCredential -CertificateThumbprint $certificateThumbprint -FederationServiceDisplayName "$adfsDisplayName" -FederationServiceName $adfsName -ServiceAccountCredential $adfsCredentials -OverwriteConfiguration  }

There’s still stuff do do on the ADFS server once I get to deploying my application: I need to define relying party trusts and custom claims, for example. However, this deployment creates a working ADFS server that will authenticate users against my domain. It’s then published to the outside world safely by the Web Application Proxy role on my WAP server.

Credit Where It’s Due

Same as before – I stand on the shoulders of others to bring you this stuff:

  • The Azure Quick Start templates were invaluable in having something to look at in the early days of resource templates before the tooling was here.
  • PSPKI is a fantastic set of PowerShell modules for dealing with certs.
  • The individual VM scripts are derived from work that was done in Black Marble by Andrew Davidson and myself to build the exact same environment in the older Azure manner without resource templates.

Leave a Reply

Your email address will not be published. Required fields are marked *