SSH bastion hosts are often treated as mere jump boxes, but their true power lies in their ability to centralize and audit all SSH access to your internal network.

Let’s see one in action. Imagine this scenario:

User alice on workstation wants to SSH into appserver (internal IP 10.0.0.10).

  1. alice connects to bastion.example.com:

    ssh alice@bastion.example.com
    
    • alice is authenticated against bastion.example.com (e.g., via SSH key or password).
  2. From bastion.example.com, alice connects to appserver:

    ssh user@10.0.0.10
    
    • alice is authenticated again to appserver (e.g., via a different SSH key, often managed by ssh-agent forwarding, or a password).

The bastion.example.com server acts as a single point of entry. All traffic destined for appserver (or any other internal server) must first pass through the bastion. This is where the magic happens.

The Problem Solved: Granular Access Control and Auditing

Without a bastion, you’d typically expose SSH ports on every internal server to your external network, or you’d need complex VPN configurations for each user. This is a nightmare for:

  • Security: Each exposed SSH port is a potential attack vector. If one server is compromised, an attacker has direct access to your internal network.
  • Auditing: Tracking who accessed which internal server, when, and from where becomes incredibly difficult.
  • Management: Managing firewall rules and user access across dozens or hundreds of servers is unsustainable.

A bastion host centralizes these concerns.

Internal Mechanics: How it Works

At its core, a bastion host is just a regular Linux server. The security and functionality come from its configuration and how you route traffic.

  1. Network Placement: The bastion host resides in a public-facing network segment (e.g., a DMZ). It has at least one network interface facing the internet and another facing your internal network.
  2. SSH Daemon (sshd): The bastion runs sshd and is configured to allow external connections. Crucially, it’s not configured to allow direct SSH access to internal servers from the internet.
  3. Internal Routing: The bastion has routes to your internal network. This is typically achieved via its internal network interface.
  4. SSH Client on Bastion: Users connect to the bastion, and then from the bastion, they initiate new SSH connections to internal hosts.
  5. SSH Agent Forwarding (Common Pattern): To avoid storing private keys on the bastion, users often use ssh-agent forwarding. When alice connects to the bastion with ssh -A alice@bastion.example.com, her local SSH agent (which holds her private key) is available to the sshd process on the bastion. When alice then SSHes from the bastion to appserver, the bastion’s sshd uses the forwarded agent to authenticate alice to appserver using her original private key. This means her private key never touches the bastion’s disk.
  6. authorized_keys Management: The bastion needs alice’s public key in its ~/.ssh/authorized_keys file to allow her initial login. Internal servers also need alice’s public key (or a key managed by her agent) for her second-stage login.

Configuration Snippets

On the Bastion Host (/etc/ssh/sshd_config):

Port 22
Protocol 2
HostKey /etc/ssh/ssh_host_rsa_key
HostKey /etc/ssh/ssh_host_ecdsa_key
HostKey /etc/ssh/ssh_host_ed25519_key

# Allow connections from anywhere, but we'll filter later
ListenAddress 0.0.0.0

PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys

# Crucial for agent forwarding
AllowAgentForwarding yes
AllowTcpForwarding yes
PermitTunnel no # Generally not needed for a simple bastion

# Log all connections and commands (with ForceCommand, see below)
SyslogFacility AUTH
LogLevel INFO

Firewall Rules (e.g., iptables on the bastion):

This is paramount. The bastion should only allow SSH (port 22) from the internet. It should allow outgoing SSH (port 22) to internal servers.

# Allow inbound SSH from anywhere
iptables -A INPUT -p tcp --dport 22 -m state --state NEW,ESTABLISHED -j ACCEPT

# Allow outbound SSH to internal network (e.g., 10.0.0.0/8)
iptables -A OUTPUT -p tcp --dport 22 -d 10.0.0.0/8 -m state --state NEW,ESTABLISHED -j ACCEPT

# Drop all other inbound traffic
iptables -P INPUT DROP

# Allow established/related outbound traffic
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# Drop all other outbound traffic (restrictive, adjust as needed)
iptables -P OUTPUT DROP

# Allow loopback
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT

Restricting User Actions (Optional but Recommended)

To make the bastion even more secure, you can use ForceCommand in sshd_config to limit what users can do. This is often combined with authorized_keys options.

On the bastion, in ~alice/.ssh/authorized_keys:

command="~/.ssh/restricted_commands.sh",no-port-forwarding,no-pty,no-user-rc,no-agent-forwarding,no-X11-forwarding ssh-rsa AAAA... alice@workstation

And ~/.ssh/restricted_commands.sh would be a script that checks if the command is an allowed SSH connection to a specific internal host, and if so, executes it. This is complex and often overkill if you trust your users and have strong auditing.

A simpler approach for basic auditing is to ensure sshd is configured to log commands, or use tools like auditd or specialized session recording software.

The most overlooked aspect of bastion host security is often the configuration of the internal servers. If an internal server’s sshd allows root login, or uses weak ciphers, or is unpatched, the bastion becomes less of a shield and more of a slightly inconvenient stepping stone. The bastion’s security is only as strong as the weakest link in the chain after the jump.

The next problem you’ll likely encounter is managing user access and SSH keys across multiple internal servers efficiently.

Want structured learning?

Take the full Ssh course →