Skip to content

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.

# Update and install the minimum utility set
sudo apt update && sudo apt upgrade -y
sudo apt install -y fail2ban ufw curl htop
Tip

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:

sudo adduser deploy
sudo usermod -aG sudo deploy

SSH Configuration: Keys, Port, Disable Passwords

Generate a key on your workstation if you don’t have one yet:

ssh-keygen -t ed25519 -C "bastion-access"
ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@bastion_ip

On the bastion, edit /etc/ssh/sshd_config:

Port 22220
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
MaxSessions 5
AllowUsers deploy
Warning

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:

sudo sshd -t
sudo systemctl restart sshd

sshd_config keys and what they do:

ParameterValuePurpose
Port22220Non-standard port, reduces log noise
PermitRootLoginnoBlocks direct root login
PasswordAuthenticationnoKeys only
MaxAuthTries3Limits authentication attempts
AllowUsersdeployWhitelist of allowed users

Firewall and Access Restrictions

UFW is the simplest way to close everything unnecessary:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22220/tcp comment "SSH bastion"
sudo ufw enable

If the bastion is only for your IP, restrict it further:

sudo ufw allow from YOUR_IP to any port 22220 proto tcp
Note

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:

sudo sysctl -w net.ipv4.ip_forward=1

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:

sudo apt install fail2ban -y
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

In /etc/fail2ban/jail.local:

[sshd]
enabled = true
port = 22220
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
findtime = 600

Restart:

sudo systemctl restart fail2ban
sudo systemctl enable fail2ban
sudo fail2ban-client status sshd

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:

# Add to /etc/rsyslog.conf:
*.* @logserver_ip:514

For user action auditing, attach auditd:

sudo apt install auditd -y
sudo auditctl -w /etc/ssh/sshd_config -p wa -k ssh_config
sudo auditctl -w /home -p r -k user_files

View events:

sudo ausearch -k ssh_config
sudo ausearch -k user_files
Warning

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:

ssh -L 5432:10.0.1.5:5432 deploy@bastion_ip -p 22220

Now local port 5432 on your machine proxies to PostgreSQL on 10.0.1.5.

2. SOCKS proxy for access to all internal hosts:

ssh -D 1080 deploy@bastion_ip -p 22220

Point your browser or proxychains at 127.0.0.1:1080.

3. Reverse tunnel for accessing your local machine from the bastion network:

ssh -R 9090:localhost:8080 deploy@bastion_ip -p 22220

This lets someone reach a local service on port 9090 via the bastion.

For persistent tunnels, use autossh or configure ~/.ssh/config:

Host bastion
    HostName bastion_ip
    Port 22220
    User deploy
    IdentityFile ~/.ssh/id_ed25519
    ServerAliveInterval 60
    ServerAliveCountMax 3

Now ssh bastion is all you need to connect, and tunnels are built with a single command.

Tip

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.