A two-server fleet starts fine: you copy your public key to both boxes and move on. At ten servers you spend half your morning deciding which key goes where. At fifty, someone has a key for one box and a different key for another, nobody remembers which, and deprovisioning a contractor means hunting through every authorized_keys file you can find. The bastion host pattern exists to stop that sprawl.
What is a bastion host?
A bastion host is a single, hardened entry point into your private network. Instead of every machine exposing SSH to the internet, only the bastion has a public reachable address. Everything inside sits on private addresses and does not need its own internet-facing port at all.
There are several names for the same idea: bastion host, jump host, jump box, or SSH gateway. Cloud providers use the term bastion, and audits use the term jump host, and they all mean the same thing: one small, locked-down machine that is the only door into the network.
Connections enter through the bastion, then hop onward to the target. With OpenSSH this is a native feature, no extra software required: ProxyJump. Your client opens a connection to the bastion and tunnels onward through it, as if SSH natively supports the chain. Because it does.
How ProxyJump works
In your ~/.ssh/config, define the bastion once and reference it from every internal host. Two lines per host group is all the config overhead this pattern costs:
Host bastion
HostName bastion.example.com
User admin
Host web-01 internal-* 10.0.0.*
ProxyJump bastion
User deploy
From then on, ssh web-01 automatically routes through the bastion. The target never sees the internet, and the client never needs a direct route to it. The pattern reads cleanly, which is worth a lot when the same server is referenced from a well-structured SSH config.
If you have ever used ProxyCommand nc -X connect -x proxy, ProxyJump is the modern, built-in replacement. OpenSSH 7.3 and later treat ProxyJump as a first-class option because the proxy case is so common. With a single host on both sides of the jump the syntax stays simple, though you can also chain jumps by comma-separating hosts in the ProxyJump value, which is how you reach an instance that lives two hops inside a private network.
The key model: your key stays with you
The important part is what the bastion changes about keys. Your private key never lives on the bastion. Your public key is on the bastion so it lets you in, and your public key is on the internal hosts so they accept the hop. But if a human uses a shared account, that account’s key would be the same for everyone, which is exactly the shared-credential smell you want to avoid.
Give each human their own account on the bastion, and put each person’s key on the hosts they need to reach. When someone leaves, remove their key from the bastion and from the internal boxes. With the right key management for teams, the whole lifecycle stays small.
What the bastion buys you beyond key hygiene
- Fewer exposed attack surfaces: one public port instead of fifty.
- One interception point for logging who connected and where they went.
- Central revocation: block a user at the bastion, block them everywhere.
- Cleaner DNS: internal hosts do not need public records.
Hardening the bastion itself
Because the bastion is the most exposed machine, it deserves more hardening than any internal host. At minimum: key-only auth, no root login over SSH, rate limiting on repeated failed attempts, automatic security updates, and a separate account per human. Treat the bastion as a machine that will be probed constantly, because it will.
One of the strongest options for a fleet of any size is to issue host and user certificates from a small CA, which turns “delete a key” into “the cert expires on its own.” The mechanics and the migration path are covered in the SSH certificate authority comparison. A CA is not mandatory for the bastion pattern, but it is the natural ceiling once the fleet grows.
What to log if everyone hops through one box
A bastion concentrates your access, which also concentrates the value of logging. Because every connection flows through one narrow pipe, visibility at that layer is far more feasible than wiring up every internal host. The pattern of recording what ran, when, and by whom is the same one used for shared access anywhere, covered in what a per-command audit log catches.
If compliance is the driver, the bastion is the natural seat for an access review: the question becomes who can reach the bastion, and which internal hosts each person can hop to. An audit that runs against one choke point is far cheaper than an audit that must touch every server. The spreadsheet-free access review walks through running exactly that review against a small fleet.
Common bastion setup mistakes
- Storing private keys on the bastion and treating it as a jump library. If the bastion is compromised, every internal host is compromised. Keys belong to humans, not to the jump box.
- Allowing password auth anywhere. The bastion should require keys, and the internal hosts should only accept hops, not direct password logins.
- Using one shared admin account on the bastion. There is no way to answer “who did that?” if everyone logs in as the same user.
- Skipping updates on the bastion itself. It is your most exposed machine, so it should be your most patched one.
The first mistake deserves the emphasis: a bastion that stores private keys is not a fortress, it is a honeypot with a door. The entire point is that compromise of the bastion gives an attacker a pivot point and your credentials. Keys always stay on the human’s machine, and the bastion only ever sees the public half.
When the bastion is not enough
The bastion solves entry and revocation, but it does not solve auditing who ran what once inside. For that you layer a logging mechanism on the channel, or you shift to a model where the bastion is itself the interactive endpoint. Small teams almost always start with per-command logging on the bastion and the few hosts that matter most; the full menu of recording options is compared in the session logging comparison.
Hardening the bastion itself
Because the bastion is the most exposed machine, it deserves more hardening than any internal host. At minimum: key-only auth, no root login over SSH, rate limiting on repeated failed attempts, automatic security updates, and a separate account per human. Treat the bastion as a machine that will be probed constantly, because it will.
Automating bastion deployment with fail2ban and UFW
Because a bastion host sits on a public IP address, it will be relentlessly scanned by automated brute-force bots within minutes of provisioning. Hardening the bastion with an aggressive fail2ban jail and a minimal ufw firewall is mandatory practice:
# 1. Lock down incoming firewall rules
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from YOUR_OFFICE_IP to any port 22 proto tcp
sudo ufw enable
# 2. Configure fail2ban jail in /etc/fail2ban/jail.local
[sshd]
enabled = true
port = 22
filter = sshd
maxretry = 3
bantime = 86400
findtime = 600
With bantime = 86400, any source IP failing authentication three times within ten minutes is banned for 24 hours at the iptables packet level. Combine this with key-only authentication (PasswordAuthentication no) and the bastion will brush off automated internet noise without registering measurable CPU or memory overhead.
Restricting bastion ports with IP allow-lists and wireguard
Exposing port 22 on the bastion to the entire public internet (0.0.0.0/0) is acceptable when your team is constantly traveling without static IPs, but for distributed teams with predictable VPN egress or office locations, locking down the bastion’s incoming firewall is a dramatic security upgrade:
# Restrict bastion SSH to WireGuard VPN interface only:
sudo ufw allow in on wg0 to any port 22 proto tcp
sudo ufw deny 22/tcp
When the bastion only listens on a private WireGuard or Tailscale interface, port 22 is completely invisible on the public IPv4 internet, neutralizing 100% of automated port scans, credential stuffing bots, and zero-day OpenSSH exploits before packets ever reach sshd.
The takeaway
A bastion with ProxyJump is not a heavy enterprise pattern. It is a small config file and a slightly different habit, and it removes the worst failure mode of fleet access: keys scattered everywhere with no single place to revoke them. If your fleet is growing, this is the moment to set it up, before the sprawl becomes the architecture.
Bastions and ProxyJump are exactly the kind of connection topology an SSH manager should make boring. termique is a free SSH manager we build, with host groups, keys, and snippets so the jump chain stays organized instead of living in your memory. termique.app, if you are curious.