Secure VM Access with Azure Bastion

Remove public SSH exposure from a Linux VM using Azure Bastion.

Introduction

30 Second Summary

Every server with a public IP address gets bombarded with login attempts within minutes of going online. Attackers run automated scripts that try thousands of username and password combinations against any exposed SSH port.

In this project, you will deploy Azure Bastion to provide secure, browser-based SSH access to a Linux VM. You will then remove the VM's public IP entirely, making it invisible to the internet while keeping it fully accessible through Bastion's encrypted tunnel.

What You'll Build

A secure remote access architecture on Azure where a Linux VM has zero public internet exposure but remains fully manageable through Azure Bastion's TLS-encrypted browser session.

By the end of this project, you'll have:

  • A Virtual Network with two subnets (default + a dedicated AzureBastionSubnet).
  • A Linux VM accessible only via Azure Bastion with no public IP address.
  • An NSG configuration that explicitly blocks all inbound SSH from the internet.
  • Secret Mission: Upgrade to Standard SKU and connect from your Mac terminal using native SSH through Azure CLI.

Are there any prerequisites?

You need an Azure subscription with an active payment method or credits. This project costs approximately $0.50-$1.00 if you complete it in one sitting and delete all resources at the end.

Create the Base Infrastructure

Before you can secure a VM, you need a VM to secure. In this step, you'll build the "insecure starting state" that represents how many teams accidentally leave their infrastructure.

You'll create a Linux VM with a public IP address and an NSG rule that allows SSH from anywhere on the internet. This is the exact configuration that attackers scan for.

In this step, get ready to:
  • Create a resource group and virtual network.
  • Create a Network Security Group that allows SSH from anywhere.
  • Deploy a Linux VM with a public IP address.
Create a resource group and virtual network

Every Azure project starts with a resource group to hold your resources, and a Virtual Network (VNet) to define the private network your VM will live on.

  • Sign in to the Azure Portal.
  • In the search bar at the top, type Resource groups and select it from the results.
  • Click Create.
  • Enter bastion-lab-rg as the resource group name.
  • Select East US for the region.
  • Click Review + create, then click Create.

Your resource group is ready. Now create the virtual network inside it.

  • In the search bar, type Virtual networks and select it.
  • Click Create.
  • Select bastion-lab-rg as the resource group.
  • Enter bastion-lab-vnet as the virtual network name.
  • Select East US for the region.
  • Click Next to proceed to the IP Addresses tab.
  • Confirm the address space is set to 10.0.0.0/16.
  • Under Subnets, confirm a subnet named default exists with the range 10.0.0.0/24. If it doesn't exist, click Add a subnet and create it with those values.
  • Click Review + create, then click Create.

What is a Virtual Network?

A VNet is your private network in Azure. Resources inside a VNet can talk to each other using private IP addresses, isolated from the public internet.

The 10.0.0.0/16 address space gives you over 65,000 available private IPs. The default subnet carves out 256 of those for your VM.

Don't see East US in the region list?

Make sure your Azure subscription is active and has a valid payment method. Some trial subscriptions restrict available regions.

If East US is unavailable, select East US 2 as an alternative. Just use the same region for all resources in this project.

Help me troubleshoot region availability in Azure.

Create a Network Security Group

A Network Security Group controls which traffic can flow in and out of your subnet. You'll create one that explicitly allows SSH from anywhere. This is intentionally insecure so you can see the risk before locking it down.

  • In the search bar, type Network security groups and select it.
  • Click Create.
  • Select bastion-lab-rg as the resource group.
  • Enter vm-nsg as the name.
  • Select East US for the region.
  • Click Review + create, then click Create.

Now add the SSH rule that opens port 22 to the internet.

  • Once the NSG is created, click Go to resource.
  • In the left menu, click Inbound security rules.
  • Click Add.
  • Set Source to Any.
  • Set Destination to Any.
  • Set Service to SSH (this auto-fills port 22 and TCP).
  • Set Priority to 300.
  • Enter AllowSSH as the name.
  • Click Add.

Now associate this NSG with your default subnet so the rule takes effect.

  • In the left menu of your NSG, click Subnets.
  • Click Associate.
  • Select bastion-lab-vnet as the virtual network.
  • Select default as the subnet.
  • Click OK.

Why allow SSH from Any?

In production, you would never allow SSH from "Any" source. You're doing it here intentionally to create the insecure baseline. This lets you prove that the VM is exposed before you lock it down with Azure Bastion in a later step.

Create a Linux VM with a public IP

Now you'll deploy the VM that represents your workload. It gets a public IP so you can prove direct SSH access works before removing it later.

  • In the search bar, type Virtual machines and select it.
  • Click Create, then select Azure virtual machine.
  • Select bastion-lab-rg as the resource group.
  • Enter bastion-lab-vm as the virtual machine name.
  • Select East US for the region.
  • For Image, select Ubuntu Server (the latest LTS version available).
  • For Size, select Standard_B1s. Click See all sizes if it's not visible in the dropdown.
  • Under Administrator account, select SSH public key as the authentication type.
  • Enter azureuser as the username.
  • For SSH public key source, select Generate new key pair.
  • Enter bastion-lab-vm_key as the key pair name.
  • Under Inbound port rules, select Allow selected ports and choose SSH (22).

Now configure the networking settings.

  • Click the Networking tab at the top.
  • Confirm Virtual network is set to bastion-lab-vnet.
  • Confirm Subnet is set to default.
  • Confirm a Public IP is assigned (Azure creates one automatically).
  • Click Review + create, then click Create.

What happens when you click Create?

Azure will show a "Generate new key pair" dialog. Click Download private key and create resource. This saves a .pem file to your Downloads folder. You'll need this file to SSH into the VM.

  • In the dialog that appears, click Download private key and create resource.
  • Wait for the deployment to complete (this takes 1-2 minutes).
  • Click Go to resource.
  • On the VM's Overview page, find the Public IP address field and note down this value. You'll need it in the next step.

Keep the public IP handy

Copy the public IP address to a text file or sticky note. You'll use it multiple times in upcoming steps to test SSH access.

VM creation failed?

If you see a quota error for Standard_B1s, your subscription may have reached its vCPU limit for the region. Try selecting a different size like Standard_B1ls or switch to a different region (use the same region for all resources).

If the key pair download dialog didn't appear, check your browser's download bar. The file should be named bastion-lab-vm_key.pem in your ~/Downloads folder.

Help me troubleshoot my Azure VM creation failure.

Your insecure infrastructure is live. You have a VM with a public IP and an NSG rule that lets anyone on the internet attempt SSH connections. Next up, you'll prove this risk is real by connecting directly from your terminal.

Verify Direct SSH Access

Your VM currently has a public IP address with port 22 wide open to the entire internet. Anyone in the world can attempt to connect to it.

In this step, you'll prove that the VM is exposed by connecting directly from your Mac. You'll also check the VM's authentication logs to see whether unauthorized login attempts have already started.

In this step, get ready to:
  • SSH into the VM using its public IP address from your Mac terminal.
  • Check authentication logs for brute-force login attempts.
SSH into the VM from your Mac terminal

Right now, your VM's NSG rule AllowSSH permits TCP port 22 traffic from any source on the internet. That means your Mac can connect directly to the VM using SSH.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.

Before SSH will accept your private key file, the file's permissions must be restricted so only you can read it.

  • Set the correct permissions on your .pem key file by running this command:
chmod 400 ~/Downloads/bastion-lab-vm_key.pem

What does this command do?

The chmod 400 command sets your key file to read-only for your user account. SSH refuses to use key files that other users on the system could potentially read.

  • Connect to your VM by running this command:
ssh -i ~/Downloads/bastion-lab-vm_key.pem azureuser@[[PUBLIC_IP="your VM's public IP address"]]

What does this command do?

The -i flag tells SSH which private key file to use for authentication. The azureuser part is the default username Azure creates when you set up a Linux VM with SSH key authentication.

  • When you see the message "Are you sure you want to continue connecting (yes/no/[fingerprint])?", type yes and press Enter.

You should see a shell prompt that looks like azureuser@bastion-lab-vm:~$. You are now connected directly to your VM over the public internet.

Connection timing out or permission denied?

If you see "Permission denied (publickey)", make sure you ran the chmod 400 command on your .pem file first. SSH rejects keys with open permissions.

If the connection times out, double-check that you are using the correct public IP address from your VM's overview page in the Azure portal.

help me troubleshoot my SSH connection to an Azure VM

Observe the security risk

You just proved that anyone with your IP address and port 22 open can attempt to connect. Even brand-new VMs often receive brute-force login attempts within minutes of getting a public IP.

  • Check for unauthorized login attempts by running this command:
sudo cat /var/log/auth.log | grep "Failed password" | head -20
  • If that file does not exist or returns nothing, try this alternative command instead:
sudo journalctl -u ssh --since "1 hour ago"

What should I see?

You may see entries showing failed login attempts from IP addresses you don't recognize. These are automated bots scanning the internet for open SSH ports.

If your VM is very new (less than a few minutes old), you might not see any attempts yet. That's fine. The important takeaway is that your VM is publicly reachable on port 22, which means it is only a matter of time before bots find it.

Why is public SSH access dangerous?

Automated bots constantly scan the internet for open port 22. Once they find a target, they launch brute-force password attacks or exploit known SSH vulnerabilities.

Even with key-based authentication, having port 22 open to the world increases your attack surface. The goal is to make the VM completely invisible to the public internet.

  • Disconnect from the VM by typing:
exit

You should be back at your local Mac terminal prompt.

You've seen the problem firsthand. Your VM is reachable by anyone on the internet. Next up, you'll deploy Azure Bastion to provide a secure access path that doesn't require a public IP on your VM at all.

Deploy Azure Bastion

You just proved that your VM is exposed to the internet. Brute-force login attempts can start hitting a public-facing VM within minutes of it going live.

The fix is Azure Bastion. Bastion is a fully managed service that provides secure SSH access to your VMs through the Azure portal over TLS (port 443). Your VM never needs a public IP because Bastion connects using the VM's private IP inside the virtual network.

In this step, get ready to:
  • Create the AzureBastionSubnet in your virtual network.
  • Deploy Azure Bastion with Basic tier.
  • Connect to your VM through Bastion in the Azure portal.
Create the AzureBastionSubnet

Azure Bastion requires its own dedicated subnet with the exact name AzureBastionSubnet. No other name will work. The subnet must be /26 or larger.

  • In the Azure portal search bar, type bastion-lab-vnet and select your virtual network from the results.
  • In the left menu, click Subnets.
  • Click + Subnet at the top.
  • In the Name field, enter exactly AzureBastionSubnet.
  • Set the Starting address to 10.0.1.0.
  • Set the Subnet size to /26 (64 addresses).
  • Click Save.

After a few seconds, you should see AzureBastionSubnet listed alongside your default subnet in the Subnets view.

Why does Bastion need its own subnet?

Azure Bastion deploys dedicated infrastructure into your virtual network. The AzureBastionSubnet isolates Bastion's resources from your workload VMs. This separation lets Azure manage Bastion's networking rules independently without interfering with your application traffic.

Subnet creation failed?

The name must be exactly AzureBastionSubnet with that exact capitalization. Any typo (like azurebastionsubnet or BastionSubnet) will cause deployment to fail later.

Also confirm the subnet size is /26 or larger. A /27 will not work for new Bastion deployments.

Help me troubleshoot my AzureBastionSubnet creation error.

Deploy the Bastion resource

With the subnet ready, you can now deploy Azure Bastion itself. The Basic tier gives you portal-based SSH connections at $0.19/hour.

  • In the Azure portal search bar, type Bastions and select Bastions from the results.
  • Click Create.
  • For Resource group, select bastion-lab-rg.
  • In the Name field, enter bastion-lab-bastion.
  • For Region, select East US.
  • For Tier, select Basic.
  • For Virtual network, select bastion-lab-vnet.
  • The Subnet field auto-populates with AzureBastionSubnet. Leave it as is.
  • Under Public IP address, select Create new.
  • Enter bastion-lab-bastion-pip as the public IP name. The SKU (Standard) and Assignment (Static) are pre-selected.
  • Click Review + create.
  • Click Create.

Why does Bastion need a public IP?

The public IP (bastion-lab-bastion-pip) is the HTTPS endpoint you connect to from the Azure portal. Your browser talks to Bastion's public IP over port 443, and Bastion forwards the session to your VM's private IP inside the VNet.

This is fundamentally different from having a public IP on the VM itself. The Bastion public IP is managed by Azure with built-in DDoS protection and only accepts authenticated portal sessions.

Deployment takes approximately 10 minutes. You will see a notification when it completes.

While you wait

Azure is provisioning dedicated infrastructure in your VNet. This is not a shared service. You are getting your own Bastion hosts, which is why it takes a few minutes and costs $0.19/hour from the moment deployment completes.

Deployment failed?

If you see a subnet-related error, go back to your VNet's Subnets page and verify the subnet is named exactly AzureBastionSubnet with a /26 or larger range.

If the region shows an error, confirm both your resource group and virtual network are in East US. Bastion must be in the same region as the VNet.

Help me troubleshoot my Azure Bastion deployment failure.

Connect to your VM via Bastion

Once deployment completes, you can connect to your VM directly from the Azure portal. No terminal needed. No public IP on the VM needed.

  • In the Azure portal search bar, type bastion-lab-vm and select your virtual machine.
  • Click Connect at the top of the VM overview page.
  • Select Connect via Bastion.
  • For Authentication Type, select SSH Private Key from Local File.
  • In the Username field, enter azureuser.
  • Click Browse under Local File and navigate to ~/Downloads/bastion-lab-vm_key.pem.
  • Click Connect.

A new browser tab opens with your SSH session. You should see the Ubuntu welcome message and a command prompt. This connection is running through Bastion over TLS on port 443.

  • Type exit and press Enter to disconnect from the Bastion session.

What just happened?

Your browser connected to Bastion's public IP over HTTPS (port 443). Bastion then opened an SSH session to your VM using the VM's private IP (10.0.0.x) inside the VNet. At no point did your laptop talk directly to the VM.

The VM still has its public IP right now, but you just proved that Bastion can reach it without using that public IP. In the next step, you will remove the public IP entirely.

Connection timed out or failed?

Make sure you selected SSH Private Key from Local File as the authentication type. Selecting the wrong type (like password or pasting the key text) will fail.

Also confirm the username is azureuser (not your Azure account email) and that you uploaded the correct .pem file from your Downloads folder.

Help me troubleshoot my Bastion connection to bastion-lab-vm.

You now have two ways to reach your VM: the old insecure public SSH path and the new secure Bastion path. Next up, you will eliminate the insecure path by removing the public IP and locking down the NSG.

Remove Public Access and Lock Down the VM

You just proved that Azure Bastion provides a secure path into your VM without needing the public IP at all.

Now it is time to eliminate the attack surface. You will remove the VM's public IP so it becomes invisible to the internet, then lock down the NSG to block SSH even if someone re-attaches a public IP later. This layered approach is called defense in depth.

In this step, get ready to:
  • Remove the VM's public IP address.
  • Update the NSG to deny inbound SSH from the internet.
  • Delete the orphaned public IP resource.
Remove the VM's public IP

The VM's public IP is the front door that anyone on the internet can knock on. Removing it makes the VM completely unreachable from outside the VNet.

  • In the Azure portal, search for bastion-lab-vm and open your VM.
  • In the left menu, click Networking.
  • Click on the Network Interface name (it appears as a clickable link).
  • Under Settings, click IP configurations.
  • Click on ipconfig1 to open it.
  • Under Public IP address, change the dropdown to Disassociate (or select None).
  • Click Save.

Once the save completes, navigate back to your VM's Overview page. The Public IP address field should now show a dash or be empty.

What just happened?

The VM is now invisible from the internet. No one can reach it via a public IP because it no longer has one. The only way in is through the VNet's private network, which is exactly what Bastion uses.

Don't see the Disassociate option?

Make sure you clicked into ipconfig1 (the IP configuration row), not the network interface overview. The public IP dropdown only appears inside the IP configuration detail view.

Help me disassociate the public IP from my VM's network interface.

Update the NSG to deny internet SSH

Removing the public IP is one layer of protection. Adding an explicit deny rule in the NSG is a second layer. Even if someone accidentally re-attaches a public IP in the future, the NSG will still block SSH traffic from the internet.

  • In the Azure portal search bar, search for vm-nsg and open your Network Security Group.
  • In the left menu under Settings, click Inbound security rules.
  • Click on the AllowSSH rule you created earlier.
  • Change the Action from Allow to Deny.
  • Click Save.

You should see the AllowSSH rule now shows Deny in the Action column.

Why not just delete the rule?

You could delete the rule entirely and rely on Azure's default DenyAllInbound rule. Keeping an explicit Deny rule is better documentation. Anyone reviewing the NSG can immediately see that SSH from the internet was intentionally blocked, not just forgotten.

Can't find the AllowSSH rule?

Make sure you are viewing Inbound security rules (not Outbound). The rule is named AllowSSH with priority 300. If you see only the default rules (starting at priority 65000), double check you opened vm-nsg and not a different NSG.

Help me find and update my AllowSSH NSG rule.

Delete the orphaned public IP resource

When you disassociated the public IP from the VM, the IP resource itself still exists in your subscription. Leaving it around creates a risk that someone could accidentally re-attach it later.

  • In the Azure portal search bar, search for Public IP addresses and open the service.
  • Find the VM's old public IP. It will show Not associated in the Associated to column.
  • Click on the public IP resource to open it.
  • Click Delete at the top of the page.
  • Confirm the deletion when prompted.

Don't delete the wrong IP

You should see two public IPs in your resource group. The one named bastion-lab-bastion-pip belongs to Bastion and is still associated. Delete the other one (the VM's old public IP) which shows Not associated.

After deletion, you should see only bastion-lab-bastion-pip remaining in your Public IP addresses list for this resource group.

Seeing a deletion error?

If the delete fails with a message about the IP still being associated, go back to your VM's network interface and confirm the disassociation from the previous substep saved successfully. The public IP must show Not associated before it can be deleted.

Help me delete the orphaned public IP resource.

Your VM is now locked down with two layers of protection: no public IP and an explicit NSG deny rule. Next up, you will verify that direct SSH fails while Bastion still connects successfully.

Verify the Lockdown

In the previous step, you removed the VM's public IP and locked down the NSG to deny all inbound SSH from the internet. Your VM should now be completely invisible to anyone scanning the internet.

But "should be" is not the same as "is." In security, you always verify. In this step, you will prove that direct SSH access fails while Azure Bastion access still works perfectly.

In this step, get ready to:
  • Attempt a direct SSH connection to confirm it fails.
  • Connect through Azure Bastion to confirm it still works.
Confirm direct SSH fails

The old public IP you noted earlier no longer belongs to your VM. If your security hardening worked, any SSH attempt to that IP will time out or refuse the connection.

  • Press Cmd+Space (macOS) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Attempt to SSH into your VM using the old public IP by running this command:
ssh -i ~/Downloads/bastion-lab-vm_key.pem azureuser@[[OLD_PUBLIC_IP="your VM's old public IP address"]]

The connection should hang for a while, then time out or display a "Connection refused" error. This confirms the VM is no longer reachable from the internet.

Why does it time out instead of refusing immediately?

Since you deleted the public IP resource entirely, that IP address no longer routes to your VM. The SSH client sends packets into the void with no response, so it waits until the timeout period expires.

If you only disassociated the IP (without deleting it), you might see "Connection refused" instead. Either result confirms your VM is unreachable.

  • Press Ctrl+C to cancel the connection attempt if it hangs.

SSH still connecting?

If the SSH command connects successfully, your public IP was not fully removed. Go back to your VM's Networking settings in the Azure portal and confirm that the IP configuration shows no public IP associated.

Also check that your NSG rule AllowSSH was changed to Deny or deleted.

Why is my SSH still connecting after removing the public IP?

Confirm Bastion still works

Azure Bastion connects to your VM using its private IP address over the internal VNet. It does not need a public IP on the VM at all.

  • In the Azure portal, navigate to your VM bastion-lab-vm.
  • Click Connect at the top of the VM overview page.
  • Select Connect via Bastion.
  • Set Authentication Type to SSH Private Key from Local File.
  • Enter azureuser in the Username field.
  • Click the file picker under Local File and select your bastion-lab-vm_key.pem file from your Downloads folder.
  • Click Connect.

A new browser tab opens with your SSH session. You are connected to a VM that has zero public internet exposure.

Why does Bastion still work without a public IP?

Azure Bastion sits inside the same VNet as your VM (in the AzureBastionSubnet). When you click Connect, Bastion reaches your VM over its private IP address (10.0.0.x) using the internal network.

Your browser talks to Bastion over HTTPS (port 443). Bastion then opens the SSH connection to the VM internally. The VM never needs to be exposed to the internet.

  • Type exit in the Bastion session to disconnect.

Bastion connection failing?

Make sure you are selecting SSH Private Key from Local File as the authentication type. Pasting the key text directly will not work.

Check that the username is exactly azureuser (lowercase, no spaces). Also confirm you are uploading the .pem file, not a different key file.

Why is my Bastion connection failing even though it worked before?

Secret mission

Native SSH via Azure CLI

You've proven that Bastion works through the Azure portal. But infrastructure teams don't open browsers to manage VMs. In this secret mission, you'll upgrade to Standard SKU and connect directly from your Mac terminal using az network bastion ssh, the same approach used by CI/CD pipelines and automation scripts in production.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project created cloud resources that bill by the hour, so cleanup matters.

Cost warning

Azure Bastion bills continuously from the moment it is deployed, regardless of whether you are connected to a VM. At $0.19/hr (Basic) or $0.29/hr (Standard), leaving Bastion running overnight or over a weekend adds up quickly.

If you are done with this project, delete the entire resource group now to stop all charges immediately.

Resources you used:

  • Azure Bastion (bastion-lab-bastion) at $0.19/hr (Basic) or $0.29/hr (Standard).
  • Linux VM (bastion-lab-vm, Standard B1s) at ~$0.01/hr.
  • Bastion Public IP (bastion-lab-bastion-pip) at ~$0.005/hr.
  • Virtual Network (bastion-lab-vnet) with two subnets.
  • Network Security Group (vm-nsg).
  • Resource group (bastion-lab-rg) containing all of the above.

Keep everything running

No action needed. Choose this if you plan to continue experimenting with Bastion or testing additional VM configurations today.

  • Be aware that Bastion alone costs approximately $4.50/day (Basic) or $7/day (Standard).
  • Set a reminder to come back and delete the resource group when you are finished.

Pause - I'll come back to this later

Deallocate the VM to save compute costs. Note that Bastion will continue billing even when the VM is stopped.

  • Navigate to your VM (bastion-lab-vm) in the Azure portal.
  • Click Stop at the top of the Overview page.
  • Confirm by clicking Yes.
  • The VM will show a status of Stopped (deallocated). You will no longer pay for the VM compute, but Bastion continues to bill at $0.19/hr.

Delete - I don't want to use this again

Remove all project resources in one action by deleting the resource group. This is the recommended option once you have finished the project.

  • In the Azure portal, navigate to Resource groups.
  • Select bastion-lab-rg.
  • Click Delete resource group at the top of the page.
  • Type bastion-lab-rg in the confirmation field to confirm.
  • Click Delete.
  • Deletion takes a few minutes. All resources (Bastion, VM, VNet, NSG, public IPs) are removed together.
  • Delete the downloaded bastion-lab-vm_key.pem file from your Downloads folder.

Nice Work!

Nice Work!

You just locked down a VM that was fully exposed to the internet and replaced direct SSH with Azure Bastion's secure, managed access path. That is a core security hardening pattern used in production Azure environments.

You've learned how to:

  • Deployed Azure Bastion with a dedicated AzureBastionSubnet to provide browser-based SSH access to a Linux VM.
  • Removed the VM's public IP and updated NSG rules to explicitly deny inbound SSH, eliminating all internet exposure.
  • Verified that direct SSH fails while Bastion access succeeds, proving a defense-in-depth architecture works.
  • Secret Mission: Upgraded to Standard SKU and connected from your Mac terminal using az network bastion ssh for portal-free secure access.

Ready to quiz yourself?