SSH bastion host setup

An SSH bastion host, sometimes called a jump host, provides a controlled entry point into servers that should not be directly reachable from the internet.

The basic idea is simple:

Internet
    |
    | SSH
    v
Bastion
    |
    | SSH
    +----> Internal server 1
    +----> Internal server 2
    +----> Internal server 3

Instead of exposing SSH on every internal server, the firewall exposes SSH to the bastion and the bastion provides the controlled path to the internal systems.

That can significantly simplify firewall rules, logging and access management.

But there is an important catch.

The bastion becomes a high-value security boundary. If it is poorly configured, you haven’t removed the problem. You have concentrated it into one particularly important server.

This guide takes a practical approach to building one, including the SSH configuration, ProxyJump, key management, destination restrictions, logging and testing.

1. Decide whether you actually need a bastion

Before building one, look at how the environment is hosted.

A self-managed bastion isn’t necessarily the best answer for every cloud environment.

For example, AWS Systems Manager Session Manager can provide interactive access to managed nodes without opening inbound ports or maintaining traditional bastion hosts and SSH keys. Azure Bastion provides SSH connectivity to Azure VMs without requiring a public IP on the VM. Those approaches move part of the access-control problem into the cloud platform rather than leaving you responsible for maintaining another internet-facing Linux server.

For AWS environments, Session Manager is therefore worth considering before deploying a traditional bastion.

For Azure VMs, Azure Bastion is another option, particularly where browser-based administration and private VM connectivity are useful.

A traditional SSH bastion still makes sense when you:

  • Need standard SSH tooling
  • Manage on-premises Linux servers
  • Have hybrid infrastructure
  • Need native SSH/SCP workflows
  • Need a consistent jump point across multiple networks
  • Don’t have an equivalent managed access service

The important question is not “How do I build a bastion?”

It is:

What is the simplest architecture that gives administrators controlled access without exposing the servers themselves?

2. Decide where the bastion sits

A typical design places the bastion in a DMZ or dedicated management subnet.

For example:

                         Internet
                            |
                       Firewall
                            |
                            | TCP/22
                            v
                    +----------------+
                    | SSH Bastion    |
                    | Public IP      |
                    +----------------+
                            |
                     Management FW
                            |
              +-------------+-------------+
              |             |             |
              v             v             v
           Server 1      Server 2      Server 3

The important part isn’t whether you call the network a DMZ.

The important part is that the bastion has only the network access it actually needs.

For example, if administrators only need SSH access to a particular server subnet:

Bastion
   |
   +---- TCP/22 ---> 10.10.20.0/24

There is little reason for the bastion to have unrestricted access to:

10.0.0.0/8
172.16.0.0/12
192.168.0.0/16

just because those networks happen to exist.

Firewall rules should define the destinations explicitly.

The bastion should also have restricted internet egress. A compromised bastion with unrestricted outbound access becomes a much more useful platform for an attacker.

3. Provision a minimal host

Start with a clean, supported Linux distribution.

Install only what is required.

At a minimum:

  • Current supported OS
  • OpenSSH server
  • System logging
  • Time synchronisation
  • Monitoring or management agent if required
  • Security tooling appropriate to your environment

Avoid turning the bastion into an administration workstation.

It shouldn’t become the place where someone installs:

  • Development tools
  • Web browsers
  • Random troubleshooting utilities
  • Compilers
  • Applications
  • Scripts downloaded from the internet

The more functionality you add, the more you have to patch and defend.

Give it a stable identity

Use:

  • A stable DNS name
  • A fixed private address
  • A controlled public address or firewall NAT
  • Monitoring
  • Centralised logging

For example:

bastion.example.com

Administrators should connect to the bastion by name rather than memorising an IP address.

4. Harden SSH

The SSH daemon configuration is the centre of the bastion.

On most Linux systems, this is:

/etc/ssh/sshd_config

A sensible starting point might look like this:

PermitRootLogin no

PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes

AllowGroups ssh-bastion

MaxAuthTries 3
LoginGraceTime 30

ClientAliveInterval 300
ClientAliveCountMax 2

X11Forwarding no
AllowAgentForwarding no
PermitTunnel no

These settings should still be tested against the OpenSSH version and authentication architecture you are actually deploying. The sshd_config manual page is the authoritative reference for what each option does in a given release.

What these settings do

PermitRootLogin no

Prevents direct SSH login as root.

Administrators should authenticate as their own accounts and use controlled privilege escalation where required.

PasswordAuthentication no

Removes normal password authentication from SSH.

KbdInteractiveAuthentication no

Disables keyboard-interactive authentication. Be careful with this setting if your MFA implementation relies on keyboard-interactive authentication. In that case, your authentication design may require it.

On older OpenSSH versions this directive was called ChallengeResponseAuthentication. If you’re working from an older configuration file or an older guide, that’s the same setting under its previous name.

PubkeyAuthentication yes

Explicitly enables public-key authentication.

AllowGroups ssh-bastion

Restricts SSH access to members of the ssh-bastion group.

For example:

sudo groupadd ssh-bastion
sudo usermod -aG ssh-bastion daniel

The exact account-management process should normally be integrated with your identity platform rather than manually maintained on every bastion.

MaxAuthTries 3

Limits authentication attempts within a connection.

LoginGraceTime 30

Gives a client 30 seconds to complete authentication.

ClientAliveInterval 300

Causes the server to send a request if no data has been received for 300 seconds.

ClientAliveCountMax 2

After two unanswered requests, the server can terminate the session.

X11Forwarding no

Disables X11 forwarding where it isn’t required.

AllowAgentForwarding no

Prevents SSH agent forwarding through the bastion.

PermitTunnel no

Prevents SSH tunnelling using the SSH tunnel device.

OpenSSH provides many more forwarding controls, including AllowTcpForwarding and DisableForwarding, so the final configuration should reflect exactly what the bastion is intended to permit.

5. Test the configuration before restarting SSH

This is one of the easiest ways to lock yourself out of a remote server.

Before restarting sshd, validate the configuration:

sudo sshd -t

If the command returns no output, the configuration syntax has passed the basic validation.

You can also inspect the effective configuration:

sudo sshd -T

sshd -T reads the effective configuration, so the configuration needs to be valid before it can provide useful output. In situations where settings depend on the connecting user, address or host, -C can be used to provide that connection context.

Then reload the SSH service:

sudo systemctl reload ssh      # Debian, Ubuntu
sudo systemctl reload sshd     # RHEL, Rocky, Alma, SUSE

On newer Ubuntu releases using SSH socket activation, changes involving the listening socket, such as changing the SSH port, can also require attention to ssh.socket rather than only the ssh service.

Keep your existing administrative session open while testing the new configuration.

Open a second terminal and establish another SSH connection before closing the original session.

That gives you a way back if you have accidentally restricted your own access.

6. Use ProxyJump rather than logging into the bastion first

This is one of the most useful parts of a modern SSH bastion design.

You don’t need to do this:

ssh admin@bastion.example.com

bastion$ ssh admin@10.10.20.11

Instead, use ProxyJump.

For a one-off connection:

ssh -J bastion.example.com admin@10.10.20.11

Or configure it permanently in your local SSH client:

Host prod-web01
    HostName 10.10.20.11
    User admin
    ProxyJump bastion.example.com

You can then simply run:

ssh prod-web01

OpenSSH establishes the connection to the jump host and uses it to reach the final destination. The ssh_config manual page documents ProxyJump and the related client options.

The private key used to authenticate to the destination remains on the administrator’s workstation. It does not need to be copied onto the bastion.

That is a major reason to prefer ProxyJump over logging into the bastion and then running another SSH command.

7. Be careful with SSH agent forwarding

Agent forwarding is convenient because the bastion can use your local SSH agent without you copying your private key to the server.

The problem is that the remote system can potentially interact with your forwarded agent while your session is active.

For a bastion, I would therefore start with:

AllowAgentForwarding no

and use ProxyJump instead.

This gives you the jump-host functionality without making the bastion part of the trust boundary for your private SSH agent.

There are environments where agent forwarding is required, but it should be a deliberate exception rather than the default.

8. Manage SSH keys properly

Every administrator should have their own identity.

Do not share a private key such as:

admin-key.pem

between five people.

A better arrangement is:

Daniel     -> Daniel's key
Ryan       -> Ryan's key
Admin 3    -> Admin 3's key

Generate an ED25519 key where supported:

ssh-keygen -t ed25519 -C "daniel@example.com"

Protect the private key with a passphrase.

The private key should remain on the administrator’s workstation or approved credential-management system.

Only the public key should be installed on the server.

When someone leaves the organisation, their access needs to be removed promptly.

That sounds obvious, but static SSH keys become difficult to manage when there are dozens or hundreds of servers.

9. Consider SSH certificates for larger environments

SSH certificates solve a different problem from ordinary SSH keys.

Instead of placing every user’s public key into authorized_keys, you establish an SSH certificate authority (CA).

The model becomes:

                  SSH CA
                    |
             signs user keys
                    |
       +------------+------------+
       |            |            |
     User 1       User 2       User 3
       |            |            |
       +------------+------------+
                    |
                 Bastion

The CA has a private signing key that should be protected carefully.

The bastion only needs the CA’s public key:

TrustedUserCAKeys /etc/ssh/ca.pub

An administrator generates their normal keypair:

ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519

The CA can then sign the user’s public key:

ssh-keygen -s /path/to/ssh-user-ca \
    -I daniel-2026-09-22 \
    -n daniel \
    -V +8h \
    ~/.ssh/id_ed25519.pub

This creates a certificate alongside the public key, normally:

~/.ssh/id_ed25519-cert.pub

The important parts are:

  • -s is the path to the CA private signing key
  • -I gives the certificate an identity
  • -n defines the principal
  • -V defines the validity period

The certificate could therefore be valid for only eight hours.

That changes the access model considerably.

Instead of a permanent public key sitting on the bastion for years, the user can receive a short-lived certificate when they need access.

The bastion trusts the CA through:

TrustedUserCAKeys /etc/ssh/ca.pub

OpenSSH then verifies that the presented certificate was signed by that CA and contains an appropriate principal. The ssh-keygen manual page covers certificate signing, principals and validity intervals in detail.

For larger environments, this can be much easier to manage than distributing individual keys to every server.

The difficult part isn’t the SSH command. It is protecting the CA private key and building the process that issues, renews and revokes certificates.

If you don’t already have a certificate-management workflow, ordinary per-user keys may be easier to operate until you do.

10. Restrict where the bastion can connect

This is where the network firewall becomes particularly important.

Suppose the bastion should only reach:

10.10.20.0/24
10.10.30.0/24

on TCP port 22.

Configure the firewall accordingly.

For example:

Source:      Bastion
Destination: 10.10.20.0/24
Service:     TCP/22
Action:      Allow

Then:

Source:      Bastion
Destination: Any
Service:     Any
Action:      Deny

The exact firewall syntax depends on your platform, but the principle is the same.

Don’t confuse PermitOpen with shell access restrictions

OpenSSH also provides:

PermitOpen 10.10.20.11:22
PermitOpen 10.10.20.12:22

But there is an important limitation.

PermitOpen controls the destinations available through TCP forwarding. It does not mean that a user who receives an interactive shell on the bastion can only run:

ssh 10.10.20.11

They could still run:

ssh 10.10.50.25

if the network allows it.

So if you need to restrict where an interactive user can connect, use the controls appropriate to that access model:

  • Outbound firewall rules
  • Restricted shells
  • ForceCommand
  • Separate bastions or access groups
  • A dedicated access proxy

The firewall is often the cleanest enforcement point because it remains effective even if the user launches another network tool rather than the SSH client.

11. Logging that is actually useful

At minimum, you want to know:

  • Who authenticated
  • When they authenticated
  • Where they came from
  • Whether authentication failed
  • When the session ended
  • Which bastion they used

On Linux, SSH authentication events are normally available through the system logging framework.

For example:

sudo journalctl -u ssh       # Debian, Ubuntu
sudo journalctl -u sshd      # RHEL, Rocky, Alma, SUSE

or, depending on the distribution:

sudo tail -f /var/log/auth.log

Centralise these logs rather than leaving the only copy on the bastion.

Forward them to your SIEM or central logging platform.

What about command logging?

SSH authentication logs don’t tell you everything an administrator typed after they received a shell.

If you need session recording, look at tools designed specifically for that purpose.

tlog is one Linux option for recording terminal sessions. Another approach is auditd, which can record process execution using execve rules.

There is an important distinction here:

auditd process execution logging is not the same thing as recording everything typed into a shell.

For example, shell built-ins and terminal interaction can make a simple execve audit trail incomplete as a record of what the administrator actually typed.

If you require full session visibility, use a terminal-session recording solution rather than assuming audit logs provide a complete transcript.

For higher-security environments, commercial privileged-access-management platforms can also provide session recording, approval workflows and just-in-time access.

12. Test the bastion and its security controls

Testing shouldn’t stop when an administrator successfully connects.

If the architecture says:

Internet
   |
 Bastion
   |
 Server

then test the server from the internet.

For example:

ssh admin@10.10.20.11

should not be possible from an external network.

The server firewall should only permit SSH from the bastion or another explicitly authorised management network.

Then test the intended path:

ssh -J bastion.example.com admin@10.10.20.11

That should work.

You should also test that the bastion cannot reach destinations it isn’t supposed to reach.

For example:

nc -vz 10.10.50.20 22

or:

timeout 5 bash -c '</dev/tcp/10.10.50.20/22'

The exact testing tool isn’t important. What matters is proving that the firewall rules actually enforce the architecture.

Test the authentication controls

Try password authentication:

ssh -o PubkeyAuthentication=no admin@bastion.example.com

This should fail if password authentication is disabled and no other interactive authentication method is configured.

Try root login:

ssh root@bastion.example.com

This should fail when:

PermitRootLogin no

is enforced.

Try an account that isn’t a member of:

ssh-bastion

It should be denied.

Then test an unauthorised destination from the bastion.

Finally, verify that a server cannot be reached directly from the external network.

These tests prove the security controls rather than simply proving that SSH works.

13. Keep the bastion patched and monitored

A bastion is an internet-facing security boundary.

Treat it accordingly.

Monitor:

  • SSH authentication failures
  • Successful logins
  • Unexpected source addresses
  • CPU and memory
  • Disk utilisation
  • Network connections
  • OS security events
  • Changes to SSH configuration
  • Changes to authorised users and keys

Patch:

  • Operating system
  • OpenSSH
  • Security agents
  • Monitoring agents
  • Any other software installed on the host

Keep the software footprint small so that patching remains manageable.

If the bastion is compromised, the question isn’t simply whether the bastion itself is affected.

You also need to assume the attacker will investigate what the bastion can reach.

That is why network segmentation and least privilege are just as important as SSH hardening.

14. Consider two bastions for production

A single bastion can become an availability problem.

If there is only one:

Administrators
      |
      v
  Bastion 1
      |
      v
 Internal servers

then maintenance or failure of that host removes administrative access.

For a production environment, two bastions can provide a more resilient design:

                    +-- Bastion 1 --+
Internet / VPN -----|               |---- Internal network
                    +-- Bastion 2 --+

You can use DNS, a load balancer or another appropriate mechanism to distribute access, depending on the design.

The two hosts should be configured consistently and monitored independently.

Don’t create a second bastion and then forget to apply the same security controls to it.

When a traditional bastion isn’t the right answer

A bastion is a useful pattern, but it isn’t automatically the best access architecture.

Consider an alternative when your platform already provides a managed access service.

AWS Systems Manager Session Manager

Session Manager can provide interactive access to managed nodes without opening inbound ports or maintaining traditional bastion hosts and SSH keys.

That can remove an entire server and its associated patching, hardening and internet exposure from the architecture.

There is an important caveat: SSH connections through Session Manager are carried through the Session Manager tunnel, and AWS notes that normal Session Manager session logging isn’t available for SSH port-forwarded sessions because the SSH data is encrypted inside the tunnel.

Azure Bastion

Azure Bastion provides SSH and RDP connectivity to Azure VMs without requiring the VMs themselves to have public IP addresses.

Azure also provides private-only Bastion deployments for architectures where the Bastion service itself should not be publicly reachable.

If all of the servers you are protecting are Azure VMs, it is worth comparing this approach with operating your own Linux bastion.

Other access platforms

Depending on the environment, other approaches include:

  • Site-to-site or client VPN
  • Zero-trust access platforms
  • Privileged access management
  • Identity-aware access proxies
  • Teleport
  • Tailscale
  • Cloud-native management services

The right choice depends on what you actually need to control: network access, SSH access, identity, privileged commands, session recording or all of them.

The bastion is a boundary, not just a jump host

A good bastion design has several layers working together:

  1. The network controls who can reach the bastion.
  2. SSH controls who can authenticate.
  3. Identity controls which users receive access.
  4. The firewall controls which internal systems the bastion can reach.
  5. ProxyJump provides the normal administrator workflow without copying private keys to the bastion.
  6. Logging records authentication and access activity.
  7. Monitoring detects unexpected behaviour.
  8. Regular testing proves that the intended restrictions actually work.

The SSH configuration is only one part of the design.

If the bastion has unrestricted access to the internal network, an administrator account has access to everything, logging is stored only on the bastion and the internal servers remain directly exposed to the internet, then putting the word “bastion” on the server hasn’t achieved very much.

Build the network restrictions first, make the SSH configuration match them, then test the whole path from the administrator’s workstation to the final server.

That is what turns a jump host into an actual access-control boundary.

Leave a Reply

Your email address will not be published. Required fields are marked *