Setting Up Your Own SSH Bastion Server
Why You Need a Bastion and Where It Lives
A bastion is the single entry point into a private network segment. Instead of exposing SSH on every server to the internet, you funnel traffic through one hardened host with a strict access policy. Typical layout: internet → bastion (public IP) → internal servers (only private subnet, SSH listening on 127.0.0.1 or a private interface).
The bastion sits in a demilitarized zone (DMZ) or a public subnet provided by your hosting platform. Internal machines have no route to the internet through the bastion — return traffic flows only over established connections. This is a baseline model you can deploy on any VPS in about 15 minutes.
Choosing an OS and Basic Setup
The bastion doesn’t need to be heavy. Debian, Ubuntu Server, or AlmaLinux — all work. I usually grab a minimal Ubuntu 22.04 LTS install and bring it to working state by hand.
Don’t put a GUI, databases, or other services on the bastion. The smaller the attack surface, the better.
Create a non-privileged user for daily work:
SSH Configuration: Keys, Port, Disable Passwords
Generate a key on your workstation if you don’t have one yet:
On the bastion, edit /etc/ssh/sshd_config:
Before restarting SSH, make sure the key is added and works. Otherwise, you’ll lock yourself out of the server.
Validate the config and restart:
sshd_config keys and what they do:
| Parameter | Value | Purpose |
|---|---|---|
Port | 22220 | Non-standard port, reduces log noise |
PermitRootLogin | no | Blocks direct root login |
PasswordAuthentication | no | Keys only |
MaxAuthTries | 3 | Limits authentication attempts |
AllowUsers | deploy | Whitelist of allowed users |
Firewall and Access Restrictions
UFW is the simplest way to close everything unnecessary:
If the bastion is only for your IP, restrict it further:
If your IP is dynamic, use a VPN instead of exposing the port. Opening SSH to the internet without source restrictions is a bad practice.
For internal traffic, add a rule on the bastion to allow packet forwarding:
Add net.ipv4.ip_forward = 1 to /etc/sysctl.conf so the setting survives a reboot.
Fail2ban and Brute-Force Protection
Install and configure Fail2ban to protect against password guessing:
In /etc/fail2ban/jail.local:
Restart:
Logging and Auditing
The bastion must log everything. On Debian/Ubuntu, SSH logs go to /var/log/auth.log. For centralized collection, configure rsyslog to ship to a separate SIEM or at least a second server:
For user action auditing, attach auditd:
View events:
Logs on the bastion are an attacker’s first target. Set up remote shipping as early as possible — otherwise, on compromise, you lose the incident history.
Port Forwarding and Tunnels Through the Bastion
The bastion’s main job is to give access to internal machines without exposing their ports. Three ways to do it:
1. Port forwarding via SSH tunnel:
Now local port 5432 on your machine proxies to PostgreSQL on 10.0.1.5.
2. SOCKS proxy for access to all internal hosts:
Point your browser or proxychains at 127.0.0.1:1080.
3. Reverse tunnel for accessing your local machine from the bastion network:
This lets someone reach a local service on port 9090 via the bastion.
For persistent tunnels, use autossh or configure ~/.ssh/config:
Now ssh bastion is all you need to connect, and tunnels are built with a single command.
For team operations, store keys in 1Password or HashiCorp Vault, and grant bastion access through ephemeral sessions with expiring tokens. This reduces the risk of key compromise.