Skip to content

Azure Bastion Host: Secure Cloud Access Made Simple

Discover how Azure Bastion can revolutionize your cloud security strategy. This comprehensive guide explains what a Bastion host is, why it's crucial for secure access to your Azure resources, and provides a step-by-step walkthrough for implementation.

You'll learn how to enhance your network security, simplify remote access, and automate Bastion deployment using tools like OpenTofu and Azure CLI. Dive in to unlock the full potential of secure, scalable cloud access for your organization.

Introduction

Deploying production workloads in cloud providers has made a lot of operational efforts easier. They allow for a lot of flexibility in your infrastructure & with their on-demand offerings, your life as an administrator will be much easier.

Challenges in Cloud Network Security

However, this ease doesn't come at a cheap price; and I'm not talking about just the financial implications of deploying to cloud providers, but also their maintenance and long-term management.

When you deploy your services to cloud providers, you can't simply lean back and relax! They do make a lot of things easier, but they don't do magic. At the end of the day, you're still in charge of anything happening to your production workloads and as a result, your customers.

One of the most critical aspect of managing a production workload is to ensure it is compliant with your security policies, not allowing adversaries to take control and damage your business, financially and reputation-wise.

Bastion Host: Secure Cloud Access Made Simple

Bastion hosts are computers like any other, sitting in your private network and opening a backdoor to the internal services, using which you can gain access to the resources which would've otherwise been closed due to deep defensive measures.

Here's a how it look like:

graph TD
    subgraph Private Network
        direction TB
        B1[Bastion]
        M1[Machine 1]
        M2[Machine 2]
        M3[Machine 3]
    end

    Internet -- "SSH (port 22)" --> B1
    B1 --> M1
    B1 --> M2
    B1 --> M3

The bastion host in this setup may also be called the "jump host", as in, you make an extra hop from your current node to the target node using one extra jump. 🦘

What is a Bastion Host? Understanding the Gateway to Secure Cloud Access

Defining the Core Concept of Bastion Hosts

A Bastion host, often referred to as a jump server or jump box, is a specially designed computer on a network that serves as a critical access point for a protected network, particularly when accessing internally isolated environments1.

In the context of cloud computing, a Bastion host acts as a secure, intermediate server that allows authorized users to connect to other servers or services within a private network, typically a Virtual Private Cloud (VPC) or Virtual Network (VNet)2.

The primary purpose of a Bastion host is to provide a controlled and monitored entry point into a protected network environment, reducing the attack surface and enhancing overall security posture.

By channeling all external access through this fortified server, organizations can implement robust security measures, such as multi-factor authentication, detailed logging, and fine-grained access controls.

The provided capabilities for audit allows organization to adhere to the strict compliance requirements and security standards, ensuring that only authorized users can access sensitive resources3.

The Evolution of Bastion Hosts: From On-Premises to Cloud

Historically, Bastion hosts emerged in the early days of network security as a means to protect sensitive on-premises infrastructure. As organizations began to adopt more complex network architectures, the need for a secure gateway became increasingly apparent. Traditional Bastion hosts were often hardened Linux or Unix servers, meticulously configured to withstand potential attacks.

With the advent of cloud computing, the concept of Bastion hosts has evolved to meet the unique challenges of distributed and scalable environments. Cloud providers like Azure have introduced managed Bastion services, such as Azure Bastion, which offer enhanced security features, seamless integration with cloud resources, and simplified management compared to traditional jump servers4.

The modern cloud-based Bastion host builds upon its on-premises predecessors by incorporating advanced technologies like SSL/TLS encryption, automated patching, and integration with cloud-native identity and access management systems.

This evolution has made Bastion hosts an indispensable component of secure cloud architecture, enabling organizations to maintain strict access controls while leveraging the flexibility and scalability of cloud environments.

Leveraging Azure Bastion: Enhancing Cloud Security and Compliance

Security Benefits of Azure Bastion: Fortifying Your Cloud Perimeter

Azure Bastion provides a robust set of security features that significantly enhance your cloud infrastructure's protection. By acting as a secure gateway, it eliminates the need to expose RDP and SSH ports directly to the internet, dramatically reducing the attack surface5.

Azure Bastion implements strong encryption for all remote connections, ensuring that data in transit remains confidential. Additionally, it supports Azure Active Directory integration, enabling multi-factor authentication and just-in-time access, further strengthening your security posture6.

Compliance Advantages: Meeting Regulatory Requirements with Azure Bastion

For organizations operating in regulated industries, Azure Bastion offers compliance advantages that are essential for maintaining data security and privacy.

It helps meet various compliance standards by providing detailed audit logs of all remote access sessions, aiding in forensic analysis and regulatory reporting.

The service's built-in security features align with best practices required by frameworks such as HIPAA, PCI DSS, and ISO 27001, simplifying the compliance process for cloud environments.

Simplified Access Management: Streamlining Secure Remote Connections

Azure Bastion eliminates the need for managing multiple VPN connections or distributing and maintaining SSH keys. With its browser-based console, users can securely access resources from any device without requiring additional client software, reducing administrative overhead and improving user experience.

Azure Bastion vs. Traditional Jump Servers: A Comparative Analysis

Key Differences: Cloud-Native Security vs. Legacy Approaches

While traditional jump servers and Azure Bastion serve similar purposes, there are key differences in their implementation and capabilities.

Traditional jump servers often require manual setup, patching, and maintenance, whereas Azure Bastion is a fully managed PaaS offering.

Azure Bastion also provides native integration with Azure services, offering a more seamless experience compared to standalone jump servers.

Advantages of Azure Bastion: Elevating Cloud Access Security

Azure Bastion offers several advantages over traditional jump servers:

  1. Automated patching and updates, ensuring the latest security measures are always in place
  2. Scalability to handle varying loads without manual intervention
  3. Native integration with Azure AD for enhanced identity management
  4. Built-in logging and monitoring capabilities for improved visibility
  5. No need for public IP addresses on your Azure VMs, enhancing security

Understanding Azure Bastion Architecture and Integration

Azure Bastion Architecture: A Deep Dive into Secure Design

Azure Bastion's architecture is designed for optimal security and performance. It deploys a hardened and managed instance within your Azure Virtual Network, which acts as the sole entry point for RDP and SSH traffic.

This architecture ensures that all remote desktop and SSH traffic is contained within your Azure environment, never exposed directly to the internet.

Effortless Integration with Azure Virtual Network

Azure Bastion integrates seamlessly with Azure Virtual Network, providing secure access to all VMs within the VNet without requiring public IP addresses.

It utilizes Azure's backbone network for connectivity, ensuring high-performance and low-latency connections7.

This integration allows for granular network security group rules, enabling you to precisely control which resources can be accessed through the Bastion host.

By leveraging Azure Bastion, organizations can achieve a higher level of security, compliance, and operational efficiency in their cloud environments.

Its cloud-native design and deep integration with Azure services make it a superior choice for secure remote access in modern cloud architectures.

Step-by-Step Guide: Creating an Azure Bastion Host

In the following sections, we'll use the power of Infrastructure as Code to efficiently produce and deploy an Azure Bastion in a Virtual Network. Specifically, we'll employ OpenTofu & Terragrunt to create a repeatable and consistent environment where the desired behavior will match that of the written codes in the TF files.

Prerequisites

For the purpose of this demo, we'll use the following stack. Feel free to alter as you see fit, or stick with the ones provided here:

  • Terragrunt v0.66: Used mainly for dependency management between stacks
  • OpenTofu v1.8: Used for declarative definition of the infrastructure
  • Azure CLI v2.63: Used in the last step for native SSH connection from the command line of the local machine.

Additionally, we'll use the Azure CLI authentication8 for our API requests to the Azure cloud. Make sure you have the required permissions to create the resources.

# If not already logged in
az login --use-device-code

# Specify your target Azure account
export ARM_SUBSCRIPTION_ID="00000000-0000-0000-0000-000000000000"
export ARM_TENANT_ID="00000000-0000-0000-0000-000000000000"

Architecture Overview

Having the required tools installed, here's a list of the stacks we'll create below:

  • vnet: Azure Virtual Network. Skip this step if you already have a VNet.
  • bastion: Azure Bastion Host. This is the main stack we'll focus on.
  • vm: Azure Virtual Machine. We'll use this to test the Bastion host.

Configuring the Azure Virtual Network

You can safely skip this step if you already have a VNet in your Azure cloud environment. For the sake of thoroughness, we'll create one from scratch.

vnet/versions.tf
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.116"
    }
  }
}

provider "azurerm" {
  features {}
}
vnet/naming.tf
module "naming" {
  source  = "Azure/naming/azurerm"
  version = "0.3.0"
}
vnet/main.tf
resource "azurerm_resource_group" "this" {
  name     = module.naming.resource_group.name_unique
  location = "Germany West Central"
}

resource "azurerm_virtual_network" "this" {
  name                = module.naming.virtual_network.name_unique
  resource_group_name = azurerm_resource_group.this.name
  location            = azurerm_resource_group.this.location
  address_space       = ["10.0.0.0/8"]
}

The following outputs will be used in other stacks when we define dependencies to help Terragrunt understand the order of execution:

vnet/outputs.tf
output "resource_group_name" {
  value = azurerm_resource_group.this.name
}

output "location" {
  value = azurerm_resource_group.this.location
}

output "virtual_network_name" {
  value = azurerm_virtual_network.this.name
}

The following Terragrunt HCL file is identical to being empty, yet we are specifying an empty input block for readability.

vnet/terragrunt.hcl
inputs = {
}

Applying this stack as well as the other following two stack is similar. Simply running the following three commands in the respective directories:

terragrunt init
terragrunt plan -out tfplan
terragrunt apply tfplan

Deploying Azure Bastion

Now that we have our VNet ready, we can proceed to create the Azure Bastion host. The following files will be used to create the Bastion host:

bastion/versions.tf
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.116"
    }
  }
}

provider "azurerm" {
  features {}
}
bastion/naming.tf
module "naming" {
  source  = "Azure/naming/azurerm"
  version = "0.3.0"
}
bastion/data.tf
data "azurerm_resource_group" "this" {
  name = var.resource_group_name
}

data "azurerm_virtual_network" "this" {
  name                = var.virtual_network_name
  resource_group_name = data.azurerm_resource_group.this.name
}
bastion/locals.tf
locals {
  vnet_cidr   = data.azurerm_virtual_network.this.address_space[0]
  subnet_cidr = (cidrsubnets(local.vnet_cidr, 8, 8, 8))[1]
}
bastion/variables.tf
variable "resource_group_name" {
  type     = string
  nullable = false
}

variable "virtual_network_name" {
  type     = string
  nullable = false
}
bastion/main.tf
resource "azurerm_subnet" "this" {
  # The following name has to be exactly as you see here!
  name                 = "AzureBastionSubnet"
  resource_group_name  = data.azurerm_resource_group.this.name
  virtual_network_name = data.azurerm_virtual_network.this.name
  address_prefixes     = [local.subnet_cidr]
}

resource "azurerm_public_ip" "this" {
  name                = module.naming.public_ip.name_unique
  location            = data.azurerm_resource_group.this.location
  resource_group_name = data.azurerm_resource_group.this.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_bastion_host" "this" {
  name                = module.naming.bastion_host.name_unique
  location            = data.azurerm_resource_group.this.location
  resource_group_name = data.azurerm_resource_group.this.name

  # Native Client i.e. ssh from the command line
  tunneling_enabled = true

  # Native Client requires at least `Standard` SKU
  sku = "Standard"


  ip_configuration {
    name                 = module.naming.firewall_ip_configuration.name_unique
    subnet_id            = azurerm_subnet.this.id
    public_ip_address_id = azurerm_public_ip.this.id
  }
}
bastion/outputs.tf
output "bastion_name" {
  value = azurerm_bastion_host.this.name
}

Notice how we are using the outputs from the vnet stack in our current bastion stack. The alternative is to use the terraform_remote_state data source9.

bastion/terragrunt.hcl
inputs = {
  resource_group_name  = dependency.vnet.outputs.resource_group_name
  virtual_network_name = dependency.vnet.outputs.virtual_network_name
}

dependency "vnet" {
  config_path = "../vnet"
}

Connecting to VMs using Azure Bastion

In a real-world scenario, you'd have a VM or a set of VMs that you'd like to connect to using the Azure Bastion host. It may also be a private endpoint10 to other Azure services such as Azure SQL Database11, Azure Key Vault12, etc.

Let us create a demo VM to test the Bastion host.

vm/versions.tf
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.116"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.6"
    }
    tls = {
      source  = "hashicorp/tls"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
}
vm/naming.tf
module "naming" {
  source  = "Azure/naming/azurerm"
  version = "0.3.0"
}
vm/data.tf
data "azurerm_resource_group" "this" {
  name = var.resource_group_name
}

data "azurerm_virtual_network" "this" {
  name                = var.virtual_network_name
  resource_group_name = data.azurerm_resource_group.this.name
}
vm/locals.tf
locals {
  vnet_cidr   = data.azurerm_virtual_network.this.address_space[0]
  subnet_cidr = (cidrsubnets(local.vnet_cidr, 8, 8, 8))[2]
}
vm/variables.tf
variable "resource_group_name" {
  type     = string
  nullable = false
}

variable "virtual_network_name" {
  type     = string
  nullable = false
}
vm/main.tf
resource "random_pet" "admin_username" {
  length    = 1

  keepers = {
    # regenerate if the VM has been recreated
    vm_name = module.naming.virtual_machine.name_unique
  }
}

resource "tls_private_key" "this" {
  algorithm = "RSA"
}

resource "azurerm_subnet" "this" {
  name                 = module.naming.subnet.name_unique
  resource_group_name  = data.azurerm_resource_group.this.name
  virtual_network_name = data.azurerm_virtual_network.this.name
  address_prefixes     = [local.subnet_cidr]
}

resource "azurerm_network_interface" "this" {
  name                = module.naming.network_interface.name_unique
  location            = data.azurerm_resource_group.this.location
  resource_group_name = data.azurerm_resource_group.this.name

  ip_configuration {
    name                          = module.naming.firewall_ip_configuration.name_unique
    subnet_id                     = azurerm_subnet.this.id
    private_ip_address_allocation = "Dynamic"
  }
}

resource "azurerm_ssh_public_key" "this" {
  name                = "vm-ssh-key"
  resource_group_name = data.azurerm_resource_group.this.name
  location            = data.azurerm_resource_group.this.location
  public_key          = tls_private_key.this.public_key_openssh
}

resource "azurerm_linux_virtual_machine" "this" {
  name                = module.naming.virtual_machine.name_unique
  resource_group_name = data.azurerm_resource_group.this.name
  location            = data.azurerm_resource_group.this.location
  size                = "Standard_B2pts_v2" # 2 ARM vCPUs, 1 GiB memory
  admin_username      = random_pet.admin_username.id
  network_interface_ids = [
    azurerm_network_interface.this.id,
  ]

  admin_ssh_key {
    username   = random_pet.admin_username.id
    public_key = azurerm_ssh_public_key.this.public_key
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  # Rocky Linux ARM64
  source_image_id = "/communityGalleries/rocky-dc1c6aa6-905b-4d9c-9577-63ccc28c482a/images/Rocky-9-aarch64/versions/9.4.20240509"
}
vm/outputs.tf
output "admin_username" {
  value = random_pet.admin_username.id
}

output "private_ip_address" {
  value = azurerm_network_interface.this.ip_configuration[0].private_ip_address
}

output "ssh_private_key" {
  value     = tls_private_key.this.private_key_pem
  sensitive = true
}

output "resource_group_name" {
  value = data.azurerm_resource_group.this.name
}

output "vm_id" {
  value = azurerm_linux_virtual_machine.this.id
}

Just as we had in the bastion stack, we're using the outputs from the vnet stack in our current vm stack using the dependency block provided by Terragrunt.

vm/terragrunt.hcl
inputs = {
  resource_group_name  = dependency.vnet.outputs.resource_group_name
  virtual_network_name = dependency.vnet.outputs.virtual_network_name
}

dependency "vnet" {
  config_path = "../vnet"
}

Native SSH Connection to Azure VM

Applying all the three stacks, we are now able to connect to the VM using either the Azure Portal or the Azure CLI. Unfortunately though, since the Azure Bastion Host is a PaaS offering, we can't use the ssh command directly from the command line13.

cd ./bastion
bastion_name=$(terragrunt output -raw bastion_name)

cd ../vm

rg=$(terragrunt output -raw resource_group_name)
vm_id=$(terragrunt output -raw vm_id)
admin_username=$(terragrunt output -raw admin_username)

terragrunt output -raw ssh_private_key > ~/.ssh/bastion
chmod 600 ~/.ssh/bastion

# if not already installed
az extension add -n bastion
az extension add -n ssh

az network bastion ssh \
  --name ${bastion_name} \
  --resource-group ${rg} \
  --target-resource-id ${vm_id} \
  --auth-type ssh-key \
  --username ${admin_username} \
  --ssh-key ~/.ssh/bastion

The final SSH command can also accept private IP addresses as you see below.

# `vm` directory
private_ip=$(terragrunt output -raw private_ip_address)

az network bastion ssh \
  --name ${bastion_name} \
  --resource-group ${rg} \
  --target-ip-address ${private_ip} \
  --auth-type ssh-key \
  --username ${admin_username} \
  --ssh-key ~/.ssh/bastion

SSH Tunneling with Azure Bastion

Another useful feature of Azure Bastion is the ability to create an SSH tunnel to your Azure VMs through the Bastion host. This can be particularly useful for accessing services running on the VM that are not exposed to the public internet13.

To create an SSH tunnel, you can use the following commands:

az network bastion tunnel \
  --name ${bastion_name} \
  --resource-group ${rg} \
  --target-resource-id ${vm_id} \
  --resource-port 22 \
  --port 50022


az network bastion tunnel \
  --name ${bastion_name} \
  --resource-group ${rg} \
  --target-ip-address ${private_ip} \
  --resource-port 22 \
  --port 50022

The distinction between resource port and local port is crucial to highlight:

  • resource-port: The port on the target resource (VM) that you want to connect to. In this case, it's the default SSH port 22, but it can also be a 5432 for a PostgreSQL database, 3306 for a MySQL database, etc.
  • port: The local port on your machine that will be used to establish the tunnel. You can choose any available port on your local machine. This will ultimately be the port you connect to with the address localhost:50022.

It may not look like it at first, but tunneling through the Bastion host can be a powerful tool for securely accessing your Azure VMs and services.

An example of a very common use case is to access a database that is only accessible through your internal VNet and not accessible from the outside world.

Azure AD Authentication

IMPORTANT NOTE: In these examples, we are using the native SSH capability and providing SSH private key for authentication to the final/target VM. Azure Bastion Host comes with suppot for Azure AD authentication as well, which is recommended for production environments.

Best Practices for Azure Bastion Implementation

Security Considerations: Protecting Your Azure Environment

When implementing Azure Bastion, security should be your top priority. Start by ensuring that your Azure Bastion subnet is named AzureBastionSubnet and has a minimum subnet mask of /2715.

Furthermore, implement Network Security Groups (NSGs) to control traffic flow, allowing only necessary inbound and outbound connections14.

On top of that, enable Azure Bastion's native logging features and integrate with Azure Monitor for comprehensive visibility into access patterns and potential security incidents.

Performance Optimization: Enhancing User Experience

To maximize Azure Bastion's performance, consider these optimization techniques:

  1. Place the Bastion host in the same region as your target VMs to reduce latency.
  2. Ensure your virtual network has sufficient bandwidth to handle expected traffic.
  3. Use Azure Bastion's Standard SKU for features like host scaling and IP-based connection.
  4. Implement Azure ExpressRoute for high-speed, private connections from on-premises networks.

Cost Management Tips: Minimizing Azure Bastion Expenses

While Azure Bastion provides significant security benefits, it's essential to manage costs effectively16:

  1. Choose the right SKU based on your usage patterns – Basic for smaller deployments, Standard for larger or more dynamic environments.
  2. Implement Azure Policy to ensure consistent, cost-effective Bastion deployments across your organization.
  3. Use Azure Cost Management tools to monitor and forecast Bastion-related expenses.
  4. Consider implementing auto-shutdown for non-production environments to reduce unnecessary runtime costs.

Considerations

As cloud environments continue to evolve, solutions like Azure Bastion will play an increasingly crucial role in maintaining robust, scalable, and secure infrastructures.

Remember, while Azure Bastion offers substantial benefits, it's important to align its implementation with your specific organizational needs and security policies.

Regularly review and update your Bastion configurations to ensure they continue to meet your evolving security requirements in the dynamic cloud landscape.

Conclusion

Azure Bastion stands as a powerful tool for enhancing cloud security and simplifying remote access management.

Let's recap the essential takeaways:

  1. Azure Bastion provides a secure, fully managed PaaS solution for accessing Azure VMs without exposing them directly to the internet.
  2. It offers significant advantages over traditional jump servers, including automated patching, scalability, and native Azure AD integration.
  3. Implementing Azure Bastion enhances compliance with various regulatory standards through detailed logging and built-in security features.
  4. The service seamlessly integrates with Azure Virtual Networks, offering high-performance, low-latency connections to your resources.
  5. Best practices for Azure Bastion implementation include proper subnet configuration, enabling logging, and optimizing for performance and cost.

By leveraging Azure Bastion, organizations can significantly improve their cloud security posture while streamlining access management.

If you enjoyed this blog post, consider sharing it with these buttons 👇. Please leave a comment for us at the end, we read & love 'em all. ❣

Share on Share on Share on Share on

Comments