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).
-
aliceconnects tobastion.example.com:ssh alice@bastion.example.comaliceis authenticated againstbastion.example.com(e.g., via SSH key or password).
-
From
bastion.example.com,aliceconnects toappserver:ssh user@10.0.0.10aliceis authenticated again toappserver(e.g., via a different SSH key, often managed byssh-agentforwarding, 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.
- 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.
- SSH Daemon (
sshd): The bastion runssshdand is configured to allow external connections. Crucially, it’s not configured to allow direct SSH access to internal servers from the internet. - Internal Routing: The bastion has routes to your internal network. This is typically achieved via its internal network interface.
- SSH Client on Bastion: Users connect to the bastion, and then from the bastion, they initiate new SSH connections to internal hosts.
- SSH Agent Forwarding (Common Pattern): To avoid storing private keys on the bastion, users often use
ssh-agentforwarding. Whenaliceconnects to the bastion withssh -A alice@bastion.example.com, her local SSH agent (which holds her private key) is available to thesshdprocess on the bastion. Whenalicethen SSHes from the bastion toappserver, the bastion’ssshduses the forwarded agent to authenticatealicetoappserverusing her original private key. This means her private key never touches the bastion’s disk. authorized_keysManagement: The bastion needsalice’s public key in its~/.ssh/authorized_keysfile to allow her initial login. Internal servers also needalice’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.