Create a Windows jump server with Azure DSC

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.


  • 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. The Config 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 its GetScript specification. So, to pass the FQDN to the DSC script when it is run on the target node, I used a DSC File resource whose only purpose is to write the incoming parm to disk so a Get-Content cmdlet in the Script 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.

        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
        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 
        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
        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,
        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
        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:

$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
    $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
                #   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
            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
                    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

#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 = ""
$PrivateIp = '' # Private IP address to be assigned to the VM
$FQDN = ($VmName + ".$Loc" + "").ToLower() # Create the FQDN for use in the DSC configuration

#region Storage account parameters
$OsDiskName = $VmName + "OsDisk"
$StorageAccount = ($VmName + "Storage").ToLower()
$StorageAccountType = "Standard_LRS" # No need for expensive storage in this test script

#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;}

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

# Create second subnet configuration
$SubnetConfig2 = New-AzureRmVirtualNetworkSubnetConfig -Name Subnet2 -AddressPrefix

# 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]

# 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] = and $Vnet.Subnets[1] =
# 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'




, , , ,



4 responses to “Create a Windows jump server with Azure DSC”

  1. taofik Avatar

    Thanks Robert for sharing. Will this script remove existing resource group in Azure as there are some servers already joined to it.


    1. Alex Neihaus Avatar
      Alex Neihaus

      Who is Robert? And I’m not sure what you mean by “remove existing resource group”. Most deployments are incremental unless specified not to be.

  2. Robert Mencarini Avatar
    Robert Mencarini

    Nicely done. Thanks for sharing. 

    1. Alex Neihaus Avatar
      Alex Neihaus

      Thank you, Robert. Hoe all is well.

Leave a Reply

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