Sign in during Public Preview to get the Team plan free, plus an early-adopter discount when we launch. Sign in
This guide shows you how to provision an Azure Bastion against Locally, put a Linux Virtual Machine with no public IP behind it, and then connect to that Virtual Machine through the Bastion. As with the other guides we're going to use the Azure CLI, but the same resources can be provisioned with HashiCorp Terraform, Pulumi or Bicep too.
az) installed. The az network bastion commands come from an extension, which the Azure CLI offers to install the first time you use them.Firstly, we need to launch Locally which we can do from a terminal by running:
$
locally build
Once Locally has started, the Locally Dashboard will open automatically:
Next we can create the Resource Group:
$
locally run az group create -n sample-bastion -l berlin
There's two things to note here:
locally run.locally run when you do.As in Azure, a Bastion lives in its own subnet, which has to be called AzureBastionSubnet. We'll create a Virtual Network with that subnet:
$
locally run az network vnet create -g sample-bastion -n sample-vnet --address-prefixes 10.0.0.0/16 --subnet-name AzureBastionSubnet --subnet-prefixes 10.0.0.0/26
And a second subnet for the Virtual Machine:
$
locally run az network vnet subnet create -g sample-bastion --vnet-name sample-vnet -n workloads --address-prefixes 10.0.1.0/24
Which leaves us with both subnets:
$
locally run az network vnet subnet list -g sample-bastion --vnet-name sample-vnet --query "[].{Name:name, Prefix:addressPrefix}" -o table
Name Prefix
------------------ -----------
AzureBastionSubnet 10.0.0.0/26
workloads 10.0.1.0/24
A Bastion needs a Standard Public IP Address, so we'll create one of those first:
$
locally run az network public-ip create -g sample-bastion -n sample-bastion-pip --sku Standard --allocation-method Static
Then the Bastion itself. The Azure CLI finds the AzureBastionSubnet in the Virtual Network for us:
$
locally run az network bastion create -g sample-bastion -n sample-bastion-host --vnet-name sample-vnet --public-ip-address sample-bastion-pip --sku Standard --query "{name:name, sku:sku.name, subnet:ipConfigurations[0].subnet.id, state:provisioningState}"
{
"name": "sample-bastion-host",
"sku": "Standard",
"state": "Succeeded",
"subnet": "/subscriptions/307d8f52-9719-460e-9f85-aa408e28ee55/resourceGroups/sample-bastion/providers/Microsoft.Network/virtualNetworks/sample-vnet/subnets/AzureBastionSubnet"
}
If the Virtual Network doesn't have an AzureBastionSubnet, the create fails instead.
We can see the Bastion in the Locally Dashboard too:
Now we can create an Ubuntu Virtual Machine in the workloads subnet. Passing an empty --public-ip-address stops the Azure CLI creating a Public IP for it, so the only way in is through the Bastion:
$
locally run az vm create -g sample-bastion -n bastionvm1 --image Ubuntu2404 --size Standard_B1s --admin-username azureuser --admin-password 'Sample-Passw0rd!' --authentication-type password --vnet-name sample-vnet --subnet workloads --public-ip-address ""
{
"fqdns": "",
"id": "/subscriptions/307d8f52-9719-460e-9f85-aa408e28ee55/resourceGroups/sample-bastion/providers/Microsoft.Compute/virtualMachines/bastionvm1",
"location": "berlin",
"macAddress": "00-0D-3A-DB-CC-9C",
"powerState": "VM running",
"privateIpAddress": "10.0.1.4",
"publicIpAddress": "",
"resourceGroup": "sample-bastion"
}
It only has a private address. Behind it is a container running Ubuntu 24.04 - see Provisioning a Virtual Machine for more on how Virtual Machines work in Locally.
Without a Public IP, connecting to the Virtual Machine directly doesn't work:
$
locally connect --virtual-machine bastionvm1
virtual machine "bastionvm1" has no public IP and no load balancer NAT rule to connect to; give it a public IP, add an inbound NAT rule reaching its network interface, or reach it through a bastion with '--bastion'
So we go through the Bastion instead. On its own, --bastion lists the Virtual Machines we can reach through it:
$
locally connect --bastion sample-bastion-host
VMs reachable through bastion "sample-bastion-host" (vnet: sample-vnet):
NAME PRIVATE IP SUBNET
bastionvm1 10.0.1.4 workloads
Connect with: locally connect --bastion sample-bastion-host --virtual-machine <NAME>
Note
locally connect is part of Locally itself, so it doesn't take the locally run prefix. Every az command does.Adding --virtual-machine connects to it:
$
locally connect --bastion sample-bastion-host --virtual-machine bastionvm1
This opens an SSH session in your terminal, with the admin password filled in for you. Rather than a shell, what answers is a view of the Virtual Machine itself, along with which Bastion the session came through:
There are a few differences from Azure here - az network bastion ssh, rdp and tunnel aren't supported, there's no in-browser session, and only Virtual Machines in the Bastion's own Virtual Network can be reached. The Bastion Emulator covers these in full.
Whilst this guide used the Azure CLI, Bastions work the same way through any of the tooling that Locally supports - a Microsoft.Network/bastionHosts resource in HashiCorp Terraform or OpenTofu, Pulumi, Bicep or an ARM Template all provision against Locally in the same way, and locally connect works however the Bastion was created.
The Virtual Machines Emulator explains what's behind the Virtual Machine you connect to. To control traffic within the Virtual Network, see Network Security Group.
Should you encounter any issues, please take a look at the troubleshooting section.
Sign in during Public Preview to get the Team plan free, plus an early-adopter discount when we launch. Sign in
Your Azure infrastructure, running on your machine. Deploy in seconds, break things freely, and ship to Azure when you're ready.