Set up SSH bastion host with jump server configuration for secure network access

Intermediate 25 min Apr 18, 2026 697 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Configure an SSH bastion host to secure access to private networks, implementing jump server functionality with key-based authentication and access controls for enhanced security.

Prerequisites

  • Root or sudo access to the bastion host server
  • SSH client installed on user machines
  • Basic understanding of SSH key management

What this solves

An SSH bastion host (or jump server) acts as a secure gateway between public networks and your private infrastructure. Instead of exposing multiple servers directly to the internet, you route all SSH connections through a single hardened bastion host, reducing attack surface and centralizing access control.

Step-by-step installation

Update system packages and install OpenSSH server

Start by updating your system and installing the SSH server if not already present.

sudo apt update && sudo apt upgrade -y
sudo apt install -y openssh-server fail2ban ufw
sudo dnf update -y
sudo dnf install -y openssh-server fail2ban firewalld

Create dedicated bastion user accounts

Create separate user accounts for each person who needs bastion access. Never use shared accounts for security auditing.

sudo useradd -m -s /bin/bash bastion-admin
sudo useradd -m -s /bin/bash bastion-dev
sudo mkdir -p /home/bastion-admin/.ssh /home/bastion-dev/.ssh
sudo chmod 700 /home/bastion-admin/.ssh /home/bastion-dev/.ssh
sudo chown bastion-admin:bastion-admin /home/bastion-admin/.ssh
sudo chown bastion-dev:bastion-dev /home/bastion-dev/.ssh

Configure SSH server hardening

Harden the SSH configuration by disabling password authentication, changing the default port, and restricting access methods.

# Change default SSH port
Port 2222

# Protocol and security settings
Protocol 2
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
ChallengeResponseAuthentication no
UsePAM yes

# Connection settings
ClientAliveInterval 300
ClientAliveCountMax 2
MaxAuthTries 3
MaxStartups 3:30:10

# Allow specific users only
AllowUsers bastion-admin bastion-dev

# Disable unnecessary features
X11Forwarding no
AllowTcpForwarding yes
GatewayPorts no
PermitTunnel no

# Logging
SyslogFacility AUTH
LogLevel VERBOSE

Set up SSH key authentication

Add SSH public keys for each bastion user. Generate keys on client machines first, then add public keys to the bastion host.

# On client machine (generate if needed)
ssh-keygen -t ed25519 -C "user@example.com"

# Copy public key content, then on bastion host:
sudo tee /home/bastion-admin/.ssh/authorized_keys << 'EOF'
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGQn8QVTzKbcXh5iMqBqB2tUxI5g7VzJ0yA5V2nE5P4h admin@example.com
EOF

sudo chmod 600 /home/bastion-admin/.ssh/authorized_keys
sudo chown bastion-admin:bastion-admin /home/bastion-admin/.ssh/authorized_keys

Configure firewall rules

Set up firewall rules to allow SSH on the custom port while blocking unnecessary access.

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 2222/tcp comment 'SSH bastion'
sudo ufw enable
sudo ufw status
sudo systemctl enable --now firewalld
sudo firewall-cmd --permanent --remove-service=ssh
sudo firewall-cmd --permanent --add-port=2222/tcp
sudo firewall-cmd --reload
sudo firewall-cmd --list-all

Configure Fail2ban for intrusion prevention

Set up Fail2ban to automatically block IP addresses that attempt multiple failed SSH connections.

[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
ignoreip = 127.0.0.1/8 203.0.113.0/24

[sshd]
enabled = true
port = 2222
logpath = /var/log/auth.log
banaction = iptables-multiport
sudo systemctl enable --now fail2ban
sudo systemctl status fail2ban

Restart SSH service with new configuration

Apply the SSH configuration changes by restarting the service.

Warning: Test the new SSH configuration before closing your current session. Open a new terminal and test connection on port 2222 first.
sudo sshd -t
sudo systemctl restart sshd
sudo systemctl enable sshd
sudo systemctl status sshd

Set up jump server configuration for users

Configure SSH client settings on user machines to use the bastion host for accessing private servers.

# Bastion host configuration
Host bastion
    HostName 203.0.113.10
    Port 2222
    User bastion-admin
    IdentityFile ~/.ssh/id_ed25519
    ServerAliveInterval 60
    ServerAliveCountMax 3

# Private server through bastion
Host private-server
    HostName 10.0.1.100
    User ubuntu
    ProxyJump bastion
    IdentityFile ~/.ssh/id_ed25519

# Wildcard for private network
Host 10.0.1.*
    User ubuntu
    ProxyJump bastion
    IdentityFile ~/.ssh/id_ed25519

Configure SSH agent forwarding

Enable SSH agent forwarding to use local SSH keys when jumping to private servers, avoiding the need to store keys on the bastion host.

# Add to existing configuration
AllowAgentForwarding yes
# Update bastion host config
Host bastion
    HostName 203.0.113.10
    Port 2222
    User bastion-admin
    IdentityFile ~/.ssh/id_ed25519
    ForwardAgent yes
    ServerAliveInterval 60

Set up logging and monitoring

Configure comprehensive logging for security auditing and monitoring of bastion host access.

# Bastion host logging
auth,authpriv.*    /var/log/bastion-auth.log
:programname, isequal, "sshd"    /var/log/bastion-ssh.log
& stop
sudo systemctl restart rsyslog
sudo touch /var/log/bastion-auth.log /var/log/bastion-ssh.log
sudo chmod 640 /var/log/bastion-*.log

Create user access control script

Implement a script to manage user access and provide session logging for compliance.

#!/bin/bash
# Bastion host session logger

USER=$(whoami)
CLIENT_IP=${SSH_CLIENT%% *}
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
SESSION_ID="${USER}-${RANDOM}"

echo "[$TIMESTAMP] User $USER connected from $CLIENT_IP (Session: $SESSION_ID)" >> /var/log/bastion-sessions.log

# Display access banner
cat << EOF
===============================================
    AUTHORIZED ACCESS ONLY
    Bastion Host - example.com
    Session ID: $SESSION_ID
    All activities are logged and monitored
===============================================

EOF

# Start shell with logging
script -a -f -q /var/log/bastion-sessions/$SESSION_ID.log

echo "[$TIMESTAMP] User $USER disconnected (Session: $SESSION_ID)" >> /var/log/bastion-sessions.log
sudo chmod +x /usr/local/bin/bastion-session
sudo mkdir -p /var/log/bastion-sessions
sudo touch /var/log/bastion-sessions.log

Configure user shell to use session script

Set the bastion session script as the default shell for bastion users to ensure all sessions are logged.

sudo chsh -s /usr/local/bin/bastion-session bastion-admin
sudo chsh -s /usr/local/bin/bastion-session bastion-dev

Verify your setup

Test your bastion host configuration from a client machine.

# Test direct bastion connection
ssh -p 2222 bastion-admin@203.0.113.10

# Test jump connection to private server
ssh private-server

# Test with verbose output for debugging
ssh -v private-server

# Check active connections on bastion
sudo ss -tuln | grep :2222
sudo who

# Review logs
sudo tail -f /var/log/bastion-auth.log
sudo tail -f /var/log/bastion-sessions.log

Common issues

SymptomCauseFix
Connection refused on port 2222Firewall blocking custom SSH portCheck firewall rules: sudo ufw status or sudo firewall-cmd --list-all
Permission denied (publickey)SSH key not properly configuredVerify key permissions: chmod 600 ~/.ssh/authorized_keys and ownership
ProxyJump connection failsSSH agent not running or forwarding disabledStart SSH agent: eval $(ssh-agent) and add key: ssh-add ~/.ssh/id_ed25519
Too many authentication failuresMultiple SSH keys being triedSpecify exact key in SSH config: IdentitiesOnly yes
Fail2ban not blocking attacksWrong log path or port configurationCheck Fail2ban status: sudo fail2ban-client status sshd

Next steps

Running this in production?

Want this handled for you? Setting this up once is straightforward. Keeping it patched, monitored, backed up and performant across environments is the harder part. See how we run infrastructure like this for European teams.

Automated install script

Run this to automate the entire setup

Don't want to manage this yourself?

We handle infrastructure for businesses that depend on uptime. Fully managed, with one fixed contact who knows your setup.

You get one fixed contact who knows your setup

Rotterdam 01:26 · reachable in a message, no ticket form