In the modern cloud-native landscape, the perimeter is no longer a static boundary; it is a fluid concept defined by identity, encryption, and micro-segmentation. One of the most persistent challenges for DevOps engineers and Site Reliability Engineers is managing secure administrative access to private compute resources without exposing them to the open internet. The bastion host, often referred to as a jump host, remains a critical architectural pattern for this purpose. However, manually managing a bastion host through console clicks or ad-hoc shell scripts is an anti-pattern that introduces significant risk, inconsistency, and operational debt. By leveraging Infrastructure as Code (IaC) with Terraform, organizations can enforce deterministic, auditable, and scalable bastion deployments. This analysis explores the architectural, security, and implementation details of provisioning bastion hosts on major cloud providers—Amazon Web Services (AWS), Microsoft Azure, and Google Cloud Platform (GCP)—using Terraform modules. The focus is on eliminating human error, ensuring compliance, and creating a reproducible foundation for secure remote access.
The core value of a bastion host in any cloud environment is the reduction of the attack surface. By restricting SSH or RDP traffic to a single, hardened, and monitored entry point, organizations can enforce strict access controls, centralize logging, and simplify network security group configurations. While cloud providers have introduced managed solutions such as Azure Bastion and AWS SSM Session Manager, self-managed bastion hosts via Terraform remain essential for legacy systems, specific compliance requirements, or scenarios where direct OS-level access is required. This article dissects the implementation details, providing expert-level guidance on variable management, module composition, networking configurations, and cross-platform best practices.
The Security Imperative and Architectural Rationale
Before diving into code, it is critical to understand why a bastion host is a standard requirement for production-grade infrastructure. Exposing individual EC2 instances or VMs directly to the public internet with open SSH or RDP ports is a primary vector for automated scanning and brute-force attacks. A bastion host acts as a gatekeeper. It is a lightweight, typically ephemeral, instance that runs a hardened operating system with minimal services. It is the only point in the network where port 22 (SSH) or port 3389 (RDP) is exposed to the public internet, and even then, it is ideally restricted to specific corporate IP ranges or IPsec-protected VPN endpoints.
The benefits of this architecture are multifaceted. First, security is enhanced because the attack surface is reduced to a single monitored endpoint. Second, simplicity is achieved by avoiding the need to configure and manage security groups or firewall rules on every private instance. Third, auditability is improved because all access attempts are funneled through one point, making log aggregation and compliance reporting significantly easier. Finally, this model aligns with compliance frameworks that require strict separation of administrative duties and network segmentation.
In a Terraform context, the goal is to automate this setup without introducing complexity. The infrastructure must be defined in a way that separates concerns: networking, compute, security, and access control should be modularized. This allows teams to swap out providers, update instance types, or modify network topologies without rewriting core logic. The following sections detail how this is achieved in AWS, Azure, and GCP.
AWS Bastion Host Implementation with Terraform Modules
Building a secure AWS bastion host involves more than just launching an EC2 instance. It requires a comprehensive VPC configuration that includes public and private subnets, a NAT Gateway for outbound internet access from private subnets, and meticulously configured Security Groups. Using Terraform modules from the Terraform Registry is the recommended approach for maintaining consistency and security.
VPC and Subnet Architecture
The foundation of any bastion setup is the Virtual Private Cloud (VPC). In a standard production architecture, the VPC is divided into public subnets and private subnets. The bastion host resides in a public subnet to receive traffic, while application and database servers reside in private subnets. To ensure that private instances can reach the internet for patching and updates without exposing themselves directly, a NAT Gateway is deployed in a public subnet.
Terraform allows for dynamic Availability Zone (AZ) selection, ensuring high availability. By using data sources to fetch available subnets and AZs, the infrastructure can be resilient against localized failures. The VPC module typically exposes outputs for public subnet IDs, private subnet IDs, and the NAT Gateway ID, which are then consumed by the bastion module.
Security Groups and Network ACLs
Security Groups in AWS act as stateful firewalls at the instance level. For a bastion host, the ingress rules must be tightly restricted. Ideally, inbound traffic on port 22 should only be allowed from known corporate IP addresses. While it is possible to restrict this at the Security Group level, it is often better practice to use Network ACLs (Network Access Control Lists) at the subnet level for a second layer of defense.
The Terraform configuration for the bastion's security group is critical. It must allow inbound SSH from the designated CIDR block and allow outbound traffic to the private subnets on port 22. Conversely, the private subnets' security groups should allow inbound SSH only from the bastion's security group or its specific IP address. This creates a chained access model.
Instance Configuration and AMI Selection
The choice of the Amazon Machine Image (AMI) and instance type is vital. For a bastion host, a lightweight instance type such as t3.micro or t3.small is sufficient. These instances are cost-effective and provide enough CPU and memory for SSH forwarding and basic administration. The AMI should be a hardened version of Amazon Linux 2 or Ubuntu, with automatic updates enabled.
Instead of hardcoding AMI IDs, which change frequently and are region-specific, Terraform's aws_ami data source should be used. This data source dynamically fetches the latest AMI based on criteria such as owner ID, name, and state. This ensures that the bastion host is always running a patched and up-to-date operating system.
Elastic IP and Key Pair Management
To ensure a stable IP address for the bastion host, an Elastic IP (EIP) should be associated with the instance. This prevents IP churn from affecting access, especially if the instance is restarted or replaced. The EIP is allocated in the VPC and attached to the EC2 instance via Terraform.
SSH key management is another critical aspect. While the private key is typically managed outside of Terraform (e.g., in a secret manager or manually distributed), the public key must be part of the EC2 configuration. Terraform provisioners can be used to manage this. Specifically, the null_resource resource can be used in conjunction with the file and remote-exec provisioners to copy the private key to the local machine or a remote location for educational or operational purposes. This is a common pattern for automated setup scripts.
Terraform Code Example: AWS Bastion
The following code snippet illustrates a basic Terraform configuration for an AWS bastion host, focusing on the EC2 instance and its security group.
```terraform
resource "awsinstance" "bastion" {
ami = data.awsami.amazonlinux.id
instancetype = "t3.micro"
keyname = var.bastionkeyname
subnetid = module.vpc.publicsubnetids[0]
vpcsecuritygroupids = [awssecuritygroup.bastionsg.id]
associatepublicip_address = true
rootblockdevice {
volumesize = 8
volumetype = "gp2"
}
tags = {
Name = "bastion-host"
}
}
resource "awssecuritygroup" "bastionsg" {
name = "bastion-sg"
vpcid = module.vpc.vpc_id
ingress {
description = "SSH from corporate network"
fromport = 22
toport = 22
protocol = "tcp"
cidrblocks = var.allowedssh_cidrs
}
egress {
fromport = 0
toport = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
```
Azure Bastion: Managed vs. Self-Hosted
Microsoft Azure offers two distinct paths for bastion access: the Azure Bastion service and self-managed VMs. Azure Bastion is a fully managed service that provides browser-based RDP and SSH access to VMs within the Azure portal. It eliminates the need for a public IP address on the VM and requires no additional networking components on the client side.
Azure Bastion Deployment with Terraform
Deploying Azure Bastion using Terraform is straightforward and leverages the azurerm provider. The service is provisioned directly in the virtual network, specifically in a dedicated subnet named AzureBastionSubnet. This subnet must have a minimum size of /27. The deployment creates the necessary resources, including the resource group, virtual network, Bastion subnet, and public IP.
The Azure Bastion service operates by establishing a secure tunnel from the browser to the VM. Traffic flows from the user's browser to the Bastion public endpoint over HTTPS (port 443), and then from Bastion to the target VM over the private network. The VM only needs a private IP address, significantly reducing its exposure.
Terraform 1.3+ is required for the latest azurerm provider versions. The configuration requires an Azure subscription with Contributor access and an authenticated Azure CLI. The Standard SKU of Azure Bastion adds native client support, IP-based connections, file transfers, shareable links, and custom port support, making it suitable for production environments.
```terraform
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"
}
}
```
SKU Options and Considerations
Azure Bastion comes in four SKU tiers, each offering different features and pricing models:
| SKU Tier | Key Features | Use Case |
|---|---|---|
| Developer | Free shared deployment, browser-based access | Development and testing in supported regions |
| Basic | Browser-based RDP/SSH, fixed capacity | Basic production access |
| Standard | Native client support, IP-based connections, file transfers, shareable links, custom ports | Production environments requiring advanced connectivity |
| Premium | Session recording, private-only deployment | High-security environments requiring audit logs and isolation |
When deploying with Terraform, the choice of SKU impacts the configuration. The Standard SKU is the default for automated deployments in many scenarios, but specific configurations may require adjusting the SKU based on organizational needs.
GCP Bastion Host with Identity-Aware Proxy (IAP)
On Google Cloud Platform, the approach to bastion hosts often involves integrating with Identity-Aware Proxy (IAP). IAP allows users to access internal applications without a public IP address. For bastion hosts, this means using IAP to protect the SSH port, combined with OS Login for user authentication.
Terraform Google Bastion Host Module
The terraform-google-modules/bastion-host/google module automates the creation of a dedicated service account, a GCE instance for the bastion, and firewall rules to allow TCP:22 SSH access from IAP. It also sets up necessary IAM bindings to allow IAP and OS Logins from specified members.
This module requires several APIs to be enabled in the project:
- Google Cloud Storage JSON API (storage-api.googleapis.com)
- Compute Engine API (compute.googleapis.com)
- Cloud Identity-Aware Proxy API (iap.googleapis.com)
- OS Login API (oslogin.googleapis.com)
The module only sets up permissions for the bastion service account; users who need access must be added via IAM bindings. This separation of duties ensures that the infrastructure is provisioned securely, while access control is managed through identity providers.
terraform
module "iap_bastion" {
source = "terraform-google-modules/bastion-host/google"
project = var.project
zone = var.zone
network = google_compute_network.net.self_link
subnet = google_compute_subnetwork.net.self_link
members = [
"group:[email protected]",
"user:[email protected]",
]
}
Best Practices for Terraform Bastion Deployment
Regardless of the cloud provider, several best practices should be adhered to when deploying bastion hosts with Terraform.
Variable and Parameter Management
Hardcoding values in Terraform files is a common mistake that leads to maintenance nightmares. Instead, use a variables.tf file to define all parameters, including instance types, regions, tags, and network CIDRs. Use .tfvars files for environment-specific values. This separation allows the same module to be deployed to different environments with different configurations.
Module Reusability
Create reusable modules for common components such as VPCs, security groups, and bastion instances. This promotes consistency and reduces the chance of errors. For AWS, use verified modules from the Terraform Registry. For Azure and GCP, consider creating internal modules or using community modules that are regularly updated.
Provisioners and Key Management
Terraform provisioners can be used to automate post-deployment tasks. For bastion hosts, this often involves copying SSH keys, installing monitoring agents, or configuring firewall rules. Use the null_resource with file and remote-exec provisioners to manage these tasks. Ensure that private keys are stored securely and not committed to version control.
Monitoring and Logging
A bastion host without monitoring is a blind spot. Integrate the bastion host with cloud-native monitoring and logging services. For AWS, use CloudWatch Logs and VPC Flow Logs. For Azure, use Azure Monitor and Log Analytics. For GCP, use Cloud Logging and VPC Flow Logs. This ensures that any suspicious activity is detected and alerted.
Conclusion
The deployment of bastion hosts using Terraform is a cornerstone of secure cloud infrastructure management. By automating the provisioning of network components, security groups, and compute instances, organizations can ensure that their access patterns are consistent, auditable, and scalable. The architectural differences between AWS, Azure, and GCP require different implementation strategies, but the underlying principles remain the same: minimize exposure, enforce strict access controls, and leverage the power of IaC.
AWS offers a flexible, self-managed approach with detailed control over VPC and security groups. Azure provides a managed service with browser-based access and advanced SKU options. GCP integrates tightly with identity-aware proxy for seamless access management. By understanding these nuances and implementing them through Terraform modules, DevOps teams can create a robust foundation for secure remote access. The key to success lies in modularity, dynamic configuration, and continuous monitoring. As cloud environments evolve, the bastion host will continue to adapt, but its role as a secure gateway to private resources will remain indispensable.