08 November 2021

How To Harden OpenSSH Client on Ubuntu 20.04

A previous version of this tutorial was written by Jamie Scaife.

Introduction

Linux servers are often administered remotely using SSH by connecting to an OpenSSH server, which is the default SSH server software used within Ubuntu, Debian, CentOS, FreeBSD, and most other Linux/BSD-based systems. Significant effort is put into securing the server-side aspect of SSH, as SSH acts as the entry into your server.

However, it is also important to consider security on the client-side, such as OpenSSH client.

OpenSSH client is the “client” side of SSH, also known as the ssh command. You can learn more about the SSH client-server model in SSH Essentials: Working with SSH Servers, Clients, and Keys.

When hardening SSH at the server side, the primary objective is to make it harder for malicious actors to access your server. However, hardening at the client side is very different, as instead you are working to defend and protect your SSH connection and client from various different threats, including:

  • Attackers on the network, known as “person-in-the-middle” attacks.
  • Compromised or malicious servers sending malformed data packets, nefarious control sequences, or large amounts of data to overload your client.
  • Human error, such as mistyping server addresses or configuration values.

In this tutorial, you will harden your Ubuntu 20.04 OpenSSH client in order to help ensure that outgoing SSH connections are as secure as possible.

Prerequisites

To complete this tutorial, you will need:

Once you have these ready, log in to your SSH client device as a non-root user to begin.

Step 1 — General Hardening

In this first step, you will implement some initial hardening configurations in order to improve the overall security of your SSH client.

The exact hardening configuration that is most suitable for your client depends heavily on your own threat model and risk threshold. However, the configuration described in this step is a general, all-round secure configuration that should suit the majority of users.

Many of the hardening configurations for OpenSSH client are implemented using the global OpenSSH client configuration file, which is located at /etc/ssh/ssh_config. In addition to this file, some configurations may also be set using the local SSH configuration file for your user, located at ~/.ssh/config.

to set the majority of the hardening options in this tutorial. Before continuing it is a good idea create a backup of your existing configuration file so that you can restore it in the unlikely event that something goes wrong.

Create a backup of the file using the following cp command:

sudo cp /etc/ssh/ssh_config /etc/ssh/ssh_config.bak
cp ~/.ssh/config ~/.ssh/config.bak

These commands will save backup copies of the files in their default location, but with the .bak extension added.

Note that your local SSH configuration file (~/.ssh/config) may not exist if you haven’t used it in the past. If this is the case, it can be safely ignored for now.

You can now open the global configuration file using nano or your favorite text editor to begin implementing the initial hardening measures:

sudo nano /etc/ssh/ssh_config

<$>[note] Note: The OpenSSH client configuration file includes many default options and configurations. Depending on your existing client setup, some of the recommended hardening options may already have been set. <$>

When editing your configuration file, some options may be commented out by default using a single hash character (#) at the start of the line. In order to edit these options, or enable the option, you’ll need to uncomment them by removing the hash.

First, disable X11 display forwarding if you are not using it by setting the following options:

[label /etc/ssh/ssh_config]
ForwardX11 <^>no<^>
ForwardX11Trusted <^>no<^>

X11 forwarding allows for the display of remote graphical applications over an SSH connection, however this is rarely used in practice. By disabling it, you can prevent potentially malicious or compromised servers from attempting to forward an X11 session to your client, which in some cases can allow for filesystem permissions to be bypassed, or for local keystrokes to be monitored.

Next, consider disabling SSH tunneling. SSH tunneling is quite widely used, so you may need to keep it enabled. You will generally know if you are using it. If it isn’t required for your particular setup, you can safely disable it as a further hardening measure:

[label /etc/ssh/ssh_config]
Tunnel <^>no<^>

You should also consider disabling SSH agent forwarding if it isn’t required, in order to prevent servers from requesting to use your local SSH agent to authenticate onward SSH connections:

[label /etc/ssh/ssh_config]
ForwardAgent <^>no<^>

In the majority of cases, your SSH client will be configured to use password authentication or public-key authentication when connecting to servers. However, OpenSSH client also supports other authentication methods, some of which are enabled by default. If these are not required, they can be disabled to further reduce the potential attack surface of your client:

[label /etc/ssh/ssh_config]
GSSAPIAuthentication <^>no<^>
HostbasedAuthentication <^>no<^>

If you’d like to know more about some of the additional authentication methods available within SSH, you may wish to review these resources:

OpenSSH client allows you to automatically pass custom environment variables when connecting to servers, for example, to set a language preference or configure terminal settings. However, if this isn’t required in your setup, you can prevent any variables being sent by ensuring that the SendEnv option is commented out or completely removed:

[label /etc/ssh/ssh_config]
# SendEnv

Finally, you should ensure that strict host key checking is enabled, to ensure that you are appropriately warned when the host key/fingerprint of a remote server changes, or when connecting to a new server for the first time:

[label /etc/ssh/ssh_config]
StrictHostKeyChecking ask

This will prevent you from connecting to a server when the known host key has changed, which could mean that the server has been rebuilt or upgraded, or could be indicative of an ongoing person-in-the-middle attack.

When connecting to a new server for the first time, your SSH client will ask you whether you want to accept the host key and save it in your ~/.ssh/known_hosts file. It’s important that you verify the host key before accepting it, which usually involves asking the server administrator or browsing the documentation for the service (in the case of GitHub/GitLab and other similar services).

Save and exit the file.

Now that you’ve completed your initial configuration file hardening, you should validate the syntax of your new configuration by running SSH in test mode:

ssh -G <^>.<^>

You can substitute the . with any hostname to test/simulate any settings contained within Match or Host blocks.

If your configuration file has a valid syntax, the options that will apply to that specific connection will be printed out. In the event of a syntax error, there will be output that describes the issue.

You do not need to restart any system services for your new configuration to take effect, although existing SSH sessions will need to be re-established if you want them to inherit the new settings.

In this step, you completed some general hardening of your OpenSSH client configuration file. Next, you’ll restrict the ciphers that are available for use in SSH connections.

Step 2 — Restricting Available Ciphers

OpenSSH supports a number of different cipher algorithms to encrypt data over a connection. In this step you will disable deprecated or legacy cipher suites within your SSH client.

Begin by opening your global configuration file in nano or your preferred text editor:

sudo nano /etc/ssh/ssh_config

Ensure that the existing Ciphers configuration line is commented out by prefixing it with a single hash (#).

Then, add the following to the top of the file:

[label /etc/ssh/ssh_config]
Ciphers -arcfour*,-*cbc

This will disable the legacy Arcfour ciphers, as well as all ciphers using Cipher Block Chaining (CBC), which are no longer recommended for use.

If there is a requirement to connect to systems that only support these legacy ciphers, you can explicitly re-enable the required ciphers for specific hosts by using a Match block. For example, to enable the 3des-cbc cipher for a specific legacy host, the following configuration could be used:

[label /etc/ssh/ssh_config]
Match host <^>legacy-server.your-domain<^>
  Ciphers +3des-cbc

Save and exit the file when you are done editing it. If you are using nano press CTRL+O and then ENTER to save the file, then CTRL+X to exit.

Finally, as in Step 1, test your SSH client configuration to check for any potential errors:

ssh -G <^>.<^>

If you have added a Match block to enable legacy ciphers for a specific host, you can also specifically target that configuration during the test by specifying the associated host address:

ssh -G <^>legacy-server.your-domain<^>

You’ve secured the ciphers available to your SSH client. Next, you will review the access permissions for files used by your SSH client.

Step 3 — Securing Configuration File and Private Key Permissions

In this step, you’ll lock down the permissions for your SSH client configuration files and private keys to help prevent accidental or malicious changes, or private key disclosure. This is especially useful when using a shared client device between multiple users.

By default on a fresh installation of Ubuntu, the OpenSSH client configuration file(s) are configured so that each user can only edit their own local configuration file (~/.ssh/config), and sudo/administrative access is required to edit the system-wide configuration (/etc/ssh/ssh_config).

However, in some cases, especially on systems that have been in existence for a long time, these configuration file permissions may have been accidentally modified or adjusted, so it’s best to reset them to make sure that the configuration is secure.

You can begin by checking the current permissions value for the system-wide OpenSSH client configuration file using the stat command, which you can use to show the status or files and/or filesystem objects:

stat -c "%a %A %U:%G" /etc/ssh/ssh_config

You use the -c argument to specify a custom output format.

<$>[note] Note: On some operating systems, such as macOS, you will need to use the -f option to specify a custom format rather than -c. <$>

In this case, the %A %a %U:%G options will print the permissions for the file in octal and human-readable format, as well as the user/group that owns the file.

This will output something similar to the following:

[secondary_label Output]
644 -rw-r--r-- root:root

In this case, the permissions are correct, root owns the file entirely, and only root has permission to write to/modify it.

<$>[note] Note: If you’d like to refresh your knowledge on Linux permissions before continuing, you may wish to review An Introduction to Linux Permissions. <$>

However, if your own output is different, you should reset the permissions back to the default using the following commands:

sudo chown root:root /etc/ssh/ssh_config
sudo chmod 644 /etc/ssh/ssh_config

If you repeat the stat command from earlier in this step, you will now receive the correct values for your system-wide configuration file.

Next, you can carry out the same checks for your own local SSH client configuration file, if you have one:

stat -c "%a %A %U:%G" ~/.ssh/config

Now the stat command should output the following:

[secondary_label Output]
644 -rw--r--r-- <^>user<^>:<^>user<^>

If the permissions for your own client configuration file permissions are any different, you should reset them using the following commands, similarly to earlier in the step:

chown <^>user<^>:<^>user<^> ~/.ssh/config
chmod 644 ~/.ssh/config

Next, you can check the permissions for each of the SSH private keys that you have within your ~/.ssh directory, as these files should only be accessible by yourself, and not any other users on the system.

Begin by printing the current permission and ownership values for each private key:

stat -c "%a %A %U:%G" ~/.ssh/<^>id_rsa<^>

This will output something similar to the following:

[secondary_label Output]
600 -rw------- <^>user<^>:<^>user<^>

It is extremely important that you properly lock down the permissions for your private key files, as failing to do so could allow other users of your device to steal them and access the associated servers or remote user accounts.

If the permissions aren’t properly configured, use the following commands on each private key file to reset them to the secure defaults:

chown <^>user<^>:<^>user<^> ~/.ssh/<^>id_rsa<^>
chmod 600 ~/.ssh/<^>id_rsa<^>

In this step, you assessed and locked down the file permissions for your SSH client configuration files and private keys. Next, you will implement an outbound allowlist to limit which servers your client is able to connect to.

Step 4 — Restricting Outgoing Connections Using a Host Allowlist

In this final step, you will implement an outgoing allowlist in order to restrict the hosts that your SSH client is able to connect to. This is especially useful for shared/multi-user systems, as well as SSH jump hosts or bastion hosts.

This security control is specifically designed to help protect against human error/mistakes, such as mistyped server addresses or hostnames. It can be easily bypassed by the user by editing their local configuration file, and so isn’t designed to act as a defense against malicious users/actors.

If you want to restrict outbound connections at the network level, the correct way to do this is using firewall rules. This is beyond the scope of this tutorial, but you can check out UFW Essentials: Common Firewall Rules and Commands.

However, if you want to add some additional fail-safes, then this security control may be of benefit to you.

It works by using a wildcard rule within your SSH client configuration file to null route all outbound connections, apart from those to specific addresses or hostnames. This means that if you were ever to accidentally mistype a server address, or attempt to connect to a server that you’re not supposed to, the request would be stopped immediately, giving you the opportunity to realize your mistake and take corrective action.

You can apply this at either the system-level (/etc/ssh/ssh_config) or using your local user configuration file (~/.ssh/config). In this example, we will use the local user configuration file.

Begin by opening the file using nano, creating it if it doesn’t already exist:

nano ~/.ssh/config

At the bottom of the file, add the following content, substituting in your own list of allowed IP addresses and hostnames:

[label ~/.ssh/config]
Match host !<^>203.0.113.1<^>,!<^>192.0.2.1<^>,!<^>server1.your-domain<^>,!<^>github.com<^>,*
  Hostname localhost

This configuration tells your SSH client that for any host or IP that is not included in the list, the client should substitute the name localhost instead before attempting to connect. The wildcard (*) option without a prefixed exclamation point option at the end of the Match line ensures that any host that is not included in the list will default to being null routed.

You must prefix IP addresses or hostnames with an exclamation point (!) since this tells SSH to not apply the null routing for the hostname or IP address. Additionally, you must use commas to separate each item in the list.

If you’re running an SSH server on your machine too, you may wish to use a hostname value other than localhost, as this will cause the null routed connections to be sent to your own local SSH server, which could be counterproductive or confusing. Any nullrouted hostname is acceptable, such as null, do-not-use, or disallowed-server.

Save and close the file once you’ve made your changes.

You can now test that the configuration is working by attempting to connect to a disallowed destination using your SSH client. For example:

ssh <^>disallowed.your-domain<^>

If the configuration is working properly, you will immediately receive an error similar to the following:

[secondary_label Output]
Cannot connect to localhost: connection refused

However, when you attempt to connect to an allowed destination, the connection will succeed as normal.

In this final step, you implemented some additional fail-safes to help protect against human error and mistakes when using your SSH client.

Conclusion

In this article you reviewed your OpenSSH client configuration and implemented various hardening measures.

This will have improved the security of your outgoing SSH connections, as well as helping to ensure that your local configuration files cannot be accidentally or maliciously modified by other users.

You may wish to review the manual pages for OpenSSH client and its associated configuration file to identify any potential further tweaks that you want to make:

Finally, if you want to harden OpenSSH at the server side too, check out How To Harden OpenSSH on Ubuntu 20.04.