Provisioning an Azure Bastion

Preview

Sign in during Public Preview to get the Team plan free, plus an early-adopter discount when we launch. Sign in

Provisioning an Azure Bastion

Available on Starter Standard Team Compare plans →

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.

Before you start

Plugins required

This requires the Microsoft.Compute and Microsoft.Network plugins, which you can install with:

$ locally plugin install --name Microsoft.Compute
$ locally plugin install --name Microsoft.Network

1. Start Locally

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:

Screenshot of the Locally Dashboard

2. Create a Resource Group

Next we can create the Resource Group:

$ locally run az group create -n sample-bastion -l berlin

There's two things to note here:

  1. The Azure CLI supports Automatic Configuration, meaning that it can automatically be configured to work against Locally just by prefixing commands with locally run.
  2. Locally intentionally uses a different set of locations to Azure as a safety precaution, so that you can be confident you're deploying against Locally rather than regular Azure. You can also configure Locally to use the Azure locations too, but you'll want to be extra sure that you're prefixing commands with locally run when you do.

3. Create the Virtual Network

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

4. Create the Bastion

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:

Screenshot of the Bastion in the Locally Dashboard

5. Create a Virtual Machine

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.

6. Connect through the Bastion

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:

Screenshot of a terminal session opened through a Bastion in Locally, showing the Virtual Machine's details and the available views

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.

7. Tidy up

To delete just the Bastion:

$ locally run az network bastion delete -g sample-bastion -n sample-bastion-host

Or to remove the Resource Group and everything within it, including the Virtual Machine's container:

$ locally run az group delete -n sample-bastion --yes

Doing this with other tooling

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.

Next steps

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.

Preview

Sign in during Public Preview to get the Team plan free, plus an early-adopter discount when we launch. Sign in

A local cloud for you and your AI agents.

Your Azure infrastructure, running on your machine. Deploy in seconds, break things freely, and ship to Azure when you're ready.