
Leaving SSH open to the public internet is the default. It’s also why auth logs fill with bot noise within minutes. This SSH Jump Host setup guide tutorial shows a practical bastion (jump host) pattern for 2026: one small “front door” server, and no direct SSH exposure on your app and database VPS instances.
You’ll end up with a hardened bastion that accepts key-based logins only. Your private servers will accept SSH only from the bastion. Day-to-day access stays fast and predictable with one command.
What you’ll build (and why it’s worth doing)
A jump host is the only system that allows inbound SSH. Everything else sits behind it.
This single change improves security immediately:
- Smaller attack surface: only one box has port 22 reachable from the internet.
- Cleaner audit trail: one place to log interactive admin access.
- Safer firewall posture: private servers can default-deny inbound SSH from anywhere except the bastion IP.
- Operational simplicity: add/remove admin access by updating bastion keys, not every server.
This tutorial assumes Ubuntu 24.04/26.04 or Debian 12/13-style systems. The same approach works on AlmaLinux/Rocky, with minor package differences.
Prerequisites and architecture checklist
Before you touch configs, decide what “done” looks like. This baseline fits most hosting setups without extra complexity.
- 1 bastion VPS: small (1 vCPU/1 GB RAM is fine) with a static public IPv4/IPv6.
- 1+ private servers: your web/app/mail servers and any dedicated servers you administer.
- SSH keys: Ed25519 keys for each admin. Avoid shared keys.
- Firewall control: UFW/nftables on the servers plus (ideally) provider-level cloud firewall rules.
If you’re building on fresh infrastructure, a HostMyCode VPS works well for both the bastion and the servers behind it.
If you want OS hardening and guardrails handled while you keep root access, use managed VPS hosting.
Step 1 — Create strong SSH keys (client side)
On your workstation (macOS/Linux/WSL), generate a dedicated keypair for bastion access. Use a passphrase.
In 2026, an unprotected private key is a self-inflicted incident.
ssh-keygen -t ed25519 -a 64 -f ~/.ssh/hmc-bastion-ed25519 -C "you@company bastion"
Next, add a simple SSH config entry. This is what makes multi-hop access feel like a normal login.
nano ~/.ssh/config
Host hmc-bastion
HostName 203.0.113.10
User admin
IdentityFile ~/.ssh/hmc-bastion-ed25519
IdentitiesOnly yes
ServerAliveInterval 30
ServerAliveCountMax 3
Tip: keep one key per trust boundary.
Don’t reuse your Git key. Don’t reuse the same key across every environment.
Step 2 — Harden SSH on the bastion (keys only, no root, no password)
Log into the bastion using your provider’s bootstrap method (console or initial key). Start with basic patching and tooling.
sudo apt update && sudo apt -y upgrade
sudo apt -y install ufw fail2ban
Create an admin user (if you don’t already have one). Then add your public key.
sudo adduser admin
sudo usermod -aG sudo admin
sudo mkdir -p /home/admin/.ssh
sudo nano /home/admin/.ssh/authorized_keys
Fix permissions now, before you chase confusing login errors later.
Bad perms commonly trigger “Permission denied (publickey)”.
sudo chmod 700 /home/admin/.ssh
sudo chmod 600 /home/admin/.ssh/authorized_keys
sudo chown -R admin:admin /home/admin/.ssh
Now lock down /etc/ssh/sshd_config. On Ubuntu/Debian, use sshd_config.d overrides.
This keeps changes readable and upgrade-friendly.
sudo nano /etc/ssh/sshd_config.d/99-bastion-hardening.conf
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
PermitTunnel no
ClientAliveInterval 300
ClientAliveCountMax 2
AllowUsers admin
Validate first, then reload. This avoids lockouts caused by typos.
sudo sshd -t && sudo systemctl reload ssh
If you want a fuller baseline (2FA options, safe rollback patterns, and what to do when things go sideways), pair this with: SSH lockdown tutorial.
Step 3 — Firewall the bastion: allow SSH, deny everything else
Your bastion has one job: SSH entry. Keep inbound rules tight and predictable.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp comment 'SSH to bastion'
sudo ufw enable
sudo ufw status verbose
If you have fixed office IPs, restrict SSH to those CIDRs.
This cuts noise and raises the bar with almost no operational cost.
sudo ufw delete allow 22/tcp
sudo ufw allow from 198.51.100.0/24 to any port 22 proto tcp comment 'Office SSH only'
For a careful “don’t lock yourself out” UFW workflow, see: VPS firewall setup guide tutorial.
Step 4 — Prepare the private servers: SSH only from the bastion
On each private server, create the same admin account. Add keys as needed.
Then enforce the core rule: only accept SSH from the bastion’s IP.
On Ubuntu/Debian with UFW:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from 203.0.113.10 to any port 22 proto tcp comment 'SSH only from bastion'
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status
If you can, enforce the same rule at the provider firewall level.
That way, even if UFW gets loosened accidentally, port 22 still isn’t reachable from the world.
Step 5 — Enable jump access using ProxyJump (clean, modern SSH)
From your workstation, you can now SSH to a private server through the bastion in one line:
ssh -J admin@203.0.113.10 admin@10.0.0.20
Most teams prefer host aliases in ~/.ssh/config. Add entries like this:
Host hmc-app-1
HostName 10.0.0.20
User admin
ProxyJump hmc-bastion
IdentityFile ~/.ssh/hmc-bastion-ed25519
IdentitiesOnly yes
Then access becomes:
ssh hmc-app-1
Common pitfall: if your private server uses a different key than the bastion, set a separate IdentityFile for that host.
Keep agent forwarding disabled unless you have a specific, reviewed reason to allow it.
Step 6 — Add controlled port forwarding for emergencies (optional)
Earlier we disabled TCP forwarding on the bastion. That’s the right default.
If you sometimes need short-lived tunnels (for example, an internal admin panel), enable forwarding deliberately. Restrict it as well.
On the bastion, allow forwarding but limit destinations with PermitOpen. Edit:
sudo nano /etc/ssh/sshd_config.d/99-bastion-forwarding.conf
AllowTcpForwarding yes
PermitOpen 10.0.0.20:443 10.0.0.30:2087
Reload SSH:
sudo sshd -t && sudo systemctl reload ssh
Example tunnel from your laptop to an internal HTTPS service:
ssh -J hmc-bastion -L 8443:10.0.0.20:443 admin@10.0.0.20
Then browse https://localhost:8443.
Treat tunnels as temporary access. Time-box them, and write down why they exist.
Step 7 — Logging and alerting: make the bastion noisy in the right way
The bastion is your choke point. Use it as one.
- Audit SSH: review
/var/log/auth.log(Debian/Ubuntu) orjournalctl -u ssh. - Fail2Ban: keep a jail for SSH and confirm bans are happening.
- Daily summary: a short email report catches slow-burn issues before they become incidents.
Install Logwatch for daily digest-style reporting:
sudo apt -y install logwatch
sudo logwatch --detail high --service sshd --range today
For a complete setup, follow: Logwatch setup tutorial.
Step 8 — Patching and safe reboots (don’t let the bastion drift)
If the bastion is down, you’ll notice fast. Patch it automatically, and schedule controlled reboots.
On Ubuntu/Debian, unattended-upgrades plus a plan for kernel reboots is a solid baseline.
Use this as your reference implementation: VPS security update tutorial (2026).
For larger fleets, consider a second bastion.
Even a cold standby with the same config and a tested access procedure beats improvising during an outage.
Quick diagnostics: common failure modes (and fast fixes)
- You can reach the bastion, but not the private server: confirm UFW on the private server allows port 22 from the bastion IP. Also confirm the bastion is using the correct source address.
- “Permission denied (publickey)” on the private server: check
~/.ssh/authorized_keyspermissions. Confirm the correct identity is selected (ssh -vvv hmc-app-1). - Host key warnings after rebuild: remove the old key entry:
ssh-keygen -R 10.0.0.20(or the hostname alias). - Slow SSH through the bastion: check DNS resolution on the private server. Bad resolvers add seconds to logins.
Operational checklist for hosting teams
- One bastion per environment (production vs staging), not shared across everything.
- Key-based auth only; password auth disabled everywhere.
- SSH allowed to private servers only from bastion IP (cloud firewall + OS firewall).
- Admin access is per-person keys; revoke by removing one key, not rotating a shared credential.
- Daily log summaries (Logwatch) and brute-force blocking (Fail2Ban).
- Document an emergency path: provider console access, break-glass key storage, and who can use it.
Summary: a safer “front door” for your infrastructure
This setup stays intentionally simple: one bastion, strict SSH policy, and private servers that never accept SSH directly from the public internet.
You’ll see fewer auth attempts, cleaner logs, and fewer “how was this exposed?” moments.
If you’re rolling this out for customer hosting or multiple WordPress sites, start with a HostMyCode VPS for the bastion and your app servers.
If you’d rather have patching and baseline hardening handled while you focus on sites and clients, managed VPS hosting is the right operational model.
Need a clean, production-ready place to run a bastion and a private server fleet? HostMyCode offers HostMyCode VPS plans that fit jump hosts nicely, plus managed VPS hosting if you want help with security baselines, updates, and monitoring.
FAQ
Do I need a jump host if I already changed SSH to a non-standard port?
Yes. A different port cuts random scans, but SSH is still exposed. A jump host removes direct exposure from the servers that hold your sites and data.
Should the bastion and private servers share the same SSH key?
No. Use per-person keys, and ideally separate keys per environment. Shared keys complicate offboarding and incident response.
Can I use a jump host with a dedicated server?
Absolutely. Your dedicated server can keep SSH locked down to only the bastion’s IP while still serving web traffic publicly.
What’s the simplest rollback if I lock myself out?
Use your provider console/KVM, log in as root, and revert the last SSH/firewall change. That’s also why you run sshd -t before you reload.
How do I scale this for a team?
Keep access centralized: manage bastion user keys, log everything, and restrict private-server SSH strictly to the bastion.
For larger teams, add a second bastion for redundancy and document a break-glass procedure.