Wednesday, 5 June 2024

Azure Bastion with native tools & AAD

 

Azure Bastion with native tools & AAD

Follow this lab to configure a small Azure Bastion environment with a couple of example VMs for config management purposes. Authenticate with your Azure AD credentials and access using native SSH and RDP.

Introduction

The Azure Bastion service has been very successful since its introduction. The standard SKU enables the use of native tooling. There are also updated AAD extensions.

Combining these gives a very functional way of accessing virtual machines in Azure whilst enabling MFA and drastically limiting the attack surface. Remember that Azure Bastion can also access virtual machines across virtual network peers.

In this lab you will:

  • use Terraform to spin up the lab environment
  • connect to the Windows Server 2022 Azure Edition virtual machine via Azure Bastion
    • authenticate using Azure Active Directory
    • using the native Windows remote desktop tool
  • connect to the Ubuntu 20.04 virtual machine via Azure Bastion
    • authenticate using Azure Active Directory
    • using openssh from either Windows or WSL2
  • explore basic automation integration
    • set variables using the instance metadata service
    • access an example secret from the key vault using the virtual machine’s managed identity
  • access a “management” application using a tunnel via Azure Bastion
  • clean up

AAD authentication

Before we build the environment, spend a few mintes to understand the requirements for AAD auth on Azure VMs, as specified on these pages:

Windows checklist

  •  Supported OS level - Windows Server 2019 or Windows 10 1809 and later VM, e.g.

    publisher = "MicrosoftWindowsServer"
    offer     = "WindowsServer"
    sku       = "2022-datacenter-azure-edition"
    version   = "latest"
    
  •  System assigned managed identity enabled on the VM

  •  AADLoginForWindows extension added to the VM

    publisher = "Microsoft.Azure.ActiveDirectory"
    type      = "AADLoginForWindows"
    
  •  RBAC role assignment on the VM (or higher scope), either

    • Virtual Machine Administrator Login
    • Virtual Machine User Login
  •  Member user (guest IDs not supported)

  •  Outgoing 443 permitted to specific URIs

Example call (Windows OS level)

az login
az extension add --name bastion
az network bastion rdp --name myBastion --resource-group myBastionRG --target-resource-id <myVMID> --enable-mfa true

Login using RDP, specifying the userid in user@domain.com format.

Linux checklist

  •  Supported Linux distribution, e.g.

    publisher = "canonical"
    offer     = "0001-com-ubuntu-server-focal"
    sku       = "20_04-lts-gen2"
    version   = "latest"
    
  •  System assigned managed identity enabled on the VM

  •  AADSSHLoginForLinux extension added to the VM

    publisher = "Microsoft.Azure.ActiveDirectory"
    type      = "AADSSHLoginForLinux"
    

    (Installs the aadsshlogin or aadsshlogin-selinx package.)

  •  RBAC role assignment on the VM (or higher scope), either

    • Virtual Machine Administrator Login
    • Virtual Machine User Login
  •  Member user (guest IDs not supported)

  •  Outgoing 443 permitted to specific URIs

Example connection:

az login
az extension add --name ssh
az ssh vm --name myVM --resource-group myResourceGroup

Pre-requirements for Terraform example

You will need

If you want to use a completely separate SSH key pair for this lab then you can create one using the following command:

ssh-keygen -m PEM -t rsa -b 4096 -f ~/.ssh/bastion -N ''

Then specify admin_ssh_public_key_file = "~/.ssh/bastion.pub" in terraform.tfvars.

Create resources

Note that this section may be run from the Cloud Shell, but we recommend that you setup and use your own bash environment.

  1. Clone the repo

    git clone https://github.com/terraform-azurerm-examples/bastion
    
  2. Change directory

    cd bastion
    
  3. Initialise

    terraform init
    
  4. Create a terraform.tfvars

    Optional. You may override the default variable values in variables.tf or add in additional AAD object IDs for the Virtual Machine Administrator/User Login roles on the resource group.

    Example terraform.tfvars:

    location = "eastus2"
    admin_ssh_public_key_file = "~/.ssh/bastion.pub"
    address_space = "10.3.240.0/25"
    

    If a windows password is not specified for the admin account then one will be generated and stored in the key vault along with the private key for the SSH key pair. All access should be via AAD authentication, so these credentials are intended for break glass scenarios.

    For reference, here are all of the defaults found in variables.tf, expressed in terraform.tfvars format.

    resource_group_name = "bastion"
    location = "West Europe"
    bastion_name = "bastion"
    admin_username = "azureadmin"
    admin_ssh_public_key_file = "~/.ssh/id_rsa.pub"
    windows_server_name = "windows"
    windows_server_admin_password = null
    virtual_network_name = "bastion"
    address_space = "172.19.76.0/25"
    subnet_name = "vms"
    scale_units = 2
    virtual_machine_admins = []
    virtual_machine_users = []
    
  5. Plan

    terraform plan
    
  6. Apply

    terraform apply
    

    Terraform will start to create the resources and will then display the outputs. The resources take about 20 minutes to deploy.

  7. Redisplay output

    Optional. If you ever need to redisplay the output values, then type:

    terraform output
    

Resources created

All of the resources are created in a single resource group.

Resource TypeDefault NameNotes
Resource groupbastion
Virtual Networkbastion172.19.76.0/25, split into two /26 subnets for VMs and Azure Bastion
BastionbastionStandard SKU
SSH Keyubuntu-ssh-public-key~/.ssh/id_rsa.pub
VMubuntuUbuntu 20.04 with AAD and Azure tools
VMwindowsWindows 2022 Server Azure Edition with AAD and Azure tools
Key Vaultbastion-<uniq>-kvSecrets: windows password, private SSH key, sql connection string

Plus associated NSGs, NICs, OS disks etc.

RDP

Use the native Windows RDP client via Azure Bastion to access the Windows server. Authenticate with your AAD credentials.

The command to initiate an RDP session can only be used from a Windows client, e.g. Windows 10 / Windows 11.

PowerShell cmdlets for Azure Bastion are available but are not covered in this lab.

  1. Copy the command

    Run a terraform output command to display the command, and then copy the result. Run this from the directory that you originally cloned and ran terraform apply from.

    terraform output rdp_to_windows_server
    

    Example output:

    az network bastion rdp --name bastion --resource-group bastion --target-resource-id <vmid>
    

    Copy the command between the double quotes, but not the quotes themselves..

  2. Authenticate

    You’ll need the Azure CLI installed at the OS level.

    Open a PowerShell terminal on your machine and login to Azure.

    az login
    

    Check you are in the right subscription. (This is your “current context”.)

    az account show
    

    If not then change subscription.

  3. RDP

    Run the az network bastion rdp command you copied earlier. You’ll be prompted to re-authenticate using your AAD credentials before you can access the desktop.

    authenticate

    The RDP session will open if you have Virtual Machine Administrator Login or Virtual Machine User Login on the target VM.

    rdp

  4. Check identity access to the secret

    Optional.

    The Windows VM’s custom data has installed the Azure CLI and PowerShell 7 plus the Az PowerShell module. The VM has a system assigned managed identity that has been given an access policy on the key vault enabling it to get secrets.

    Use the identity to read the example sql secret from the key vault.

    • Copy the command

      terraform output example_secret_powershell
      

      Example command:

      Connect-AzAccount -Identity | Out-Null; Get-AzKeyVaultSecret -Name sql -VaultName bastion-<uniq>-kv -AsPlainText
      
    • Open a PowerShell 7 (x64) terminal from the remote desktop session’s Start menu

    • Paste the PowerShell command

      You will authenticate as the managed identity and retrieve the secret from the key vault.

      Example secret via PowerShell

      Expected output:

      Server=tcp:myserver.database.windows.net,1433;Database=myDataBase;User ID=mylogin@myserver;Password=myPassword;Trusted_Connection=False;Encrypt=True;
      

    The managed identity successfully retrieved the secret. This is a very useful pattern when using a config management server and you need to be able to add keys, secrets and certs into your automation without compromising security.

  5. Disconnect from the remote desktop session

SSH

Now you will SSH via Azure Bastion to the linux VM and authenticate using AAD.

This command works from both Windows (which now has the native openssh client) and from linux systems such as WSL2.

  1. Copy the command

    terraform output user_ssh_to_linux_server
    

    Example output:

    az network bastion ssh --name bastion --resource-group bastion --target-resource-id <vmid> --auth-type AAD
    
  2. Open a PowerShell terminal on your machine

  3. Paste the az network bastion ssh command

    You may be prompted to re-authenticate using your AAD credentials if the token needs refreshing.

    ssh

    Note that you are logged in as your Azure AD user principal name.

  4. Use the Instance Metadata Service (IMDS)

    Optional.

    The Ubuntu VM used cloud-init to install the Azure CLI, Terraform, jq, stress and tree. It has also set the JQ_COLORS environment variable to match the jsonc output form the Azure CLI.

    Access the instance metadata service using the command below.

    curl -H Metadata:true --noproxy "*" "http://169.254.169.254/metadata/instance?api-version=2021-02-01" | jq
    

    There is some useful information in there which may be useful in scripts.

    Here are some example techniques to set variables from the IMDS output JSON.

    • straight IMDS REST API call

      image_reference=$(curl -sSLH Metadata:true --noproxy "*" "http://169.254.169.254/metadata/instance/compute/storageProfile/imageReference?api-version=2021-02-01")
      
    • extracting a sub-value using jq

      subscription_id=$(curl -sSLH Metadata:true --noproxy "*" "http://169.254.169.254/metadata/instance/compute/?api-version=2021-02-01" | jq -r .subscriptionId)
      
    • slurping the JSON into a variable used as a here string

      imds=$(curl -sSLH Metadata:true --noproxy "*" "http://169.254.169.254/metadata/instance/?api-version=2021-02-01")
      id=$(jq -r .compute.resourceId <<< $imds)
      subscription_id=$(jq -r .compute.subscriptionId <<< $imds)
      resource_group_name=$(jq -r .compute.resourceGroupName <<< $imds)
      resource_group_id=${id%%/providers/Microsoft.Compute/*}
      

      This approach has a modest speed gain.

  5. Check managed identity access to the secret

    Optional.

    The Ubuntu VM also has a managed identity and an access policy to get secrets.

    • Copy the command

      terraform output example_secret_cli
      

      Example command:

      az login --identity --allow-no-subscriptions --output none; az keyvault secret show --name sql --vault-name bastion-<uniq>-kv --query value --output tsv
      
    • Paste the command into your Ubuntu VM ssh session

      Expected output:

      Server=tcp:myserver.database.windows.net,1433;Database=myDataBase;User ID=mylogin@myserver;Password=myPassword;Trusted_Connection=False;Encrypt=True;
      

Tunneling

The core SSH and RDP commands will be all that most admins will need. But what if you want to punch through to another port? For instance, an admin portal web page that is running on one of those servers?

Let’s use the example of Jenkins, which runs a web UI defaulting to port 8080. We’ll emulate that with a simple HTML page.

Create a basic web server

You should still be on the Ubuntu VM as your AAD credentials. Spin up a simple web server.

  1. Create a directory

    sudo mkdir -m 755 /web
    
  2. Create an index.html file

    sudo curl https://raw.githubusercontent.com/terraform-azurerm-examples/bastion/main/index.html --output /web/index.html
    
  3. Start the web server on port 8080

    sudo /usr/bin/python3 -m http.server --directory /web 8080
    

    Expected output:

    Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...
    
  4. Keep this session open

Tunnel

On your machine, start up the tunnel and then access the site via the browser.

  1. Open a new terminal session and go back into your Terraform folder

  2. Display the tunnel command

    terraform output tunnel_to_linux_server
    

    Copy the command between the prompts.

    Example command format:

    az network bastion tunnel --name bastion --resource-group bastion --target-resource-id <vmid> --resource-port 8080 --port 8080
    

    The resource port is the one on the VM. It will be mapped to the --port value.

  3. Run the az network bastion tunnel command at the OS level

    Tunnel

  4. Open the web page

    Open up a browser and go to http://localhost:8080.

    If you see the basic web page below then you have traversed the tunnel through Azure Bastion and accessed the web page running on the virtual machine.

    Web Page

Deploy Azure Bastion by using specified settings

 

Deploy Azure Bastion by using specified settings

This tutorial helps you deploy Azure Bastion from the Azure portal by using your own manual settings and a SKU (product tier) that you specify. The SKU determines the features and connections that are available for your deployment. For more information about SKUs, see Configuration settings - SKUs.

In the Azure portal, when you use the Configure manually option to deploy Bastion, you can specify configuration values such as instance counts and SKUs at the time of deployment. After Bastion is deployed, you can use SSH or RDP to connect to virtual machines (VMs) in the virtual network via Bastion using the private IP addresses of the VMs. When you connect to a VM, it doesn't need a public IP address, client software, an agent, or a special configuration.

The following diagram shows the architecture of Bastion.

Diagram that shows the Azure Bastion architecture.

In this tutorial, you deploy Bastion by using the Standard SKU. You adjust host scaling (instance count), which the Standard SKU supports. If you use a lower SKU for the deployment, you can't adjust host scaling. You can also select an availability zone, depending on the region to which you want to deploy.

After the deployment is complete, you connect to your VM via private IP address. If your VM has a public IP address that you don't need for anything else, you can remove it.

In this tutorial, you learn how to:

  • Deploy Bastion to your virtual network.
  • Connect to a virtual machine.
  • Remove the public IP address from a virtual machine.

Prerequisites

To complete this tutorial, you need these resources:

  • An Azure subscription. If you don't have one, create a free account before you begin.

  • virtual network where you'll deploy Bastion.

  • A virtual machine in the virtual network. This VM isn't a part of the Bastion configuration and doesn't become a bastion host. You connect to this VM later in this tutorial via Bastion. If you don't have a VM, create one by using Quickstart: Create a Windows VM or Quickstart: Create a Linux VM.

  • Required VM roles:

    • Reader role on the virtual machine
    • Reader role on the network adapter (NIC) with the private IP of the virtual machine
  • Required inbound ports:

    • For Windows VMs: RDP (3389)
    • For Linux VMs: SSH (22)


Example values

You can use the following example values when creating this configuration, or you can substitute your own.

Basic virtual network and VM values

NameValue
Virtual machineTestVM
Resource groupTestRG1
RegionEast US
Virtual networkVNet1
Address space10.1.0.0/16
SubnetsFrontEnd: 10.1.0.0/24

Bastion values

NameValue
NameVNet1-bastion
+ Subnet NameAzureBastionSubnet
AzureBastionSubnet addressesA subnet within your virtual network address space with a subnet mask of /26 or larger; for example, 10.1.1.0/26
Availability zoneSelect value(s) from the dropdown list, if desired.
Tier/SKUStandard
Instance count (host scaling)3 or greater
Public IP addressCreate new
Public IP address nameVNet1-ip
Public IP address SKUStandard
AssignmentStatic

Deploy Bastion

This section helps you deploy Bastion to your virtual network. After Bastion is deployed, you can connect securely to any VM in the virtual network using its private IP address.

 Important

Hourly pricing starts from the moment that Bastion is deployed, regardless of outbound data usage. For more information, see Pricing and SKUs. If you're deploying Bastion as part of a tutorial or test, we recommend that you delete this resource after you finish using it.

  1. Sign in to the Azure portal.

  2. Go to your virtual network.

  3. On the page for your virtual network, on the left pane, select Bastion.

  4. On the Bastion pane, expand Dedicated Deployment Options.

  5. Select Configure manually. This option lets you configure specific additional settings (such as the SKU) when you're deploying Bastion to your virtual network.

    Screenshot that shows dedicated deployment options for Azure Bastion and the button for manual configuration.

  6. On the Create a Bastion pane, configure the settings for your bastion host. Project details are populated from your virtual network values. Under Instance details, configure these values:

    • Name: The name that you want to use for your Bastion resource.

    • Region: The Azure public region in which the resource will be created. Choose the region where your virtual network resides.

    • Availability zone: Select the zone(s) from the dropdown, if desired. Only certain regions are supported. For more information, see the What are availability zones? article.

    • Tier: The SKU. For this tutorial, select Standard. For information about the features available for each SKU, see Configuration settings - SKU.

    • Instance count: The setting for host scaling, which is available for the Standard SKU. You configure host scaling in scale unit increments. Use the slider or enter a number to configure the instance count that you want. For more information, see Instances and host scaling and Azure Bastion pricing.

    Screenshot of Azure Bastion instance details.

  7. Configure the Virtual networks settings. Select your virtual network from the dropdown list. If your virtual network isn't in the dropdown list, make sure that you selected the correct Region value in the previous step.

  8. To configure AzureBastionSubnet, select Manage subnet configuration.

    Screenshot of the section for configuring virtual networks.

  9. On the Subnets pane, select +Subnet.

  10. On the Add subnet pane, create the AzureBastionSubnet subnet by using the following values. Leave the other values as default.

    • The subnet name must be AzureBastionSubnet.
    • The subnet must be /26 or larger (for example, /26/25, or /24) to accommodate features available with the Standard SKU.

    Select Save at the bottom of the pane to save your values.

  11. At the top of the Subnets pane, select Create a Bastion to return to the Bastion configuration pane.

    Screenshot of the pane that lists Azure Bastion subnets.

  12. The Public IP address section is where you configure the public IP address of the bastion host resource on which RDP/SSH will be accessed (over port 443). The public IP address must be in the same region as the Bastion resource that you're creating.

    Create a new IP address. You can leave the default naming suggestion.

  13. When you finish specifying the settings, select Review + Create. This step validates the values.

  14. After the values pass validation, you can deploy Bastion. Select Create.

    A message says that your deployment is in process. The status appears on this page as the resources are created. It takes about 10 minutes for the Bastion resource to be created and deployed.

Connect to a VM

You can use any of the following detailed articles to connect to a VM. Some connection types require the Bastion Standard SKU.

You can also use these basic connection steps to connect to your VM:

  1. In the Azure portal, go to the virtual machine that you want to connect to.

  2. At the top of the pane, select Connect > Bastion to go to the Bastion pane. You can also go to the Bastion pane by using the left menu.

  3. The options available on the Bastion pane depend on the Bastion SKU. If you're using the Basic SKU, you connect to a Windows computer by using RDP and port 3389. Also for the Basic SKU, you connect to a Linux computer by using SSH and port 22. You don't have options to change the port number or the protocol. However, you can change the keyboard language for RDP by expanding Connection Settings.

    Screenshot of Azure Bastion connection settings.

    If you're using the Standard SKU, you have more connection protocol and port options available. Expand Connection Settings to see the options. Typically, unless you configure different settings for your VM, you connect to a Windows computer by using RDP and port 3389. You connect to a Linux computer by using SSH and port 22.

    Screenshot of expanded connection settings.

  4. For Authentication Type, select from the dropdown list. The protocol determines the available authentication types. Complete the required authentication values.

    Screenshot that shows the dropdown list box for authentication type.

  5. To open the VM session in a new browser tab, leave Open in new browser tab selected.

  6. Select Connect to connect to the VM.

  7. Confirm that the connection to the virtual machine opens directly in the Azure portal (over HTML5) by using port 443 and the Bastion service.

    Screenshot of a computer desktop with an open connection over port 443.

     Note

    When you connect, the desktop of the VM will look different from the example screenshot.

Using keyboard shortcut keys while you're connected to a VM might not result in the same behavior as shortcut keys on a local computer. For example, when you're connected to a Windows VM from a Windows client, Ctrl+Alt+End is the keyboard shortcut for Ctrl+Alt+Delete on a local computer. To do this from a Mac while you're connected to a Windows VM, the keyboard shortcut is Fn+Ctrl+Alt+Backspace.

Enable audio output

You can enable remote audio output for your VM. Some VMs automatically enable this setting, whereas others require you to enable audio settings manually. The settings are changed on the VM itself. Your Bastion deployment doesn't need any special configuration settings to enable remote audio output.

 Note

Audio output uses bandwidth on your internet connection.

To enable remote audio output on a Windows VM:

  1. After you're connected to the VM, an audio button appears on the lower-right corner of the toolbar. Right-click the audio button, and then select Sounds.
  2. A pop-up message asks if you want to enable the Windows Audio Service. Select Yes. You can configure more audio options in Sound preferences.
  3. To verify sound output, hover over the audio button on the toolbar.

Remove a VM's public IP address

When you connect to a VM by using Azure Bastion, you don't need a public IP address for your VM. If you aren't using the public IP address for anything else, you can dissociate it from your VM:

  1. Go to your virtual machine. On the Overview page, click the Public IP address to open the Public IP address page.

  2. On the Public IP address page, go to Overview. You can view the resource that this IP address is Associated to. Select Dissociate at the top of the pane.

    Screenshot of details for a virtual machine's public IP address.

  3. Select Yes to dissociate the IP address from the VM network interface. After you dissociate the public IP address from the network interface, verify that it's no longer listed under Associated to.

  4. After you dissociate the IP address, you can delete the public IP address resource. On the Public IP address pane for the VM, select Delete.

    Screenshot of the button for deleting a public IP address resource.

  5. Select Yes to delete the public IP address.