Reusable Terraform module for a minimal SSH jump host — an EC2 instance in a public subnet, intended purely as a network hop into private resources (RDS, private-subnet services, etc.).
The module is opinionated on security posture: SSH ingress is restricted by caller-supplied CIDR (no public-internet default), IMDSv2 is required, the root volume is encrypted, and the instance is granted the SSM Session Manager managed policy as a break-glass fallback.
module "bastion" {
source = "git::https://github.com/synapsestudios/terraform-aws-bastion-server.git?ref=v4.1.0"
namespace = "prod-myapp"
environment = "prod"
vpc_id = module.vpc.vpc_id
subnet_id = module.vpc.public_subnets[0]
# Required — module does not default to 0.0.0.0/0.
allowed_cidr_blocks = ["203.0.113.42/32"]
# Optional — pin AMI to avoid plan-time replacement every time
# Amazon publishes a new AL2023 image. When omitted the module resolves
# the latest AL2023 via a data source.
ami_id = "ami-0123456789abcdef0"
tags = { ApplicationName = "myapp" }
}
# Cross-resource rules (e.g. letting the bastion reach a database SG) are
# the composing root module's responsibility. Reference `module.bastion.security_group_id`
# and attach them where both sides of the relationship are visible.
resource "aws_vpc_security_group_ingress_rule" "bastion_to_db" {
security_group_id = module.aurora.security_group_id
referenced_security_group_id = module.bastion.security_group_id
from_port = 5432
to_port = 5432
ip_protocol = "tcp"
description = "PostgreSQL access from bastion"
}Retrieve the SSH key from Secrets Manager, then either SSH directly or open a tunnel:
aws secretsmanager get-secret-value \
--secret-id "$(terraform output -raw ssh_key_name)" \
--query SecretString --output text > ~/.ssh/bastion.pem
chmod 600 ~/.ssh/bastion.pem
# Tunnel for a Postgres client talking to a private-subnet Aurora cluster:
ssh -i ~/.ssh/bastion.pem \
-L 5432:<aurora-endpoint>:5432 \
ec2-user@$(terraform output -raw public_ip)For emergency access when the SSH key is unavailable, the instance is enrolled in SSM Session Manager — aws ssm start-session --target <instance-id> works without opening port 22.
Tests follow the Synapse Terraform Testing guide and use the native terraform test framework. The -test-directory flag is required on init so Terraform discovers modules referenced from test files in non-default directories.
| Lane | Path | Cost | When it runs |
|---|---|---|---|
| Unit | tests/unit/ |
Free — mock_provider |
On every PR |
| Integration | tests/integration/ |
Real AWS | Manual dispatch only |
Per the Synapse doc, E2E and Environment-E2E levels apply to root-module repos (deployable configs composing multiple modules); this repo is a single reusable module, so it has no E2E lane. The integration lane covers the full real-AWS path including SSH reachability.
# Unit — no AWS credentials required
terraform init -test-directory=tests/unit
terraform test -test-directory=tests/unit
# Integration — requires AWS credentials
terraform init -test-directory=tests/integration
terraform test -test-directory=tests/integrationThe shared tests/setup/ helper module provisions a minimal VPC + public subnet consumed by the integration lane via run block composition. The tests/integration/ssh-check/ helper module wraps the null_resource + remote-exec SSH reachability probe.
| Name | Version |
|---|---|
| terraform | >= 1.6 |
| aws | ~> 5.0 |
| null | ~> 3.0 |
| tls | ~> 4.0 |
| Name | Version |
|---|---|
| aws | ~> 5.0 |
| tls | ~> 4.0 |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| allowed_cidr_blocks | CIDR blocks permitted to reach the bastion on port 22. | list(string) |
n/a | yes |
| environment | Deployment environment tag value (e.g. dev, uat, prod, shared). Applied to every resource created by this module. | string |
n/a | yes |
| namespace | Determines naming convention of assets. Generally follows DNS naming convention. | string |
n/a | yes |
| subnet_id | ID of subnet to deploy bastion server on. | string |
n/a | yes |
| tags | A mapping of tags to assign to the AWS resources. | map(string) |
n/a | yes |
| vpc_id | ID of the VPC to deploy bastion server on. | string |
n/a | yes |
| ami_id | (Optional) Explicit AMI ID to launch the bastion with. When null, the latest Amazon Linux 2023 AMI is used. | string |
null |
no |
| instance_type | (Optional) EC2 Instance type to provision. | string |
"t3.micro" |
no |
| volume_size | (Optional) The size of the volume in gibibytes. | number |
30 |
no |
| volume_type | (Optional) The type of volume. Can be 'standard', 'gp2', 'gp3', 'io1', 'sc1', or 'st1'. | string |
"gp3" |
no |
| Name | Description |
|---|---|
| instance_id | EC2 instance ID of the bastion server |
| public_ip | The public IP address associated with this Bastion server |
| security_group_id | Security group ID of the bastion |
| ssh_key_name | The name of the Secrets Manager secret containing the bastion's SSH private key |
| ssh_key_secret_arn | ARN of the Secrets Manager secret containing the bastion's SSH private key |
| ssh_private_key_pem | The bastion's SSH private key in PEM format. Sensitive. |