Modern cloud infrastructure demands strict control over network access paths. The traditional method of exposing virtual machines with public IP addresses to the internet for administrative purposes creates a significant security liability. To mitigate this risk, organizations deploy Bastion Hosts, also known as jump boxes or management gateways. These specialized instances act as a single, hardened entry point for administrative traffic, allowing operators to reach private resources without exposing them to the public internet. With the rise of Infrastructure as Code (IaC), Terraform has become the industry standard for provisioning these secure environments. This article provides a deep technical analysis of deploying Bastion Hosts on both Microsoft Azure and Amazon Web Services (AWS) using Terraform. It covers the architectural prerequisites, specific resource configurations, module utilization, and the advanced features available in modern Bastion SKUs. By leveraging Terraform’s declarative approach, engineers can ensure consistent, auditable, and secure deployment of management infrastructure across hybrid and multi-cloud environments.
The Architecture of Azure Bastion
Azure Bastion is a managed service that is provisioned directly within a customer’s virtual network. Unlike traditional jump servers that require an operating system patch management cycle, Azure Bastion is a fully managed resource that handles the connection tunneling between the Azure Portal or native clients and the target virtual machines. When deployed automatically via Terraform, the service operates with specific architectural constraints that must be respected to ensure successful deployment and operation.
The core components of an Azure Bastion deployment include a resource group, a virtual network, a dedicated subnet, and a public IP address. The virtual network serves as the isolation boundary, typically defined with a private address space such as 10.0.0.0/16. Within this network, a specific subnet is required for the Bastion service to operate. This subnet is not arbitrary; it has strict naming and sizing requirements to allow the Azure backend services to provision the necessary network functions.
One of the most critical aspects of Azure Bastion architecture is the SKU tier selection. Azure offers multiple tiers to accommodate different security and functional requirements:
| SKU Tier | Key Features | Use Case |
|---|---|---|
| Developer | Free shared deployment; browser-based RDP/SSH only | Development and testing in supported regions |
| Basic | Browser-based RDP/SSH; fixed capacity | Cost-effective production access |
| Standard | Native client support; IP-based connections; file transfers; shareable links; custom ports | Enterprise production environments |
| Premium | Session recording; private-only deployment (no public IP) | Highly regulated environments requiring audit trails |
When deploying via Terraform, if the sku attribute is not explicitly defined, the service defaults to the Standard SKU. This default behavior is advantageous for most production scenarios because it enables native client support, which is essential for complex remote administration tasks that browser-based interfaces may struggle to handle, such as specific keyboard layouts or advanced input methods. The Standard SKU also supports custom ports, allowing administrators to reach services other than the standard RDP (3389) and SSH (22) ports.
The traffic flow in an Azure Bastion environment is strictly controlled. The user’s browser or client connects to the Bastion public endpoint over HTTPS on port 443. The Bastion service then establishes the RDP or SSH session to the target virtual machine over the private network. Consequently, the target virtual machines do not require public IP addresses. This design significantly reduces the attack surface by eliminating direct public ingress to the virtual machines. All management traffic is routed through the managed jump box, ensuring that access is governed by the Bastion service’s security controls and the network security groups applied to the virtual network.
Terraform Configuration for Azure
Terraform enables the definition, preview, and deployment of cloud infrastructure using High-Level Language (HCL) syntax. For Azure Bastion, the configuration involves defining the provider version, establishing the resource group, creating the virtual network, allocating the public IP, and finally declaring the Bastion host resource.
The provider configuration must specify the azurerm provider from the hashicorp source. A recommended version constraint is ~> 3.80 to ensure access to the latest features while maintaining stability. The features {} block is required in the provider definition to enable the necessary Azure features for resource management.
The following code block illustrates the foundational resources required for an Azure Bastion deployment. Note the specific naming convention for the subnet, which is mandatory for the Bastion service to recognize and utilize the network segment.
```terraform
terraform {
requiredversion = ">= 1.3.0"
requiredproviders {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.80"
}
}
}
provider "azurerm" {
features {}
}
resource "azurermresourcegroup" "bastion" {
name = "rg-bastion-prod"
location = "eastus"
tags = {
Environment = "Production"
ManagedBy = "Terraform"
}
}
resource "azurermvirtualnetwork" "main" {
name = "vnet-prod-eastus-001"
location = azurermresourcegroup.bastion.location
resourcegroupname = azurermresourcegroup.bastion.name
address_space = ["10.0.0.0/16"]
tags = {
Environment = "Production"
}
}
```
The subnet resource must be named exactly AzureBastionSubnet. Additionally, the subnet requires a minimum size of /26 to accommodate the private IP addresses that the Bastion service requires for its internal operations. Following the creation of the network resources, a static standard public IP address is allocated. This IP is not assigned directly to a virtual machine but to the Bastion host resource.
```terraform
resource "azurermsubnet" "bastion" {
name = "AzureBastionSubnet"
resourcegroupname = azurermresourcegroup.bastion.name
virtualnetworkname = azurermvirtualnetwork.main.name
addressprefixes = ["10.0.0.128/26"]
}
resource "azurermpublicip" "bastion" {
name = "ip-bastion-prod"
location = azurermresourcegroup.bastion.location
resourcegroupname = azurermresourcegroup.bastion.name
allocation_method = "Static"
sku = "Standard"
}
```
Finally, the azurerm_bastion_host resource ties these components together. It references the resource group, the virtual network subnet ID, and the public IP ID. The sku parameter can be explicitly set to Standard or Premium if the default Standard behavior needs to be overridden or if session recording is required.
```terraform
resource "azurermbastionhost" "example" {
name = "bastion-prod-eastus-001"
location = azurermresourcegroup.bastion.location
resourcegroupname = azurermresourcegroup.bastion.name
ipconfiguration {
name = "ipconfig-001"
subnetid = azurermsubnet.bastion.id
publicipaddressid = azurermpublicip.bastion.id
}
tags = {
Environment = "Production"
}
}
```
Advanced Azure Deployment Patterns
While the basic deployment ensures connectivity, production environments often require additional security and operational features. Terraform modules, such as those found in the terraform-azurerm-examples repository, provide a robust framework for these advanced scenarios. These examples often include the deployment of Linux and Windows virtual machines with Azure Active Directory (AAD) extensions configured.
In these advanced setups, the focus shifts from simple connectivity to identity-driven access. The Terraform configuration may include key vaults to store generated credentials, such as Windows administrator passwords or private SSH keys. These credentials are intended for break-glass scenarios, meaning they are only used if AAD authentication fails. For daily operations, all access should be via AAD authentication, which integrates with the organization’s existing identity provider. This approach eliminates the need to manage password rotation for individual virtual machines and provides centralized audit logging of access attempts.
The Terraform outputs in these configurations typically provide the commands necessary to connect using native SSH and RDP clients. This is particularly useful for the Standard and Premium SKUs, which support native clients. The outputs might include the Bastion host name, the virtual machine names, and the specific authentication parameters required for the client connections.
AWS Bastion Host Implementation with Terraform
While Azure Bastion is a fully managed service, AWS utilizes a different approach with the Bastion Host pattern, often implemented using the EC2 Instance Connect or a dedicated EC2 instance configured as a jump box. Terraform modules in the AWS registry facilitate this by creating a complete Virtual Private Cloud (VPC) environment that supports secure access to private resources.
The AWS Bastion implementation typically involves the creation of a VPC with multiple subnets: public subnets for the Bastion and NAT Gateway, private application subnets for compute resources, and private database subnets for data stores. The NAT Gateway allows instances in private subnets to initiate outbound connections to the internet without exposing inbound ports.
A verified Terraform module for AWS Bastion hosts handles the provisioning of the EC2 instance, the assignment of an Elastic IP, and the configuration of security groups. The security group is critical, allowing SSH traffic only from specific IP ranges or through the Bastion host itself. One of the technical challenges in AWS Terraform deployments is managing resource dependencies. The creation of the Bastion host may depend on the security group being fully applied, and the provisioning of SSH keys on the Bastion may depend on the instance being in a "running" state.
Terraform handles these dependencies through the depends_on meta-argument and explicit references. For example, if using a null_resource to provision SSH keys, it must reference the instance_id of the Bastion EC2 instance. The null_resource can utilize file and remote-exec provisioners to copy the key pair and configure the authorized_keys file. This ensures that the Bashion host is fully configured and accessible before the Terraform apply process completes.
The following is a conceptual representation of the dependency chain in an AWS Bastion deployment:
```terraform
resource "awsinstance" "bastion" {
ami = "ami-0c55b159cbfafe1f0"
instancetype = "t3.micro"
keyname = awskeypair.demo.keyname
vpcsecuritygroupids = [awssecuritygroup.bastion.id]
subnetid = aws_subnet.public[0].id
associatepublicip_address = true
tags = {
Name = "BastionHost"
}
}
resource "nullresource" "copykey" {
dependson = [awsinstance.bastion]
connection {
type = "ssh"
host = awsinstance.bastion.publicip
user = "ec2-user"
private_key = file("key.pem")
}
provisioner "file" {
source = "key.pem"
destination = "/home/ec2-user/.ssh/authorized_keys"
}
}
```
In this scenario, the null_resource ensures that the SSH key is correctly placed on the Bastion host. This is particularly important if the key pair was generated outside of Terraform or if the instance image does not have the expected key format. The remote-exec provisioner can also be used to run commands, such as sudo yum update -y or apt-get upgrade, to ensure the operating system is patched immediately after creation.
Comparative Analysis and Best Practices
The choice between Azure Bastion and an AWS EC2-based Bastion host often comes down to operational preference and organizational strategy. Azure Bastion offers a managed service with built-in security features, such as session recording in the Premium SKU, which is highly valuable for compliance. AWS provides more flexibility in terms of operating system selection and customization but requires more manual effort for patching and security hardening.
When deploying either service with Terraform, several best practices should be adhered to:
- Use separate resource groups or VPCs for Bastion infrastructure to isolate management traffic.
- Implement Network Security Groups (NSGs) or Security Groups that restrict inbound traffic to only the required administrative ports.
- Enable diagnostic settings to log connections and access attempts.
- Use Terraform variables to parameterize region, environment, and network ranges to facilitate reuse across environments.
- Configure scaling policies where applicable, although Bastion hosts are typically single-instance services.
The elimination of public IPs on virtual machines and the routing of all management traffic through a managed jump box significantly reduce the attack surface. Terraform makes it straightforward to deploy Bastion consistently across environments, ensuring that the right NSG rules, diagnostic settings, and scaling configuration are in place from day one. This consistency is crucial for maintaining a secure and auditable infrastructure in cloud-native environments.
Conclusion
The deployment of Bastion Hosts using Terraform represents a critical component of modern cloud security architecture. Whether utilizing the fully managed Azure Bastion service or building a custom EC2-based solution on AWS, the principles of network isolation and controlled access remain paramount. Terraform’s ability to define infrastructure declaratively allows for the precise configuration of virtual networks, subnets, and security groups required for Bastion operations. For Azure, understanding the SKU tiers and their capabilities is essential for selecting the right balance of features and cost. The Standard SKU’s support for native clients and custom ports makes it the default choice for production, while the Premium SKU adds an extra layer of compliance with session recording. For AWS, the use of Terraform modules to manage the complex dependency chains of VPCs, NAT Gateways, and EC2 instances ensures that the Bastion host is deployed with the correct security configurations and SSH key provisioning. By eliminating public IP exposure on target virtual machines and centralizing administrative access through a hardened, managed entry point, organizations can significantly mitigate the risk of unauthorized access. The integration of Terraform into these workflows not only automates the deployment process but also provides a version-controlled, auditable trail of infrastructure changes, making it an indispensable tool for DevOps and security teams alike.