
Your biggest SSH risk usually isn’t the crypto. It’s sprawl: dozens of servers, exposed port 22, shared keys, and no usable audit trail.
This SSH bastion host tutorial shows how to centralize access to your VPS and dedicated servers. You’ll use one hardened entry point, short-lived user access, and session logs you can trust during an incident.
You’ll build a bastion on Ubuntu 24.04 LTS, route admin access through it with ProxyJump, require MFA, and record sessions with clear ownership.
The goal is simple: fewer exposed systems, easier onboarding/offboarding, and logs that hold up for troubleshooting and compliance.
What you’ll build (and what you’ll stop exposing)
- One public SSH endpoint (the bastion). Your app/database servers can keep SSH private-only.
- ProxyJump-based access so admins type
ssh web1, not a rotating list of IPs and ports. - MFA at the bastion for interactive logins.
- Per-user keys + tight authorization (no shared “ops” key).
- Session recording to a log directory (and optional off-box shipping).
If you need a clean place to host the bastion, a small HostMyCode VPS is usually enough: 1 vCPU, 1–2 GB RAM, and fast storage.
If you’d rather not maintain the security baseline yourself, managed VPS hosting is the simpler production option.
Prerequisites and network layout (keep it boring)
This tutorial assumes:
- Bastion: Ubuntu 24.04 LTS, public IPv4, SSH on port 22 (keep the default unless you have a policy).
- Targets: one or more VPS/dedicated servers on a private network (preferred) or at least firewall-restricted to the bastion IP.
- You can edit firewalls (UFW/iptables/cloud firewall) and SSH configs.
- Admins have a laptop/desktop with OpenSSH 9.x (macOS, Linux, or Windows 11+ OpenSSH).
Step 1 — Provision the bastion and create a non-root admin
Start from a fresh server image. Patch it, reboot, then create a real admin account.
On Ubuntu:
sudo apt update
sudo apt -y upgrade
sudo reboot
After reboot, create an admin user (replace alice):
sudo adduser alice
sudo usermod -aG sudo alice
From this point on, don’t SSH in as root.
If you want a dedicated walkthrough for least-privilege sudo, use: Sudo setup guide tutorial (2026).
Step 2 — Lock SSH down on the bastion (keys only, sane timeouts)
Edit /etc/ssh/sshd_config (or use drop-in overrides under /etc/ssh/sshd_config.d/).
A dedicated file is easier to review later:
sudo nano /etc/ssh/sshd_config.d/10-bastion-hardening.conf
Use settings like these (adjust to your policy):
Port 22
Protocol 2
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication yes
PubkeyAuthentication yes
AllowTcpForwarding yes
X11Forwarding no
PermitTunnel no
ClientAliveInterval 300
ClientAliveCountMax 2
LoginGraceTime 30
MaxAuthTries 4
MaxSessions 10
MaxStartups 10:30:60
# Restrict who can even try
AllowGroups ssh-bastion
Create the group and add your admin user:
sudo groupadd ssh-bastion
sudo usermod -aG ssh-bastion alice
Validate and reload SSH safely:
sudo sshd -t
sudo systemctl reload ssh
Pitfall: don’t lock yourself out.
Keep an active root console (or provider rescue console) open while you test.
Step 3 — Firewall: only SSH in, only private SSH out
On the bastion, allow only SSH inbound.
With UFW:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw enable
sudo ufw status verbose
On each target server, restrict SSH so only the bastion can reach it.
Example using UFW on a target:
sudo ufw allow from <BASTION_PRIVATE_IP> to any port 22 proto tcp
sudo ufw deny 22/tcp
sudo ufw reload
sudo ufw status numbered
If you’re chasing “it worked yesterday, now SSH times out,” this checklist saves time: Firewall troubleshooting tutorial (2026).
Step 4 — Set up per-user SSH keys (no shared keys)
On each admin workstation, generate a key per person.
In 2026, Ed25519 is the default choice:
ssh-keygen -t ed25519 -a 64 -f ~/.ssh/id_ed25519_bastion_alice -C "alice@company-bastion"
Add alice’s public key to the bastion user account:
sudo -u alice mkdir -p /home/alice/.ssh
sudo -u alice chmod 700 /home/alice/.ssh
sudo nano /home/alice/.ssh/authorized_keys
sudo -u alice chmod 600 /home/alice/.ssh/authorized_keys
Repeat for each admin.
For hosting resellers, this is the point where access stops living in someone’s head and starts living in a list you can audit.
Step 5 — Enforce MFA on the bastion (TOTP for interactive SSH)
Put MFA at the point of entry: the bastion login itself.
A common Ubuntu approach is PAM + Google Authenticator-compatible TOTP (supported by most authenticator apps).
Install the module:
sudo apt -y install libpam-google-authenticator
For each user who should have MFA, run (as that user):
sudo -u alice google-authenticator
Choose settings that match your environment.
For a bastion, these defaults work well:
- Time-based tokens: yes
- Disallow token reuse: yes
- Rate limiting: yes
Then enable it in SSH PAM. Edit:
sudo nano /etc/pam.d/sshd
Add near the top (before common-auth is fine):
auth required pam_google_authenticator.so nullok
Important: nullok lets users log in without TOTP configured.
Remove nullok once everyone is enrolled. Many teams prefer a single break-glass account with console-only access instead of leaving nullok enabled.
Now confirm SSH allows keyboard-interactive prompts (you already set KbdInteractiveAuthentication yes).
Reload:
sudo sshd -t
sudo systemctl reload ssh
Test a fresh SSH session from your workstation to the bastion.
After key auth, you should get a TOTP prompt.
Step 6 — Make ProxyJump the default (clean workstation config)
A good bastion setup fades into the background.
You define it once in ~/.ssh/config. After that, you connect to hosts by name.
Example config (adjust hostnames and private IPs):
Host bastion
HostName bastion.example.net
User alice
IdentityFile ~/.ssh/id_ed25519_bastion_alice
IdentitiesOnly yes
ServerAliveInterval 30
ServerAliveCountMax 3
Host web1
HostName 10.10.0.11
User ubuntu
ProxyJump bastion
Host web2
HostName 10.10.0.12
User ubuntu
ProxyJump bastion
Host db1
HostName 10.10.0.21
User ubuntu
ProxyJump bastion
Now you can run:
ssh web1
No extra flags. No manual tunnels.
This also cuts down on fat-finger mistakes during an incident, when people start copying the wrong IP under pressure.
Step 7 — Restrict what bastion users can do (groups and forced commands)
A bastion shouldn’t turn into a shared shell box.
Two rules keep it that way:
- Only specific groups can log in (you already set
AllowGroups ssh-bastion). - Port forwarding is a permission, not a default.
If only some admins need TCP forwarding, use Match blocks in SSH config:
sudo nano /etc/ssh/sshd_config.d/20-bastion-match.conf
# Default: no port forwarding
AllowTcpForwarding no
Match Group ssh-bastion
AllowTcpForwarding yes
Reload SSH and test again.
Step 8 — Add session logging you can trust (tlog or audit-friendly recording)
Shell history isn’t audit logging. On a bastion, you want session recording with reliable attribution.
On Ubuntu 24.04, tlog is a solid option. It records terminal I/O for interactive sessions and can store it locally or forward it to a central collector.
Install tlog and SSSD components:
sudo apt -y install tlog sssd
Enable recording for the ssh-bastion group using PAM. Edit:
sudo nano /etc/pam.d/sshd
Add below the MFA line:
session required pam_tlog.so
Then configure tlog storage.
For small teams, local JSON logs are often enough. Check the config paths:
sudo ls -la /etc/tlog/
sudo cat /etc/tlog/tlog-rec-session.conf
Plan for disk usage and log rotation.
If you already centralize logs, ship recordings off the bastion. HostMyCode has a practical rsyslog + TLS pattern here: Log shipping tutorial (2026).
Quick diagnostic: open a new SSH session, run a few commands, then verify logs appear in your configured tlog output directory.
If you get nothing, your PAM stack order is likely wrong, or the session isn’t interactive.
Step 9 — Add a clean onboarding/offboarding workflow (15-minute checklist)
This is where a bastion starts saving you real time.
Write a short runbook, then follow it every time.
Onboard a new admin
- Create user:
adduser - Add to group:
usermod -aG ssh-bastion - Add their public key to
authorized_keys - Enroll MFA:
google-authenticator - Confirm login + ProxyJump to at least one target
Offboard an admin
- Disable account immediately:
sudo usermod -L username - Remove from
ssh-bastiongroup - Archive session logs if needed for audit
- Rotate any shared secrets the user had (ideally none)
Step 10 — Hardening targets for bastion-only SSH
On each target server, make SSH accept only the bastion path:
- Firewall: allow port 22 only from bastion private IP.
- SSHD: disable password auth, disable root login.
- Optionally: add
AllowUsersorAllowGroupsto limit admin accounts.
If you’re still building your baseline, HostMyCode’s Ubuntu hardening walkthrough fits neatly with this model: Server hardening tutorial for a new Ubuntu VPS.
Step 11 — Prove it works: access tests and failure-mode drills
Run these checks before you call the project finished.
- Direct-to-target SSH test: from your laptop,
ssh target-public-ipshould fail (timeout or blocked). - Via bastion test:
ssh web1should succeed via ProxyJump. - MFA test: wrong TOTP should fail, correct TOTP should work.
- Log test: run
whoami,sudo -l, and confirm the session is recorded. - Break-glass test: confirm you can recover via provider console if MFA locks everyone out.
If you already monitor your servers, add a simple bastion checklist: SSH port reachable, disk usage under threshold, and alerts for auth-log anomalies.
This pairs well with: Server monitoring tutorial (2026).
Operational notes for hosting teams (resellers and agencies)
If you manage client sites, the bastion becomes the one place admin access begins.
Treat it as production, even if it’s tiny.
- Patch weekly. A bastion should run few services, so updates are usually low-drama.
- Keep disk headroom. Session logs grow slowly—until they don’t.
- Document which targets exist and who can access them.
- Separate environments: one bastion for production, another for staging.
For migrations, build the bastion first, then move workloads.
Cutovers feel calmer when access is already controlled. If you’re moving sites from shared hosting, the staging-based method here is a safe pattern: Web hosting migration tutorial (2026).
Summary: the “one door” model for SSH in 2026
You end up with one SSH entry point, MFA at the edge, per-user keys, and real session visibility.
The first time someone asks, “who ran that command on the server?”, you’ll have an answer backed by logs.
If you want a bastion that stays responsive with multiple admins and keeps logs on fast storage, start with a HostMyCode VPS.
If you’d rather hand off patching, baseline hardening, and day-to-day upkeep, choose managed VPS hosting so your team can focus on the sites you host.
If you’re centralizing admin access for multiple websites or client servers, the bastion pattern is a fast security win. HostMyCode offers predictable performance on HostMyCode VPS, and you can offload routine server upkeep with managed VPS hosting.
FAQ
Should my bastion be on the same provider/network as my servers?
Ideally yes. Private networking reduces exposure and avoids whitelisting public IP changes.
If that’s not possible, lock target SSH to the bastion’s static public IP.
Can I use the bastion for port forwarding to panels or databases?
Yes, but keep it deliberate. Allow TCP forwarding only for an admin group, and record sessions.
If you need safer temporary access, use time-boxed firewall rules.
What if MFA breaks and nobody can log in?
Keep a break-glass path: provider console access and a documented recovery procedure.
Test it once, then keep it ready like a fire extinguisher.
Do I still need to harden my target servers?
Absolutely. The bastion reduces exposure, but targets still need patching, key-only SSH, and least-privilege sudo.
Treat the bastion as the front desk, not a guarantee.
How big should the bastion be?
For most teams, 1 vCPU and 1–2 GB RAM is fine. Size it based on log retention and concurrent sessions, not CPU.