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:
- Automated patching and updates, ensuring the latest security measures are always in place
- Scalability to handle varying loads without manual intervention
- Native integration with Azure AD for enhanced identity management
- Built-in logging and monitoring capabilities for improved visibility
- 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.
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.116"
}
}
}
provider "azurerm" {
features {}
}
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:
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.
Applying this stack as well as the other following two stack is similar. Simply running the following three commands in the respective directories:
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:
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.116"
}
}
}
provider "azurerm" {
features {}
}
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
}
locals {
vnet_cidr = data.azurerm_virtual_network.this.address_space[0]
subnet_cidr = (cidrsubnets(local.vnet_cidr, 8, 8, 8))[1]
}
variable "resource_group_name" {
type = string
nullable = false
}
variable "virtual_network_name" {
type = string
nullable = false
}
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
}
}
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.
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.
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 {}
}
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
}
locals {
vnet_cidr = data.azurerm_virtual_network.this.address_space[0]
subnet_cidr = (cidrsubnets(local.vnet_cidr, 8, 8, 8))[2]
}
variable "resource_group_name" {
type = string
nullable = false
}
variable "virtual_network_name" {
type = string
nullable = false
}
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"
}
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.
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 addresslocalhost: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 /27
15.
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:
- Place the Bastion host in the same region as your target VMs to reduce latency.
- Ensure your virtual network has sufficient bandwidth to handle expected traffic.
- Use Azure Bastion's
Standard
SKU for features like host scaling and IP-based connection. - 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:
- Choose the right SKU based on your usage patterns –
Basic
for smaller deployments, Standard for larger or more dynamic environments. - Implement Azure Policy to ensure consistent, cost-effective Bastion deployments across your organization.
- Use Azure Cost Management tools to monitor and forecast Bastion-related expenses.
- 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:
- Azure Bastion provides a secure, fully managed PaaS solution for accessing Azure VMs without exposing them directly to the internet.
- It offers significant advantages over traditional jump servers, including automated patching, scalability, and native Azure AD integration.
- Implementing Azure Bastion enhances compliance with various regulatory standards through detailed logging and built-in security features.
- The service seamlessly integrates with Azure Virtual Networks, offering high-performance, low-latency connections to your resources.
- 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
-
https://docs.microsoft.com/en-us/azure/compliance/offerings/ ↩
-
https://learn.microsoft.com/en-us/azure/bastion/bastion-overview ↩
-
https://learn.microsoft.com/en-us/azure/bastion/bastion-faq ↩
-
https://docs.microsoft.com/en-us/azure/bastion/bastion-overview ↩
-
https://learn.microsoft.com/en-us/azure/networking/fundamentals/networking-overview ↩
-
https://learn.microsoft.com/en-us/cli/azure/authenticate-azure-cli ↩
-
https://developer.hashicorp.com/terraform/language/state/remote-state-data ↩
-
https://learn.microsoft.com/en-us/azure/private-link/private-endpoint-overview ↩
-
https://learn.microsoft.com/en-us/azure/azure-sql/database/sql-database-paas-overview?view=azuresql ↩
-
https://learn.microsoft.com/en-us/azure/key-vault/general/basic-concepts ↩
-
https://learn.microsoft.com/en-us/azure/bastion/native-client ↩↩
-
https://docs.microsoft.com/en-us/azure/bastion/bastion-nsg ↩
-
https://docs.microsoft.com/en-us/azure/security/fundamentals/best-practices-and-patterns ↩
-
https://azure.microsoft.com/en-us/pricing/details/azure-bastion/ ↩