Skip to content

Jump Hosts (Bastion Servers)

Overview

Jump hosts (bastion hosts) provide controlled access to internal networks. Instead of exposing internal servers directly, all access goes through the hardened jump host.

┌──────────────────────────────────────────────────────────────────────────┐
│                         Jump Host Architecture                            │
│                                                                           │
│   Internet          DMZ              Internal Network                     │
│                                                                           │
│   ┌─────┐      ┌─────────┐      ┌─────────┐  ┌─────────┐                │
│   │ You │─────▶│ Bastion │─────▶│  Web    │  │   DB    │                │
│   └─────┘      │  (jump) │      │ Server  │  │ Server  │                │
│                └─────────┘      └─────────┘  └─────────┘                │
│                     │           ┌─────────┐  ┌─────────┐                │
│                     └──────────▶│  App    │  │  Redis  │                │
│                                 │ Server  │  │ Server  │                │
│                                 └─────────┘  └─────────┘                │
│                                                                           │
│   Direct access to internal servers: ❌                                  │
│   Access through bastion: ✅                                             │
│                                                                           │
└──────────────────────────────────────────────────────────────────────────┘

The modern, simple way to use jump hosts.

Basic Usage

ssh -J jump_host destination
ssh -J bastion.example.com internal-server

With User and Port

ssh -J jumpuser@bastion.example.com:2222 admin@internal-server

Multiple Jumps

ssh -J jump1,jump2,jump3 destination
ssh -J bastion.example.com,internal-jump.local admin@deep-server

SSH Config

# ~/.ssh/config
Host bastion
    HostName bastion.example.com
    User jumpuser
    IdentityFile ~/.ssh/bastion_key

Host internal-*
    ProxyJump bastion
    User admin
    IdentityFile ~/.ssh/internal_key

Host internal-web
    HostName 10.0.0.10

Host internal-db
    HostName 10.0.0.20

Host internal-app
    HostName 10.0.0.30

Usage becomes simple:

ssh internal-web    # Automatically jumps through bastion
ssh internal-db     # Same
scp file.txt internal-app:/home/admin/   # Also works

ProxyCommand (Legacy)

Before ProxyJump, ProxyCommand was used:

ssh -o ProxyCommand="ssh -W %h:%p jumphost" destination

SSH Config with ProxyCommand

Host internal-*
    ProxyCommand ssh -W %h:%p bastion.example.com

Difference

Method Introduced Syntax Features
ProxyJump OpenSSH 7.3 -J host Simple, chainable
ProxyCommand Older -o ProxyCommand=... Flexible, scriptable

Use ProxyJump unless you need custom logic.

Agent Forwarding vs ProxyJump

Agent Forwarding (Risky)

ssh -A bastion.example.com
# Then from bastion:
ssh internal-server

Problem: Anyone with root on bastion can use your agent.

ProxyJump (Safer)

ssh -J bastion.example.com internal-server

Better: Your keys never touch the bastion server.

File Transfer Through Jump

SCP

scp -J bastion.example.com file.txt admin@internal:/home/admin/
scp -J bastion.example.com admin@internal:/var/log/app.log ./

Rsync

rsync -avz -e "ssh -J bastion.example.com" /local/dir/ admin@internal:/remote/dir/

SFTP

sftp -J bastion.example.com admin@internal

Port Forwarding Through Jump

Local Forward

Access internal service through bastion:

ssh -J bastion.example.com -L 5432:localhost:5432 admin@internal-db
# localhost:5432 → bastion → internal-db:5432

With Config

Host internal-db
    HostName 10.0.0.20
    User admin
    ProxyJump bastion
    LocalForward 5432 localhost:5432

Remote Forward

ssh -J bastion.example.com -R 8080:localhost:3000 admin@internal-web

Multiple Jump Hosts

Chained Jumps

ssh -J jump1,jump2 destination
You → jump1 → jump2 → destination

Config for Deep Networks

# ~/.ssh/config
Host bastion
    HostName bastion.example.com
    User admin

Host internal-jump
    HostName 10.0.0.1
    ProxyJump bastion
    User admin

Host deep-server
    HostName 192.168.10.50
    ProxyJump internal-jump
    User app
ssh deep-server
# You → bastion → internal-jump → deep-server

Bastion Server Setup

Hardened Configuration

# /etc/ssh/sshd_config on bastion

# Listen on standard port
Port 22

# Key-only authentication
PasswordAuthentication no
PubkeyAuthentication yes

# No root login
PermitRootLogin no

# Limit users
AllowUsers jumpuser admin

# Allow forwarding (needed for jump)
AllowTcpForwarding yes
AllowAgentForwarding no    # Don't allow agent forwarding

# Minimal features
X11Forwarding no
PermitTunnel no

# Logging
LogLevel VERBOSE

# Idle timeout
ClientAliveInterval 300
ClientAliveCountMax 2

# Restrict tunneling to internal network
Match User jumpuser
    PermitOpen 10.0.0.0/8:22 192.168.0.0/16:22
    AllowTcpForwarding yes
    PermitTTY no
    ForceCommand /bin/false

Jump-Only User

User that can only be used for jumping:

# Create user
useradd -m -s /usr/sbin/nologin jumpuser

# SSH key
mkdir /home/jumpuser/.ssh
cat > /home/jumpuser/.ssh/authorized_keys << 'EOF'
restrict,port-forwarding,permitopen="10.0.0.*:22" ssh-ed25519 AAAAC3... user@client
EOF

chown -R jumpuser:jumpuser /home/jumpuser/.ssh
chmod 700 /home/jumpuser/.ssh
chmod 600 /home/jumpuser/.ssh/authorized_keys

Session Recording

For audit compliance:

Using script

# On bastion, record sessions
Match User *
    ForceCommand /usr/bin/script -f -q /var/log/ssh-sessions/%u-%Y%m%d%H%M%S.log

Using asciinema

# Wrapper script
#!/bin/bash
asciinema rec --quiet --command "$SHELL" /var/log/sessions/$(whoami)-$(date +%Y%m%d%H%M%S).cast

Monitoring and Alerts

Failed Login Alerts

# /etc/fail2ban/jail.local
[sshd]
enabled = true
port = ssh
maxretry = 3
bantime = 3600

Session Logging

# Check who's connected
who
w

# Recent logins
last -a

# Failed logins
lastb

# Auth logs
journalctl -u sshd | tail -100

Alternatives

Commercial Solutions

  • AWS SSM Session Manager: Agentless, no SSH needed
  • Teleport: SSH certificates, audit logging, MFA
  • Boundary (HashiCorp): Zero-trust access

VPN

VPN provides network-level access vs SSH's application-level:

Aspect Jump Host VPN
Access control Per-user, per-server Network-wide
Audit Excellent Varies
Performance Good Better for bulk
Setup Simple More complex
Attack surface SSH only VPN + services

Troubleshooting

Can't Connect Through Jump

# Test direct connection to bastion
ssh bastion

# Test connectivity from bastion to internal
ssh bastion "nc -zv internal-server 22"

# Verbose output
ssh -v -J bastion internal-server

Permission Denied

# Check keys are accessible
ssh-add -l

# Check config
ssh -G internal-server | grep -i proxy

# Ensure both hop keys work
ssh -i ~/.ssh/bastion_key bastion
ssh -i ~/.ssh/internal_key internal-server  # directly if possible

Slow Connection

# Enable multiplexing on bastion connection
Host bastion
    ControlMaster auto
    ControlPath ~/.ssh/sockets/%r@%h-%p
    ControlPersist 600

Timeout

# Add keep-alive
Host bastion
    ServerAliveInterval 60
    ServerAliveCountMax 3