I’ve blogged a lot over the last few years on how to set up a Windows jump (or bastion) server in public clouds. It’s amazing to me how crucial this is in Windows environments. It seems there’s a never-ending requirement to build jump boxes. Certainly every time you add a new application and/or network container (either VPC or Vnet), you might need to add bastion hosts.
An older post describing how to do it in AWS has become quite popular. But it involves an administrator logging into the Remote Desktop Gateway (RDG) and configuring it via the UI. I wanted a “pure” DevOps infrastructure-as-code approach, so I wrote one for Azure.
But even that script is incomplete. You still had to create the VM and then login and run the script to install and configure Remote Desktop Gateway. I wanted a script that would, from scratch and in one pass, create an Azure Vnet (and everything associated with it), launch a VM and then install and configure the RDG via PowerShell Desired State Configuration (DSC).
My “mother-of-all-Windows-jump-server scripts” is below. There’s so much going on that I will only list a few notes as bullets below. I’ve commented it extensively so you should be able to follow it. So, sit back and watch it happen — it takes about 25 minutes to run.
If you have any questions or comments about the script, please leave a comment below or contact me.
Notes:
- The Remote Desktop Gateway is all good-to-go. You need to log in once on TCP 3389, retrieve the self-signed cert in c:\temp and add the self-signed certificate to the local client’s Trusted Root Certification Authorities. Then you can close port 3389 in the Network Security Group and log in to the VM using its private address and the RDG as the gateway. That was the point of all this, right?
- Instead of passing a lot of parameters to the script, I’ve coded variables by topic in
#region
comments. This is because the fully-qualified domain name of the RDG ($FQDN
) is passed from the running script to the DSC configuration by Azure DSC. - The most intricate part of the script is caused by the brain-dead AzureRM cmdlet
Start-AzureRmAutomationDscCompilationJob
. TheConfig
parameter requires a file. That meant creating a file from the running script and storing it on disk. (Keep in mind, I wanted a single script, not one plus another file on disk.) To solve this, the actual DSC configuration is embedded in the running script as a here-string. - The DSC
Script
resource does not accept parameters in itsGetScript
specification. So, to pass the FQDN to the DSC script when it is run on the target node, I used a DSCFile
resource whose only purpose is to write the incoming parm to disk so aGet-Content
cmdlet in theScript
resource can retrieve it.
I hope this is of some use to you. I can’t decide, frankly, if it’s the worst POS code I’ve ever produced or the most elegant. But it sure was fun and I leave it to you to decide where on the POS-elegant spectrum it falls.
<# .SYNOPSIS This script: Creates a new resource group Creates a VNet with two subnets in the RG Allocates a static public IP Creates a NSG and adds rules permitting TCP 3389, 80 and 443 Launches a WinSrv2012R2 instance Configures RDG server via Remote Desktop Services PowerShell provider using a DSC configuration The DSC configuration: Is included in this script as a here-string (Watch out for quotes and double quotes!) Expects the fully-qualified domain name from this script to be passed to it as a parameter Uses a DSC File resource to create a variable in the configuration since the DSC Script resource does not resolve parameters .DESCRIPTION A single AzureRM script to create a Vnet, launch a VM, connect it to Azure DSC and run a DSC configuration to install and configure Remote Desktop Gateway .PARAMETER dnsName FQDN of the to-be-generated self-signed certificate .OUTPUTS Azure Resource Group, Vnet, networking resources, VM, Azure Automation Account and Azure DSC node configuration Working Remote Desktop Gateway with self-signed certificate Self-signed cert at $HOME/desktop/$dnsName.cert .NOTES This script adds non-AD local groups to RD-CAP and permits all accesses to back-end resources Alex Neihaus 2017-08-22 (c) 2017 Air11 Technology LLC -- licensed under the Apache OpenSource 2.0 license, https://opensource.org/licenses/Apache-2.0 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Author's blog: https://yobyot.com #> $VerbosePreference = "Continue" # DSC configuration script follows as a PowerShell "here string". TEST THIS CONFIG BEFORE EMBEDDING. If it has errors, this script will fail. # In the here-string that follows DO NOT USE any single quotes (') as this will mess up the here string. # Note that parameters for the DSC configuration are initialized in the script below and passed to Azure DSC via Start-AzureRmAutomationDscCompilationJob $configText = @' configuration InstallConfigureRemoteDesktopGateway { param( [Parameter(Mandatory=$true)] [string]$FQDN ) $VerbosePreference = "Continue" Import-DscResource -ModuleName PSDesiredStateConfiguration Import-DscResource -Name WindowsFeature Import-DscResource -Name Script Node localhost { File LoggingDirectory # Create a directory for log files and for the cert { Ensure = "Present" DestinationPath = "C:\Temp" Type = "Directory" MatchSource = $false # Only create the temp directory the first time the configuration is run } WindowsFeatureSet InstallRemoteDesktopGatewayFeatures { Name = @("web-server", "NPAS-Policy-Server", "Web-ISAPI-Ext", "Web-Mgmt-Compat", "RSAT-NPAS", "RPC-over-HTTP-Proxy", "RSAT-RDS-Gateway", "RDS-Gateway", "Telnet-client") Ensure = "Present" DependsOn = "[File]LoggingDirectory" } File FQDNFile { # Because the DSC Script resources does _not_ accept parameters in its GetScript statement, we will write the compile-time # FQDN parm to disk on this node and read it in in the Script resource below DependsOn ="[File]LoggingDirectory" Ensure = "Present" Type = "File" Contents = $FQDN DestinationPath = "C:\Temp\FQDN.txt" } Script "ConfigureRemoteDesktopGateway" { DependsOn = @("[WindowsFeatureSet]InstallRemoteDesktopGatewayFeatures", "[File]LoggingDirectory") SetScript = { Start-Transcript -Path "C:\Temp\ConfigureRemoteDesktopGateway.txt" -Append -Force -IncludeInvocationHeader Import-Module RemoteDesktopServices # Get the FQDN from the file created in the File resource # Create a self-signed certificate. This MUST installed in the LocalMachine Trusted Root store for RDP clients to see it. $DomainName = Get-Content -Path "C:\Temp\FQDN.txt" -Raw $x509Obj = New-SelfSignedCertificate -CertStoreLocation Cert:\LocalMachine\My -DnsName $DomainName # Export the cert to the desktop for use on clients $x509Obj | Export-Certificate -FilePath "C:\Temp\$DomainName.cer" -Force -Type CERT # See https://blogs.technet.microsoft.com/ptsblog/2011/12/09/extending-remote-desktop-services-using-powershell-part-5/ # for details of using the RDS provider. Its very poorly documented. If you need to find additional items, sl to the GatewayServer location you are interested in # and gci . -recurse | fl # Create RD-CAP with two user groups; defaults permit all device redirection. Might be worth tightening up in terms of security. $capName = "RD-CAP-$(Get-Date -Format FileDateTimeUniversal)" Set-Location RDS:\GatewayServer\SSLCertificate #Change to location where self-signed certificate is specified Set-Item .\Thumbprint -Value $x509obj.Thumbprint # Update RDG with the thumprint of the self-signed cert. # Create a new Connection Authorization Profile New-Item -Path RDS:\GatewayServer\CAP -Name $capName -UserGroups @("administrators@BUILTIN"; "Remote Desktop Users@BUILTIN") -AuthMethod 1 # Create a new Resouce Authorization Profile with "ComputerGroupType" set to 2 to permit connections to any device $rapName = "RD-RAP-$(Get-Date -Format FileDateTimeUniversal)" New-Item -Path RDS:\GatewayServer\RAP -Name $rapName -UserGroups @("administrators@BUILTIN"; "Remote Desktop Users@BUILTIN") -ComputerGroupType 2 Restart-Service TSGateway # We are done; Put everything into effect Stop-Transcript } GetScript = { Import-Module RemoteDesktopServices Return @{ Result = [string]$(Get-ChildItem -name RDS:\GatewayServer\SSLCertificate\Thumbprint) } } TestScript = { Import-Module RemoteDesktopServices If ((Get-ChildItem RDS:\GatewayServer\SSLCertificate\Thumbprint\).CurrentValue -eq "NULL") # There is no cert stored yet { Return $false # The script has not run yet } Else { Return $true # We have previously run this script } } } } } '@ #region VM administrative variables $AdminName = "administrator" # Administrator name in created VM $VmPassword = "AComplexPassword!@34!!??" # Admin password for VM $VmName = "RdgVm" # VMName -- also set to machine name #endregion #region Vnet parmeters $Loc = "eastus" # Azure region for all resources except automation account $Rg = "RdgVmRg" # Resource group name $AaLoc = "eastus2" # Azure region for automation account $VnetName = "Vnet" # VNet name $Cidr = "10.0.0.0/16" $PrivateIp = '10.0.20.5' # Private IP address to be assigned to the VM $FQDN = ($VmName + ".$Loc" + ".cloudapp.azure.com").ToLower() # Create the FQDN for use in the DSC configuration #endregion #region Storage account parameters $OsDiskName = $VmName + "OsDisk" $StorageAccount = ($VmName + "Storage").ToLower() $StorageAccountType = "Standard_LRS" # No need for expensive storage in this test script #endregion #region DSC-related parameters $Config = "InstallConfigureRemoteDesktopGateway" # Name of DSC configuration; must match name configuration name in here-string $nodeConfig = $Config + ".localhost" $ConfigFile = New-Item -Path $Env:TEMP\$Config.ps1 -ItemType File -Value $configText -Force # Create DSC configuration on local filesystem as Import-AzureRmAutomationDscConfiguration REQUIRES a file as input $ConfigSource = $ConfigFile.FullName $AaName = "DSCAutomationAccount" # Automation account name # DSC parameters MUST be in the form of a hashtable [hashtable]$DSCParms = @{"FQDN" = $FQDN;} #endregion Login-AzureRmAccount Select-AzureRMSubscription -SubscriptionName "AzureSubScriptionName" # Remove previous resource groups Remove-AzureRmResourceGroup -ResourceGroupName $Rg -Force # Create resource group New-AzureRmResourceGroup -Name $Rg -Location $Loc # Create containing resource group # Create first subnet configuration $SubnetConfig1 = New-AzureRmVirtualNetworkSubnetConfig -Name Subnet1 -AddressPrefix 10.0.10.0/24 # Create second subnet configuration $SubnetConfig2 = New-AzureRmVirtualNetworkSubnetConfig -Name Subnet2 -AddressPrefix 10.0.20.0/24 # Create a virtual network $Vnet = New-AzureRmVirtualNetwork -ResourceGroupName $Rg -Location $Loc ` -Name $VnetName -AddressPrefix $Cidr -Subnet $SubnetConfig1, $SubnetConfig2 # Create a public IP address and specify a DNS name $Pip = New-AzureRmPublicIpAddress -ResourceGroupName $Rg -Location $Loc ` -AllocationMethod Static -IdleTimeoutInMinutes 4 -Name "$Rg$(Get-Random)pip" -DomainNameLabel ($VmName).ToLower() # Sets DNS to $VmName.[region].cloudapp.azure.com # Create an inbound network security group rule for port 3389 $NsgRuleRdp = New-AzureRmNetworkSecurityRuleConfig -Name SecurityGroupRuleRDP -Protocol Tcp ` -Direction Inbound -Priority 1000 -SourceAddressPrefix * -SourcePortRange * -DestinationAddressPrefix * ` -DestinationPortRange 3389 -Access Allow # Create an inbound network security group rule for port 80 $NsgRuleWeb = New-AzureRmNetworkSecurityRuleConfig -Name SecurityGroupRuleWWW -Protocol Tcp ` -Direction Inbound -Priority 1001 -SourceAddressPrefix * -SourcePortRange * -DestinationAddressPrefix * ` -DestinationPortRange 80 -Access Allow # Create an inbound network security group rule for port 443 $NsgRuleTls = New-AzureRmNetworkSecurityRuleConfig -Name SecurityGroupRuleTLS -Protocol Tcp ` -Direction Inbound -Priority 1002 -SourceAddressPrefix * -SourcePortRange * -DestinationAddressPrefix * ` -DestinationPortRange 443 -Access Allow # Create a network security group $Nsg = New-AzureRmNetworkSecurityGroup -ResourceGroupName $Rg -Location $Loc ` -Name SecurityGroup -SecurityRules $NsgRuleRdp, $NsgRuleWeb, $NsgRuleTls # Create a virtual network card and associate with public IP address and NSG # In this script $Vnet.Subnets[0] = 10.0.10.0/24 and $Vnet.Subnets[1] = 10.0.20.0/24 # The value of $PrivateIp MUST match the containing subnet in the call to New-AzureRmNetworkInterface below $Nic = New-AzureRmNetworkInterface -Name Nic -ResourceGroupName $Rg -Location $Loc ` -SubnetId $Vnet.Subnets[1].Id -PublicIpAddressId $Pip.Id -NetworkSecurityGroupId $Nsg.Id -PrivateIpAddress $PrivateIp # For convenience, the password is hardcoded here and used to create $VmCred without prompting at the console $VmCred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $AdminName, ($VmPassword | ConvertTo-SecureString -AsPlainText -Force) # Create a storage account in this RG to hold the VHD $StgAcct = New-AzureRmStorageAccount -ResourceGroupName $Rg -Name $StorageAccount -Type $StorageAccountType -Location $Loc # Set up a pretty name for the VM's disk in the storage account $OSDiskUri = $StgAcct.PrimaryEndpoints.Blob.ToString() + "vhds/" + $OsDiskName + ".vhd" # Create PSVirtualMachine object for New-AzureRmVm $VmConfig = New-AzureRmVMConfig -VMName $VmName -VMSize Standard_D1_v2 | ` Set-AzureRmVMOperatingSystem -Windows -ComputerName $VmName -Credential $VmCred | ` Set-AzureRmVMSourceImage -PublisherName MicrosoftWindowsServer -Offer WindowsServer -Skus 2012-R2-Datacenter -Version latest | ` Add-AzureRmVMNetworkInterface -Id $Nic.Id | Set-AzureRmVMOSDisk -VhdUri $OSDiskUri -CreateOption FromImage # Create the virtual machine New-AzureRmVM -ResourceGroupName $Rg -Location $Loc -VM $VmConfig #Create an automation account in the new RG -- used for Auzre DSC New-AzureRmAutomationAccount -Name $AaName -Location $AaLoc -ResourceGroupName $Rg #Import the DSC Configuration Import-AzureRmAutomationDscConfiguration -AutomationAccountName $AaName -ResourceGroupName $Rg -Published -SourcePath $ConfigSource -Force #Remove the DSC configuration file from the local filesystem Remove-Item -Path $Env:TEMP\$Config.ps1 -Force #Compile the DSC Configuration Start-AzureRmAutomationDscCompilationJob -AutomationAccountName $AaName -ConfigurationName $Config -ResourceGroupName $Rg -Parameters $DSCParms # Register the new VM node with Azure DSC and apply the DSC configuration; be sure to specify the VMlocation Register-AzureRmAutomationDscNode -AzureVMName $VmName -ResourceGroupName $Rg -AutomationAccountName $AaName -NodeConfigurationName $nodeConfig -RebootNodeIfNeeded $true -AzureVMLocation $Loc -AllowModuleOverwrite $true -ConfigurationMode 'ApplyAndAutocorrect'
Leave a Reply