Back to tutorials
Tutorial

SSH Bastion Host Tutorial (2026): Centralize VPS Access with ProxyJump, MFA, and Per-User Audit Logs

SSH bastion host tutorial for 2026: set up ProxyJump, MFA, per-user keys, and session logging for safer VPS admin access.

By Anurag Singh
Updated on Jul 14, 2026
Category: Tutorial
Share article
SSH Bastion Host Tutorial (2026): Centralize VPS Access with ProxyJump, MFA, and Per-User Audit Logs

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

  1. Create user: adduser
  2. Add to group: usermod -aG ssh-bastion
  3. Add their public key to authorized_keys
  4. Enroll MFA: google-authenticator
  5. Confirm login + ProxyJump to at least one target

Offboard an admin

  1. Disable account immediately: sudo usermod -L username
  2. Remove from ssh-bastion group
  3. Archive session logs if needed for audit
  4. 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 AllowUsers or AllowGroups to 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-ip should fail (timeout or blocked).
  • Via bastion test: ssh web1 should 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.