SSH Bastion / Jump Host
Set up an SSH bastion (jump host) to securely access private servers through a single public entry point
An SSH bastion (or jump host) is a single server exposed to the internet that acts as a gateway to access servers on a private network. Instead of exposing all servers publicly, you SSH into the bastion first, then jump to internal servers.
Internet → Bastion (public IP) → Private Server 1
→ Private Server 2
→ Private Server 3Requirements
- One server with a public IP (the bastion)
- One or more servers reachable from the bastion (private network)
- SSH key authentication (passwords strongly discouraged)
Configure the bastion server
On the bastion, harden SSH and allow only key auth:
sudo nano /etc/ssh/sshd_configPermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AllowTcpForwarding yes
GatewayPorts no
X11Forwarding no
MaxAuthTries 3Restart SSH:
sudo systemctl restart sshConnect through the bastion (one-liner)
ssh -J user@bastion-ip user@private-server-ipThe -J flag (ProxyJump) connects to bastion-ip first, then jumps to private-server-ip through it.
Example:
ssh -J admin@203.0.113.1 ubuntu@10.0.0.5Configure ~/.ssh/config (recommended)
Add this to your local ~/.ssh/config to avoid typing the jump every time:
# Bastion
Host bastion
HostName 203.0.113.1
User admin
IdentityFile ~/.ssh/bastion_key
ServerAliveInterval 60
# Private servers (accessed via bastion)
Host web1
HostName 10.0.0.5
User ubuntu
IdentityFile ~/.ssh/web_key
ProxyJump bastion
Host db1
HostName 10.0.0.10
User ubuntu
IdentityFile ~/.ssh/db_key
ProxyJump bastion
# Wildcard: all 10.0.0.x via bastion
Host 10.0.0.*
User ubuntu
ProxyJump bastionNow you can connect simply with:
ssh web1
ssh db1
ssh 10.0.0.20Copy files through the bastion
SCP through a jump host:
scp -J admin@bastion-ip /local/file ubuntu@10.0.0.5:/remote/pathWith ~/.ssh/config configured:
scp /local/file web1:/remote/pathRsync through a bastion:
rsync -avz -e "ssh -J admin@bastion-ip" /local/dir ubuntu@10.0.0.5:/remote/dirForward a port through the bastion
Access a service on a private server (e.g., a web UI) on your local machine:
# Tunnel port 8080 on private server → localhost:8080
ssh -J admin@bastion-ip -L 8080:10.0.0.5:8080 ubuntu@10.0.0.5Then open http://localhost:8080 in your browser.
With ~/.ssh/config:
ssh -L 8080:localhost:8080 web1Agent forwarding (use your local key on private servers)
Instead of copying your private key to the bastion, use SSH agent forwarding so the bastion proxies auth challenges back to your local machine:
On your local machine:
# Add key to agent
ssh-add ~/.ssh/web_key
# Connect with agent forwarding
ssh -A admin@bastion-ip
# From bastion, you can now ssh to private servers using your local key
ssh ubuntu@10.0.0.5In ~/.ssh/config:
Host bastion
HostName 203.0.113.1
User admin
ForwardAgent yesAgent forwarding gives any root user on the bastion the ability to use your keys. Only enable it on trusted bastions. Consider using ProxyJump instead, it doesn't forward your agent to the bastion.
Bastion hardening checklist
# Disable password auth
sudo sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
# Allow only specific users to SSH
echo "AllowUsers admin" | sudo tee -a /etc/ssh/sshd_config
# Rate limit SSH connections
sudo ufw limit ssh
# Install fail2ban
sudo apt install fail2ban -y
# Restrict bastion to only jump (no shell)
# For users that should only jump and not use the bastion directly:
# Set their shell to /usr/sbin/nologin and use ForceCommandRestrict a user to jump-only (no shell)
# Create a jump-only user
sudo useradd -m -s /usr/sbin/nologin jumpuser
sudo mkdir -p /home/jumpuser/.ssh
sudo cp /root/.ssh/authorized_keys /home/jumpuser/.ssh/
sudo chown -R jumpuser:jumpuser /home/jumpuser/.sshIn /etc/ssh/sshd_config:
Match User jumpuser
AllowTcpForwarding yes
X11Forwarding no
PermitTTY no
ForceCommand /bin/falseThis user can only be used as a ProxyJump target, no interactive shell.
Audit bastion access
Log all connections through the bastion:
# View all SSH logins
sudo journalctl -u ssh | grep "Accepted"
# Real-time monitoring
sudo tail -f /var/log/auth.log | grep sshd