Monday, May 8, 2017

Open RDP access to Azure virtual machines from your specific location with PowerShell

A PowerShell utility script to get your current external internet IP address and modify the Azure network security groups for your Azure VM’s to allow you to make RDP connections.

I recently got to see an Azure OMS demo by Ryan Zoeller from the Microsoft Cloud and Enterprise Black Belt team. Ryan mentioned in passing the challenges of setting up remote access to demo VM’s. At one extreme, the networking can be configured to allow access from anywhere, which opens up a potential attack vector. At the other extreme, networking can be locked down tight, but that limits your ability to access them during a demo at a client’s site. Everything in between is complicated and requires actual work.

I wanted a better solution, so I built one. I’m not going to do any of the actual work in the middle ground, I’m just building a utility script to make more effective use of one of the extremes.

Lock down the Azure network security group(s) (NSGs) for your Azure VM’s. Then, if you need to connect to them from a new location, run the code below to make the necessary changes to the NSGs to allow you to connect.


This code assumes your PowerShell session is already connected to Azure and that you are in the right context (tenant and subscription).

First, we need to know what IP address we will appear to Azure to be coming from. Randall Degges saw a need and provided a solution, ipify.org, a free to use web API to get your external address.

# Get current external IP address
$IP = Invoke-RestMethod -Uri 'https://api.ipify.org?format=json' | Select -ExpandProperty IP

Then we create the [PSSecurityRule] object which describes the firewall hole we are going to open up on all of the network security groups. We could use the New-AzureRMNetworkSecurityRuleConfig command, but instead we are going to leverage PowerShell’s dynamic tying to cast a hashtable as the correct object type. This doesn’t work with all object classes in all circumstances, but it works here, and I prefer the syntax as more human readable than the alternative.

# Create a security rule allowing inbound RDP connections from the external IP address
$SecurityRule = [Microsoft.Azure.Commands.Network.Models.PSSecurityRule]@{
        Name                      = ( 'RDP-' + $IP.Replace( '.', '-' ) )
        Description               = "RDP access from $IP"
        Protocol                  = "Tcp"
        DestinationAddressPrefix  = "*"
        DestinationPortRange      = 3389
        SourceAddressPrefix       = "$IP/32"
        SourcePortRange           = "*"
        Access                    = "Allow"
        Direction                 = "Inbound" }

Next we get all of the Azure virtual machines, NICs assigned to a VM, and network security groups (NSGs) that exist in this context.

# Get the VMs, assciated NICs, and all Network Security Groups in the current context
$VMs  = Get-AzureRmVM
$NICs = Get-AzureRmNetworkInterface | Where ID -in $VMs.NetworkProfile.NetworkInterfaces.Id
$NSGs = Get-AzureRmNetworkSecurityGroup

Loop through the NSGs …

# For each network security group
ForEach ( $NSG in $NSGs )
    {

Determine if the NSG is in use by a VM. NSGs can be associated directly with a NIC or to a subnet. We compare the list of the NSG’s associated NICs (if any) with the collection of NICs attached to VMs. Then we compare the list of the NSG’s associated subnets (if any) with the list of subnets attached to NICs attached to VMs. (I tried using Compare-Object to do the comparisons, but Compare misbehaves when one the of objects may be an array with a Null element.)

    # Determine if $NSG is in use by a VM,
    # that is, if it is connected to a NIC for a VM,
    # or if it is connected to a subnet which is connected to a NIC for a VM.
    $NSGinUse = $False
    ForEach ( $NIC    in $NSG.NetworkInterfaces.Id ) { If ( $NIC    -in $NICs.Id                         ) { $NSGinUse = $True } }
    ForEach ( $Subnet in $NSG.Subnets.Id           ) { If ( $Subnet -in $NICs.IpConfigurations.Subnet.Id ) { $NSGinUse = $True } }

If this NSG is and use, and the new security rule doesn’t already exist…

    # If the network security group is in use by a VM and
    # this security rule has not been previously added...
    If ( $NSGinUse -and $NSG.SecurityRules.Name -notcontains $SecurityRule.Name )
        {

Find a priority that isn’t already in use by an existing rule. This is a weakness in this bit of code. We are going to stick the new security rule in a somewhat random spot in the list. Most of the time, this will work just fine. If for some reason you have an existing list of security rules that is large and/or complex, this may fail or fail to put the rule in a place on the list where it will actually be processed. In those cases, you will need to modify this line to match your environment.

But this will almost always work as is.

        # Find an available priority
        $Priority = 1..40 | ForEach { $_ * 100 } | Where { $_ -notin $NSG.SecurityRules.Priority } | Select -First 1
        $SecurityRule.Priority = $Priority

Add the rule to the security group.

        # Add the new rule to the security group
        $NSG.SecurityRules.Add( $SecurityRule )
        }

And finally, post the change to Azure.

    # Post the changes to Azure
    $Null = Set-AzureRmNetworkSecurityGroup -NetworkSecurityGroup $NSG
    }

Here it is all together.

# Get current external IP address
$IP = Invoke-RestMethod -Uri 'https://api.ipify.org?format=json' | Select -ExpandProperty IP

# Create a security rule allowing inbound RDP connections from the external IP address
$SecurityRule = [Microsoft.Azure.Commands.Network.Models.PSSecurityRule]@{
        Name                      = ( 'RDP-' + $IP.Replace( '.', '-' ) )
        Description               = "RDP access from $IP"
        Protocol                  = "Tcp"
        DestinationAddressPrefix  = "*"
        DestinationPortRange      = 3389
        SourceAddressPrefix       = "$IP/32"
        SourcePortRange           = "*"
        Access                    = "Allow"
        Direction                 = "Inbound" }

# Get the VMs, assciated NICs, and all Network Security Groups in the current context
$VMs  = Get-AzureRmVM
$NICs = Get-AzureRmNetworkInterface | Where ID -in $VMs.NetworkProfile.NetworkInterfaces.Id
$NSGs = Get-AzureRmNetworkSecurityGroup

# For each network security group
ForEach ( $NSG in $NSGs )
    {
    # Determine if $NSG is in use by a VM,
    # that is, if it is connected to a NIC for a VM,
    # or if it is connected to a subnet which is connected to a NIC for a VM.
    $NSGinUse = $False
    ForEach ( $NIC    in $NSG.NetworkInterfaces.Id ) { If ( $NIC    -in $NICs.Id                         ) { $NSGinUse = $True } }
    ForEach ( $Subnet in $NSG.Subnets.Id           ) { If ( $Subnet -in $NICs.IpConfigurations.Subnet.Id ) { $NSGinUse = $True } }

    # If the network security group is in use by a VM and
    # this security rule has not been previously added...
    If ( $NSGinUse -and $NSG.SecurityRules.Name -notcontains $SecurityRule.Name )
        {
        # Find an available priority
        # Note: This is a weakness in the script. With no understanding of the existing rules, the selected priority for the new rule is somewhat random.
        # In the rare circumstance where the existing rules are numerous and/or complex, this may result in the new rule being ignored.
        $Priority = 1..40 | ForEach { $_ * 100 } | Where { $_ -notin $NSG.SecurityRules.Priority } | Select -First 1
        $SecurityRule.Priority = $Priority

        # Add the new rule to the security group
        $NSG.SecurityRules.Add( $SecurityRule )

        # Post the changes to Azure
        $Null = Set-AzureRmNetworkSecurityGroup -NetworkSecurityGroup $NSG
        }
    }

No comments:

Post a Comment