
Leaving SSH open on every server invites brute-force noise, credential stuffing, and the occasional “how did this get in here?” moment. This SSH jump host setup guide tutorial fixes that with a simple pattern: expose SSH on one hardened bastion. Then reach everything else through it using private networking or strict firewall rules.
The setup below is production-friendly for 2026. Use a small bastion VM, keys-only auth, optional TOTP 2FA for humans, tight inbound rules, and logs you can actually use. It works for a solo-admin VPS and scales cleanly for teams managing customer fleets.
What you’ll build: a bastion (jump host) pattern that fits real hosting operations
A jump host (bastion) is the only machine that accepts inbound SSH from the internet. Every other VPS or dedicated server either:
- accepts SSH only from the bastion’s IP, or
- accepts SSH only on a private network, reachable from the bastion.
In day-to-day hosting ops, that buys you three concrete improvements:
- Smaller attack surface: one public SSH endpoint instead of ten.
- Cleaner access control: one place to enforce keys, MFA, and logging.
- Faster offboarding: revoke access on the bastion and you’ve effectively cut off everything behind it.
For managed workflows, treat the jump host as your front door.
Use it for routine work like checking logs, restarting services, applying patches, and handling migrations.
Prerequisites and a safe network layout
This tutorial assumes you have:
- A bastion VPS (Ubuntu Server 24.04 LTS or Debian 12). Small is fine: 1 vCPU, 1 GB RAM, NVMe storage.
- One or more target servers (VPS or dedicated) running OpenSSH.
- Either a private network between bastion and targets, or the ability to firewall targets so they only accept SSH from the bastion’s public IP.
Need a clean server to start with? A HostMyCode VPS works well for the bastion role.
managed VPS hosting is the easier option if you want help with baseline hardening and patching.
Step 1 — Provision the bastion and apply baseline hardening
Log in to the bastion using your provider console or the initial SSH key. Patch it first. Then enable unattended security updates.
sudo apt update && sudo apt -y upgrade
sudo apt -y install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
Create a dedicated admin user. Commit to keys-only access from the start.
sudo adduser admin
sudo usermod -aG sudo admin
Copy your SSH key to the new user. Confirm you can log in before touching sshd settings:
ssh-copy-id admin@BASTION_PUBLIC_IP
ssh admin@BASTION_PUBLIC_IP
If you want a structured checklist for verifying exposed services and risky ports, fold that into your normal ops routine.
For a focused approach, see VPS security audit tutorial.
Step 2 — Configure sshd on the bastion (keys-only, minimal attack surface)
Edit /etc/ssh/sshd_config on the bastion. Defaults differ by distro and image.
Set these explicitly instead of relying on whatever came preinstalled.
sudo nano /etc/ssh/sshd_config
# Keep SSH on 22 unless you have a policy reason to change it.
Port 22
Protocol 2
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
UsePAM yes
AllowUsers admin
# Faster disconnects for dead sessions
ClientAliveInterval 300
ClientAliveCountMax 2
# Avoid DNS delays
UseDNS no
# Forwarding is useful for bastion workflows, keep it controlled
AllowTcpForwarding yes
X11Forwarding no
PermitTunnel no
Validate the config, then reload safely:
sudo sshd -t
sudo systemctl reload ssh
Pitfall: don’t lock yourself out. Keep your current session open while you test a fresh login from a second terminal.
Step 3 — Add optional 2FA on the bastion (TOTP) for human logins
Keys stop password guessing. 2FA helps when keys get copied, mishandled, or stolen.
On Ubuntu/Debian, you can add TOTP via PAM.
Install the Google Authenticator PAM module:
sudo apt -y install libpam-google-authenticator
Switch to the admin user and enroll:
su - admin
google-authenticator
Use a conservative baseline when answering prompts:
- Time-based tokens: yes
- Update
.google_authenticator: yes - Disallow multiple uses: yes
- Increase window: no (unless you have known clock drift)
- Enable rate-limiting: yes
Then edit /etc/pam.d/sshd and add:
sudo nano /etc/pam.d/sshd
auth required pam_google_authenticator.so
Finally, update /etc/ssh/sshd_config so SSH can use keyboard-interactive via PAM:
KbdInteractiveAuthentication yes
AuthenticationMethods publickey,keyboard-interactive
Reload SSH and test a brand-new login. You should see your key accepted first, then a verification code prompt.
sudo sshd -t
sudo systemctl reload ssh
Operational note: keep 2FA on the bastion.
Avoid enabling it on backend servers unless you’ve already designed automation and break-glass access around it.
Step 4 — Lock the bastion firewall to the minimum
The bastion should expose only SSH. If you run monitoring or anything else, open it deliberately.
Restrict it by source IP.
On Ubuntu/Debian, UFW is simple and readable:
sudo apt -y install ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw enable
sudo ufw status verbose
If your admins have fixed office IPs, lock it down further:
sudo ufw delete allow 22/tcp
sudo ufw allow from 203.0.113.10 to any port 22 proto tcp
sudo ufw allow from 198.51.100.0/24 to any port 22 proto tcp
sudo ufw status numbered
Emergency access: document a break-glass path (provider console or IPMI for dedicated servers).
Write this down before a firewall change goes sideways.
Step 5 — Prepare the target servers: accept SSH only from the bastion
On each target VPS/dedicated server, the goal is the same: SSH should not be publicly reachable.
Put targets on a private subnet reachable from the bastion. Or enforce a strict allowlist so only the bastion can connect.
On a target server with UFW:
sudo apt -y install ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow SSH only from the bastion public IP
sudo ufw allow from BASTION_PUBLIC_IP to any port 22 proto tcp
# If you host web services, keep 80/443 open as needed
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
Harden SSH on targets the same way (keys-only, no root login).
In most environments, you can skip 2FA on targets because you already control access at the bastion.
Step 6 — Configure SSH ProxyJump on your laptop (the clean workflow)
You don’t need extra tooling to use a bastion.
Configure ~/.ssh/config locally so SSH hops through the bastion automatically.
nano ~/.ssh/config
Host bastion
HostName BASTION_PUBLIC_IP
User admin
IdentityFile ~/.ssh/id_ed25519
Host web-01
HostName 10.10.0.11
User admin
ProxyJump bastion
Host web-02
HostName 10.10.0.12
User admin
ProxyJump bastion
After that, connecting feels normal:
ssh web-01
No private IPs on the targets? ProxyJump still works with public IPs.
The target firewall just needs to allow SSH only from the bastion IP.
Step 7 — Use agent forwarding carefully (or don’t)
Agent forwarding (-A) lets you use your local SSH keys through the bastion without copying keys onto it. That convenience comes with a tradeoff.
If the bastion is compromised, forwarded agents can be abused.
A practical stance for hosting operations:
- Store no private keys on backend servers.
- Avoid agent forwarding for daily work unless you trust your bastion hardening and monitoring.
- Use dedicated keys for bastion login and separate keys for automation accounts, rotated regularly.
If you truly need agent forwarding (for example, Git checkouts on backend hosts), enable it per host instead of globally:
Host web-01
ForwardAgent yes
Step 8 — Add auditing: log what matters, where you’ll actually read it
A bastion you never check becomes a single point of failure. Worse, it can fail quietly.
Aim for daily summaries and quick ways to spot odd access patterns.
- Enable basic log reporting: Logwatch setup tutorial
- Track uptime and alert on SSH outages: VPS monitoring setup tutorial
Quick diagnostics to run on the bastion while you’re setting things up:
# Recent SSH logins (Ubuntu/Debian)
sudo journalctl -u ssh --since "24 hours ago" | tail -n 200
# Failed attempts summary
sudo journalctl -u ssh --since "24 hours ago" | grep -E "Failed|Invalid" | tail -n 50
If your servers are busy, you can add a central log destination later. Don’t start there.
Start with something you’ll keep up with.
Step 9 — Make migrations and emergency work simpler with a bastion
Migrations bundle DNS changes, SSL work, web config, and occasional mail routing under a deadline.
A bastion keeps your access path stable, even as public IPs and hostnames shift.
Two patterns that help in real cutovers:
- Staging-to-live cutovers: keep both old and new servers reachable via the bastion while you validate URLs, SSL, and content.
- Rollback windows: during DNS propagation, you can reliably reach both environments to compare behavior.
For a WordPress-safe validation workflow on VPS, see Hosting staging environment tutorial.
For DNS-specific issues after a move, keep DNS propagation troubleshooting tutorial bookmarked.
Step 10 — Hardening checklist (printable, boring, effective)
- Bastion SSH:
PermitRootLogin no, keys-only, MFA optional,AllowUsersrestricted. - Bastion firewall: inbound only 22/TCP, ideally source-IP restricted.
- Targets: SSH allowed only from bastion (public IP allowlist) or only on private network.
- Keys: unique keypairs per admin; rotate on staff changes; disable lost keys immediately.
- Logging: daily summaries (Logwatch) and alerts for downtime; review failed logins weekly.
- Access separation: no shared “admin” private key files; use individual accounts, or at least individual keys.
- Break-glass: provider console/IPMI documented; recovery steps stored somewhere accessible during an outage.
Troubleshooting: the failures you’ll actually hit
“Connection closed” or “Permission denied” after enabling 2FA
- Confirm
AuthenticationMethods publickey,keyboard-interactiveis set (comma matters). - Check
KbdInteractiveAuthentication yesand that PAM is enabled (UsePAM yes). - Review logs:
sudo journalctl -u ssh -n 200
ProxyJump works sometimes, then hangs
- Confirm the target SSH service is listening:
sudo ss -lntp | grep :22 - Confirm firewall allows SSH from bastion IP on the target.
- If you’re using private IPs, confirm routing between subnets and that the bastion can reach the target:
pingandnc -vz 10.10.0.11 22.
You can SSH to the bastion, but not to targets
- From the bastion, test direct SSH:
ssh admin@10.10.0.11 - If that fails, the issue is target-side (firewall, sshd_config, keys, routing).
- If that works, your local SSH config is wrong (Hostnames, ProxyJump name mismatch, wrong IdentityFile).
If you’d rather implement the jump-host pattern without fighting networking edge cases, start with a clean HostMyCode VPS for the bastion. Then place workloads on managed VPS hosting so patching and baseline security stay on schedule. It’s a low-cost way to cut SSH exposure across every server you run.
FAQ: SSH jump host setup for hosting admins
Do I need a private network for a jump host?
No. A private network is cleaner, but you can also keep targets on public IPs and restrict SSH inbound to the bastion’s IP only.
Should I change SSH to a non-standard port on the bastion?
It reduces log noise, not risk. Keys-only auth, MFA, and firewall source restrictions do the real work. Stick to port 22 unless policy requires otherwise.
Is 2FA required if I already use SSH keys?
Not required, but useful for human access. If your admin laptop key is stolen, 2FA prevents immediate access. Avoid 2FA on backend servers unless you’re managing emergency access carefully.
How do I onboard a new admin safely?
Create an individual user (or at least an individual key) on the bastion, restrict access with AllowUsers, and log their sessions. Don’t share one private key across a team.
Summary: the practical win you should expect
After you implement a bastion, two things change fast. You have fewer public SSH endpoints to defend.
You also gain one consistent place to enforce access policy.
That’s exactly what you want for a hosting VPS fleet—especially during migrations, incident response, and routine maintenance.
If you’re scaling beyond a single server, build this on a reliable base like a HostMyCode VPS.
Move critical workloads to dedicated servers when you need fixed resources and stronger isolation.
Either way, the bastion pattern keeps SSH exposure predictable and under control.