What is a Bastion Host and why do you need one?
If you run any kind of production infrastructure, you eventually face a problem: you need your servers to be private, but you still need to be able to log into them.
The naive solution is to give every server a public IP address and keep port 22 open to the world. That works, but it means every one of your machines is constantly being scanned, probed, and hammered by automated attacks. Each server is its own attack surface.
A bastion host, also called a jump server, solves this by collapsing all of that surface down to a single point.
How a bastion host works
The concept is simple. You put all your sensitive servers (databases, application backends, internal APIs) inside a private network with no public IP addresses. Those machines cannot be reached directly from the internet. Then you create one dedicated server, the bastion host, that does have a public IP address. This is the only machine exposed to the internet.
When you need to access an internal server, you SSH into the bastion first. From there, you SSH into the internal machine. The bastion acts as a controlled choke point. Anyone trying to reach your database has to get through it first, and it is the one machine you harden aggressively.
The term comes from military fortification architecture. A bastion is a projecting section of a wall specifically designed to let defenders cover adjacent areas. The analogy is direct: one hardened point you monitor carefully, rather than trying to defend ten.
Setting up a bastion host on AWS
On AWS, the typical setup involves a VPC (Virtual Private Cloud) with two subnets: a public subnet for the bastion and a private subnet for everything else.
- Launch an EC2 instance in your public subnet. A t3.micro is plenty for a bastion host. Use Amazon Linux 2 or Ubuntu.
- Assign it an Elastic IP so the address doesn't change on restart.
- Configure a Security Group that allows SSH (TCP port 22) inbound from your IP address only. Not
0.0.0.0/0. Your IP only. - Launch your application servers in the private subnet. Configure their Security Groups to allow port 22 inbound from the bastion's Security Group (not a specific IP, but the Security Group ID).
- The private servers can have no public IP. They route outbound internet traffic through a NAT Gateway.
To SSH into a private instance through the bastion:
ssh -J ubuntu@bastion-public-ip ubuntu@10.0.1.25
The -J flag tells SSH to use the first address as a jump host. The connection tunnels through the bastion automatically.
Setting up a bastion host on Google Cloud
On GCP, the pattern is similar but uses VPC firewall rules instead of Security Groups.
- Create a VM in a subnet with an external IP. This is your bastion.
- Add a VPC firewall rule allowing SSH ingress from your IP to the bastion (target tag:
bastion). - Create your private VMs without external IPs.
- Add a VPC firewall rule allowing SSH ingress from the bastion's internal IP range to private instances (source IP range: the bastion's internal IP, or use a tag).
GCP also provides Identity-Aware Proxy (IAP), which lets you SSH into instances through Google's infrastructure without any public IPs at all, including the bastion. IAP tunnels the connection through Google's HTTPS endpoints, which eliminates the exposed port 22 entirely.
gcloud compute ssh --tunnel-through-iap my-private-instance --project=my-project --zone=us-central1-a
Hardening the bastion host
Because the bastion is the single entry point, it becomes a high-value target. Anything an attacker does to compromise it gives them access to everything behind it. Harden it thoroughly.
Disable password authentication. Only allow SSH key-based access. See our guide on locking down your SSH server for the exact config changes.
Restrict access by IP. The Security Group or firewall rule for your bastion should only allow port 22 from specific, known IP ranges. If your team uses a fixed office IP or a VPN, restrict access to that range. If your IP changes, update the rule. Do not leave it open to the world.
Enable SSH logging and audit it. Every connection through the bastion should be logged. On Ubuntu/Debian, this is /var/log/auth.log. Set up log shipping to a SIEM or CloudWatch if this is production infrastructure.
Use a minimal OS image. The bastion host should not run any services other than SSH. No web server, no database, no application code. Less running software means less attack surface.
Consider SSM instead of SSH for AWS. AWS Systems Manager Session Manager lets you connect to EC2 instances without opening any SSH ports at all. Access goes through AWS's control plane, and you do not need a bastion or a public IP. It is worth evaluating for new environments.
Automating the jump with SSH config
The annoyance of two-hop SSH is real, but you can automate it entirely with your local ~/.ssh/config file. Once configured, you type one command and it handles the tunneling in the background.
Create or edit ~/.ssh/config:
Host bastion
HostName 203.0.113.10
User ubuntu
IdentityFile ~/.ssh/id_ed25519
Port 22
Host private-db
HostName 10.0.1.25
User ubuntu
IdentityFile ~/.ssh/id_ed25519
ProxyJump bastion
Host private-app
HostName 10.0.1.30
User ubuntu
IdentityFile ~/.ssh/id_ed25519
ProxyJump bastion
Now you can connect directly to private-db with a single command:
ssh private-db
SSH reads the config, sees the ProxyJump directive, automatically connects to the bastion first, and tunnels through to the private IP. You can run scp, rsync, and other SSH-based tools against private-db the same way.
Using SSH agent forwarding through the bastion
One question that comes up frequently: what SSH key should you use to connect from the bastion to the private server?
The wrong answer is to copy your private key to the bastion. If the bastion is ever compromised, the attacker gets your private key.
The right answer is SSH agent forwarding. Your local SSH agent holds the key. The bastion asks the agent on your local machine to perform the authentication, and the key never leaves your laptop.
Enable this in ~/.ssh/config:
Host bastion
HostName 203.0.113.10
User ubuntu
ForwardAgent yes
Or pass it with -A on the command line:
ssh -A ubuntu@bastion-public-ip
With agent forwarding enabled, connecting from the bastion to the private server uses the same key you have on your local machine, without the key ever being present on the bastion itself.
Managing jump hosts from a browser
If you are away from your main machine or using a device where editing ~/.ssh/config is not an option, SSHDock has native jump host support built into its connection UI. You enter the bastion's hostname and credentials, then enter the private server's details below it. SSHDock handles the tunneling without any config file editing.
This is useful for teams where some members need occasional access to private servers but are not comfortable managing SSH configs manually, or for situations where you need quick access from a tablet or a borrowed computer.
For a step-by-step walkthrough of setting up a bastion specifically for AWS EC2, see our AWS EC2 SSH guide.