holger
Posted on December 20, 2022
Objectives
The steps in this document can be used to create a test environment for Azure Bastion and test the corresponding functionality by accessing a Windows and a Linux VM.
The code samples are solely for testing and learning purposes and should not be used in production environments.
Once testing is finished, the corresponding resources that were deployed throughout the testing should be removed in order to avoid costs.
Objectives:
- restrict inbound and outbound network traffic as much as possible, and
- use native clients.
Extra:
- Store private key inside a key vault
The terraform code samples can be found here.
Prepare the Provider and Create the Resource Group
As the very first step, the terraform providers need to be added and the destination resource group needs to be created.
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
}
tls = {
source = "hashicorp/tls"
}
}
}
provider "azurerm" {
features {}
}
As for the resource group, the following terraform code can be used.
resource "azurerm_resource_group" "rg" {
location = "westeurope"
name = "bastion-test-rg"
tags = {
owner = "me"
environment = "test"
}
}
Create the Networking Configuration
Virtual Network
The below code would create the Virtual Network itself as well as two subnets. Note that Azure Bastion requires a subnet with the name AzureBastionSubnet
to be created and it must be of size /26
as a minimum. [1]
# Create virtual network
resource "azurerm_virtual_network" "vnet" {
name = "test-vnet"
address_space = ["10.40.2.0/24"]
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
tags = {
owner = "me"
environment = "test"
}
}
# Create subnets
resource "azurerm_subnet" "bastion_subnet" {
name = "AzureBastionSubnet"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = ["10.40.2.0/25"]
}
resource "azurerm_subnet" "vm_subnet" {
name = "vm-subnet"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = ["10.40.2.128/25"]
}
Network Security Group
The testing scenario should only allow network connections that are absolutely required. General inbound/outbound network traffic should be avoided. Therefore, the Network Security Group includes only the following rules:
- RDP and SSH traffic is allowed inbound from the Azure Bastion subnet, so that Azure Bastion can connect to the virtual machines
- Any other inbound traffic is denied
- Outbound traffic is allowed into the VM subnet so that VMs within the subnet could communicate
- Any other outbound traffic is denied
As last step in belows terraform configuration, the Network Security Group is associated with the VM subnet.
# Create Network Security Group for VM Subnet and the corresponding rule for RDP from Azure Bastion
resource "azurerm_network_security_group" "vm_subnet_nsg" {
name = "nsg-vm-subnet"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
tags = {
owner = "me"
environment = "test"
}
}
resource "azurerm_network_security_rule" "inbound_allow_rdp" {
network_security_group_name = azurerm_network_security_group.vm_subnet_nsg.name
resource_group_name = azurerm_resource_group.rg.name
name = "Inbound_Allow_Bastion_RDP"
priority = 500
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "3389"
source_address_prefix = azurerm_subnet.bastion_subnet.address_prefixes[0]
destination_address_prefix = azurerm_subnet.vm_subnet.address_prefixes[0]
}
resource "azurerm_network_security_rule" "inbound_allow_ssh" {
network_security_group_name = azurerm_network_security_group.vm_subnet_nsg.name
resource_group_name = azurerm_resource_group.rg.name
name = "Inbound_Allow_Bastion_SSH"
priority = 510
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = azurerm_subnet.bastion_subnet.address_prefixes[0]
destination_address_prefix = azurerm_subnet.vm_subnet.address_prefixes[0]
}
resource "azurerm_network_security_rule" "inbound_deny_all" {
network_security_group_name = azurerm_network_security_group.vm_subnet_nsg.name
resource_group_name = azurerm_resource_group.rg.name
name = "Inbound_Deny_Any_Any"
priority = 1000
direction = "Inbound"
access = "Deny"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "*"
destination_address_prefix = azurerm_subnet.vm_subnet.address_prefixes[0]
}
resource "azurerm_network_security_rule" "outbound_allow_subnet" {
network_security_group_name = azurerm_network_security_group.vm_subnet_nsg.name
resource_group_name = azurerm_resource_group.rg.name
name = "Outbound_Allow_Subnet_Any"
priority = 500
direction = "Outbound"
access = "Allow"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = azurerm_subnet.vm_subnet.address_prefixes[0]
destination_address_prefix = azurerm_subnet.vm_subnet.address_prefixes[0]
}
resource "azurerm_network_security_rule" "outbound_deny_all" {
network_security_group_name = azurerm_network_security_group.vm_subnet_nsg.name
resource_group_name = azurerm_resource_group.rg.name
name = "Outbound_Deny_Any_Any"
priority = 1000
direction = "Outbound"
access = "Deny"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = azurerm_subnet.vm_subnet.address_prefixes[0]
destination_address_prefix = "*"
}
resource "azurerm_subnet_network_security_group_association" "nsg_vm_subnet_association" {
network_security_group_id = azurerm_network_security_group.vm_subnet_nsg.id
subnet_id = azurerm_subnet.vm_subnet.id
}
Virtual Machines
In order to be able to test RDP as well as SSH sessions, one Linux (Ubuntu) and one Windows (Windows Server 2022) are deployed.
Linux Virtual Machine (Ubuntu)
For the Linux VM an Ubuntu 18.04-LTS was chosen. In order to be able to login after the deployment, an SSH key must be created during deployment time.
# Create an SSH key
resource "tls_private_key" "ubn_ssh" {
algorithm = "RSA"
rsa_bits = 4096
}
Subsequently, the network interface and the VM resource can be created.
# Create network interface
resource "azurerm_network_interface" "nic_ubn_01" {
name = "nic_ubn-01"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
ip_configuration {
name = "nic_ubn-01-configuration"
subnet_id = azurerm_subnet.vm_subnet.id
private_ip_address_allocation = "Dynamic"
}
}
# Create virtual machine
resource "azurerm_linux_virtual_machine" "vm_ubn_01" {
name = "vm-ubn-01"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
network_interface_ids = [azurerm_network_interface.nic_ubn_01.id]
size = "Standard_DS1_v2"
os_disk {
name = "disk-os-ubn-01"
caching = "ReadWrite"
storage_account_type = "Premium_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "UbuntuServer"
sku = "18.04-LTS"
version = "latest"
}
computer_name = "vm-ubn-01"
admin_username = "ubn-azureuser"
disable_password_authentication = true
admin_ssh_key {
username = "ubn-azureuser"
public_key = tls_private_key.ubn_ssh.public_key_openssh
}
}
Windows Virtual Machine (Windows Server 2022)
The Windows Virtual Machine will be created similarly - an SSH key is obviously not required in this case. Not that the values for admin_username
and admin_password
are stored separately in a variable file, which is why only the references are visible in the configuration below.
# Create network interface
resource "azurerm_network_interface" "nic_win_01" {
name = "nic_win-01"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
ip_configuration {
name = "nic_win-01-configuration"
subnet_id = azurerm_subnet.vm_subnet.id
private_ip_address_allocation = "Dynamic"
}
}
resource "azurerm_windows_virtual_machine" "vm-win-01" {
name = "vm-win-01"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
size = "Standard_DS2_v2"
admin_username = var.admin_username
admin_password = var.admin_password
network_interface_ids = [
azurerm_network_interface.nic_win_01.id
]
os_disk {
name = "disk-os-win-01"
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "MicrosoftWindowsServer"
offer = "WindowsServer"
sku = "2022-Datacenter"
version = "latest"
}
}
Configure Azure Bastion
As the very last step, Azure Bastion can be deployed.
For testing purposes the Standard
SKU is used so that tunneling capabilities for using native clients are provided. The core capability of this SKU is the ability to scale up to 50 scale units - however, for this testing scenario, this is not relevant and only the minimum of 2 scale units should be configured.
Other than that, Azure Bastion requires a Public IP address against which the Azure Portal will connect incoming requests. For a full diagram of the connectivity flow for Azure Bastion, see the Azure Bastion Networking documentation. [2]
resource "azurerm_public_ip" "bastion_pip" {
name = "pip-bastion"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
allocation_method = "Static"
sku = "Standard"
}
resource "azurerm_bastion_host" "bastion_host" {
name = "bastion-host"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
sku = "Standard"
scale_units = 2
copy_paste_enabled = true
file_copy_enabled = true
shareable_link_enabled = true
tunneling_enabled = true
ip_configuration {
name = "config-01"
subnet_id = azurerm_subnet.bastion_subnet.id
public_ip_address_id = azurerm_public_ip.bastion_pip.id
}
}
Deploying the Resources
Once the configuration is finalized, the deployment can be performed by executing terraform init
, terraform plan
and finally terraform apply
. This should result in all resources to be created.
Apply complete! Resources: 18 added, 0 changed, 0 destroyed.
Subsequently the Azure Portal can be checked to verify that the resources were created successfully.
Accessing the Virtual Machines through Azure Bastion
SSH Access into Linux
Using the Azure Portal, it is now possible to login to the corresponding VM through Azure Bastion through the webbrowser.
On the login page of Azure Bastion, the previously created private key needs to be supplied via a file (or through Azure Key Vault).
Once logged-in, netstat -atWn
reveals that only one private network connection from the Azure Bastion Node 10.40.2.5
is esablished against port 22 of the Ubuntu VM.
For connecting through the command line, the SSH CLI extension is required. It can be installed by executing this command:
$ az extension add --name ssh
Once the extension is installed, the tunnel can be created and SSH connection can be established natively.
$ az network bastion ssh --name bastion-host --resource-group bastion-test-rg --target-resource-id /subscriptions/{subscription-id}/resourceGroups/bastion-test-rg/providers/Microsoft.Compute/virtualMachines/vm-ubn-01 --auth-type "ssh-key" --username ubn-azureuser --ssh-key .\ssh_private_key.pem
This should result in a successful login.
Command group 'network bastion' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus
Welcome to Ubuntu 18.04.6 LTS (GNU/Linux 5.4.0-1098-azure x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Fri Dec 16 14:03:50 UTC 2022
System load: 0.07 Processes: 108
Usage of /: 4.5% of 28.89GB Users logged in: 1
Memory usage: 5% IP address for eth0: 10.40.2.133
Swap usage: 0%
0 updates can be applied immediately.
New release '20.04.5 LTS' available.
Run 'do-release-upgrade' to upgrade to it.
Last login: Fri Dec 16 13:34:37 2022 from 10.40.2.5
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.
ubn-azureuser@vm-ubn-01:~$
Again, upon verifying the active network connections, only private connections from Bastion into the Ubuntu VM are established.
ubn-azureuser@vm-ubn-01:~$ netstat -atWn
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
tcp 0 280 10.40.2.133:22 10.40.2.5:47280 ESTABLISHED
tcp 0 0 10.40.2.133:22 10.40.2.5:46262 ESTABLISHED
tcp6 0 0 :::22 :::* LISTEN
If files need to be uploaded from the local computer to the target VM, other native clients, i.E. Putty need to be used.
In order for this to work a port forwarding from the local machine against Azure Bastion needs to be established by using the following command:
$ az network bastion tunnel --name bastion-host --resource-group bastion-test-rg --target-resource-id /subscriptions/{subscription-id}/resourceGroups/bastion-test-rg/providers/Microsoft.Compute/virtualMachines/vm-ubn-01 --resource-port 22 --port 52000
Once the tunnel is established, the console would indicate that it is waiting for incoming connections:
Command group 'network bastion' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus
Opening tunnel on port: 52000
Tunnel is ready, connect on port 52000
Ctrl + C to close
Afterwards, a native tool (i.E. Putty) could be used to connect:
Note that the key needs to be converted into the PPK format using PuttyGen
in order for the authentication to work successfully.
RDP Access into Windows
Accessing Windows through the webbrowser works very similar, besides it is only possible to use Username/Password to login.
Windows is attempting to connect to public IP addresses but is not successful due to the restrictive NSG that was put in place. Connections to 168.63.129.16
are always successful though, since this is a virtual public IP address that is used to facilitate a communication channel to Azure platform resources [3] and is fundamental to the systems functionality.
Other than that, the connection is coming from the Azure Bastion node as expected.
Port forwarding RDP to Bastion is as easy as port forwarding SSH into Linux.
$ az network bastion tunnel --name bastion-host --resource-group bastion-test-rg --target-resource-id /subscriptions/{subscription-id}/resourceGroups/bastion-test-rg/providers/Microsoft.Compute/virtualMachines/vm-win-01 --resource-port 3389 --port 52000
Once the tunnel is established, the console would indicate that it is waiting for incoming connections:
Command group 'network bastion' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus
Opening tunnel on port: 52000
Tunnel is ready, connect on port 52000
Ctrl + C to close
Now, the RDP client can be opened and pointed to localhost:52000
. Upon entering the Windows user credentials, the RDP session would be successfully established.
Extra: Store Private Key for Linux inside an Azure KeyVault
Storing the private key inside an Azure Key Vault requires a little bit of additional configuration:
- A Key Vault needs to be created
- Permissions need to be assigned against the Data Plane (i.E. Key Vault Secrets Officer) [4]
- The key needs to be uploaded to the Key Vault
Key Vault Terraform Configuration
First of all, the current client config needs to be added to the terraform configuration so that the user who is executing the script can have the Key Vault Secrets Officer role assigned conveniently during running the terraform configuration.
data "azurerm_client_config" "current" {}
The Azure Key Vault itself can then be added through the following code:
resource "azurerm_key_vault" "kv" {
name = "kv-bastion-test-001"
resource_group_name = azurerm_resource_group.rg.name
tenant_id = data.azurerm_client_config.current.tenant_id
location = azurerm_resource_group.rg.location
sku_name = "standard"
purge_protection_enabled = false
enable_rbac_authorization = true
}
resource "azurerm_role_assignment" "kv_secrets_officer" {
scope = azurerm_key_vault.kv.id
role_definition_name = "Key Vault Secrets Officer"
principal_id = data.azurerm_client_config.current.object_id
}
resource "azurerm_key_vault_secret" "ssh-key" {
name = "ubn-ssh-key"
value = tls_private_key.ubn_ssh.private_key_pem
key_vault_id = azurerm_key_vault.kv.id
tags = {
vm = azurerm_linux_virtual_machine.vm_ubn_01.name
}
depends_on = [
azurerm_role_assignment.kv_secrets_officer
]
}
With these configuration changes, the deployment can be executed. Once the deployment is done, the following command could be used to create the SSH session against the Ubuntu VM. The command below would download the Private Key from the Key Vault and save it inside the ssh_key.pem
file before establishing the session. Note that the key file would need to be manually deleted (if desired) after closing the session.
$ az network bastion ssh --name bastion-host --resource-group bastion-test-rg --target-resource-id /subscriptions/{subscription-id}/resourceGroups/bastion-test-rg/providers/Microsoft.Compute/virtualMachines/vm-ubn-01 --auth-type ssh-key --username ubn-azureuser --ssh-key $(az keyvault secret show --name ubn-ssh-key --vault-name kv-bastion-test-001 --query "value" --output tsv > ssh_key.pem; echo ".\ssh_key.pem")
Command group 'network bastion' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus
Welcome to Ubuntu 18.04.6 LTS (GNU/Linux 5.4.0-1098-azure x86_64)
- Documentation: https://help.ubuntu.com
- Management: https://landscape.canonical.com
- Support: https://ubuntu.com/advantage
System information as of Tue Dec 20 14:02:24 UTC 2022
System load: 0.1 Processes: 105
Usage of /: 4.4% of 28.89GB Users logged in: 0
Memory usage: 5% IP address for eth0: 10.40.2.133
Swap usage: 0%
0 updates can be applied immediately.
The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.
ubn-azureuser@vm-ubn-01:~$
Cleanup
Once done with testing, above resources could be removed by running terraform destroy
.
References
# | Title | URL | Accessed On |
---|---|---|---|
1 | Azure Bastion Subnet | https://learn.microsoft.com/en-us/azure/bastion/configuration-settings#subnet | 2022-12-19 |
2 | Connect to a VM via specified private IP address through the portal | https://learn.microsoft.com/en-gb/azure/bastion/connect-ip-address | 2022-12-19 |
3 | What is IP address 168.63.129.16? | https://learn.microsoft.com/en-us/azure/virtual-network/what-is-ip-address-168-63-129-16 | 2022-12-19 |
4 | Azure built-in roles for Key Vault data plane operations | https://learn.microsoft.com/en-us/azure/key-vault/general/rbac-guide | 2022-12-19 |
Posted on December 20, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.