Before We Begin

These are steps that I consider reasonable, and to the best of my knowledge are reasonably secure by current standards at the time of writing (20190122). It's important to note that none of these steps, in isolation or in totality are a panacea. Security works best in layers, this is just one that's an easy target on every *NIX host system. Depending on your deployment and your needs, this may be overkill, or insufficient, but has worked fantastically in reducing both the number of bots wasting CPU time, and the amount of CPU time they're able to waste when they're able to even get that far.

This article will consist primarily of snippets from a customized /etc/ssh/sshd_config, and ~/.ssh/config, with annotations both in comments and surrounding text as needed to explain why certain decisions are being made.
DISCLAIMER: I am not a security expert or a security researcher, just a guy that's kinda passionate about not getting pwned and willing to read the manual for services that I use heavily. While that may as well be a superpower now, don't blame me if you use this information and still get hacked.

Server Side: /etc/ssh/sshd_config

The first, and perhaps most obvious tweaks are to have sshd listen only on the addresses which you expect to be using for management, for example: if your deployment doesn't require logging in remotely through the WAN interface, don't listen on that interface! You can't attack a service that isn't there! The second step, which is stated frequently in any group that enjoys self-hosting services or other sysadmin/netadmin type projects, is to have sshd listen on a non-standard port, ideally over 1000. This makes it a bit more difficult to be sure that you're running sshd at all, but is far from foolproof as tools like nmap will still find your non-standard port. Such a configuration may look like this:

		## High, non-standard port
		Port 3285
		## Allow both IPv4 and IPv6 connections
		## Do not set this if you're not accepting both protocols
		## Enable only what you'll actually use
		AddressFamily any
		## This can be specified multiple times, once for each address you want to use
		## Leaving this as will bind to all addresses on the server, make sure you actually want that!


The next important step is restricting how clients can even connect to the server, not just restricting logins to use key-based authentication, which you'll hear from practically everyone, but exactly which methods of communicating with the server are considered valid. As before, you only want to enable what you'll actually be using, and in the interest of protecting whatever data is being transferred over your SSH connection, you'll want to find a good balance between being secure, fast, and computationally expensive. The easier it is to compute these steps, the easier it is to brute-force your session details.

		## Only use Ed25519 keys, they're currently the most secure, while also being small and relatively quick to use
		## This is what's hashed and presented to the clients as our "identity"
		HostKey /etc/ssh/ssh_host_ed25519_key
		## Do not allow an ephemeral session key to be older than 10 minutes
		RekeyLimit default 600
		KexAlgorithms curve25519-sha256,
		PukkeyAcceptedKeyTypes ssh-ed25519,

This is the path to the key used by the server to identify itself when a client tries to connect, this is what ssh stores in ~/.ssh/known_hosts after you type "yes" when connecting to a new server, and what gets compared when ssh warns you about a possible man-in-the-middle attack. We're only using ED25519 keys because of their efficiency and relative cryptographic strength over the usual RSA/DSA key types. This is also why we set PubKeyAcceptedKeyTypes to be exclusively an ed25519 key or an ed25519 certificate. The only downside here is that older sshd systems may not support this method, which is bad news if you just realized you're running such a system, upgrade as soon as you can, or find other ways to layer some security on top of it. This also means older ssh clients may not be able to log in anymore, so if you're uncertain, run ssh -Q key to get a list of supported key types from your client.
Since we're using Ed25519 only, this could at least in theory be omitted and allow the server to fall back to the defaults, but I prefer to be explicit wherever possible. This option specifies both how much data can be transmitted with a given session key, and how long any given session key can be used in seconds. Our setting of default 600 means that we'll allow the default amount of data to be transferred, but also ensure that a session key can only possibly be used for 10 minutes at a time. This should make it more time consuming to decrypt any potentially captured data, assuming such an adversary has the resources to break the other options specified.
As the name implies, these are the ciphers used to encrypt your connection to the server. We're using a rather restricted set of ciphers that should be secure, and expensive enough to be impractical to crack, should the traffic be captured. These options in particular have the additional benefit of limiting the ability of clients to connect, which for the purposes of securing your server is always beneficial.
This is perhaps one of the most important options in limiting server accessibility, as negotiating a KEX Algorithm is one of the first steps in establishing an ssh connection. By only allowing the use of curve 25519, we're limiting the number of viable clients significantly and in doing so, forcing most bots to fail their connection attempts before even getting to the point of checking their credentials.
Perhaps one of the most overloaded acronyms in technology, the MACs option specifies a list of Message Authentication Codes to be used, we're prioritizing the "umac" options here because they're both "strong" and fast, and while it's included, is our last resort MAC because of its susceptibility to length extension attacks that could potentially lead to information leakage. To my knowledge, there's no such weakness in UMAC options at this time.
This option restricts what public key types can actually be used to authenticate to the server, in our case, we're only allowing keys based on Ed25519, as stated above, this is due to their relative strength and impracticality to break, with the side effect of restricting valid logins to fairly recent clients.


So now we get into the portion of the configuration that actually dictates how to log into the server once the connection is established. By comparison, this portion is quite simple:

		LoginGraceTime 5
		PermitRootLogin no
		StrictModes yes
		MaxAuthTries 2
		MaxStartups 2:25:10
		PubkeyAuthentication yes
		AuthorizedKeysFile .ssh/authorized_keys
		PasswordAuthentication no
		PermitEmptyPasswords no
		AuthenticationMethods publickey
		FingerprintHash sha256
		AllowUsers user1 user2
		DenyUsers root admin student toor
This option simply dictates how long a user has to authenticate before the server drops their connection. The setting of "5" grants only 5 seconds before dropping an unauthenticated connection. This period of time does not really allow a human to enter a password, which is fine, since we no longer want to use passwords anyway. This also means that bots get dropped after 5s if they're able to connect at all with the previous settings.
Pretty self explanatory, we do not allow root to log in. Period. If you need root access, use sudo or doas, we do not want to allow god mode via remote login. I don't care how secure the rest of your network is, this spells game over for anyone that can actually get in.
Enabling this causes sshd to verify the file modes and ownership of the user's files and home directory before allowing logins. This should not be necssary, but I like to know that if my home directory or ssh directory gets messed up, sshd won't allow remote login.
Again, fairly self explanatory, this determines how many attempts you get to authenticate to the server before being dropped. Since bots can actually try to send a password in the 5s window allowed, we'll drop any connection on their second failed authentication attempt.
This is one of the more confusing options available to sshd, it controls how many concurrent unauthenticated sessions can be running at once, optionally with a rate to drop new unauthenticated connections. So with the setting of 2:25:10, after 2 concurrent unauthenticated connections, we'll drop 25% of inbound unauthenticated connections, with a maximum of 10 unauthenticated connections, after which we refuse to accept any new connections. Keep in mind that the maximum amount of time any singe unauthenticated connection will be alive is 5s.
As the option suggests, this enables public-key authentication.
This specifies the path for the file holding authorized key signatures, so when logging into a server it checks that user's ~/.ssh/authorized_keys for a matching signature.
As the same suggests, this controls the ability to log in using a password, this is only a viable authentication method when setting up key-based authentication, after you have key-based authentication configured, disable this and restart the server.
Never enable this. Allowing login with an empty password is a recipie for disaster.
This option controls which methods are allowed for authentication. We have it set for only public key authentication, so there's no possible way to authenticate via password.
This just explicitly states that we're using SHA2-256 for fingerprinting, rather than implicitly doing so via the defaults.
With the rest of the options set, this is not likely to be needed, but as stated earlier, it's better to be explicit and to layer your security. This list defines which users are allowed to log in via ssh, so you can restrict remote login to a subset of user accounts.
This is the exact opposite of the previous option, where we explicitly deny certain accounts from being able to log in remotely, regardless of the prior settings. This is technically redundant, but there is no harm in being explicit, especially in shared environments where you may need to communicate these decisions to other administrators.


These options don't really have a great category to describe them by, and aren't necessarily going to make a difference to your security compared to the other settings already covered, but they can help ensure that your system isn't being abused or indexed appropriately by attakers.

		DisableForwarding yes
		PermitTunnel no
		VersionAddendum TempleOS
This disables the ability to do any sort of forwarding, such as X11 forwarding. It's recommended particularly in deployments where there is no X server or other service that may be forwarded through ssh.
This permits/pevents forwarding of a tun(4) device, commonly used to create a VPN.
This controls the string appended to the version reported by sshd, it will generally default to your OS version, which is information you'd probably rather not expose to a potential attacker, why make their job easier? As such, I enjoy setting it to report some OS that can't possibly match the utility of the system, such as "TempleOS" or "Windows3.1". Doing so has the nice benefit of screwing up the results of nmap scans. On its own, it's pretty useless, but it's just one more way you can lie to a potential attacker about what your systems are capable of.

Client Side: ~/.ssh/config

As you may have expected, there's more to SSH than just the server-side hardening, we also have the client-side to worry about! The options here are not quite so important, since the client will negotiate with the server to determine how to connect, but there's a few things you can do to ensure you're connecting as intended.
NOTE: If you want to enforce a set of system-wide defaults for all ssh clients on a server, make these changes to /etc/ssh/ssh_config instead.

		Host *
		  IdentityFile ~/.ssh/id_ed25519
		  PasswordAuthentication no
		  ConnectionAttempts 2
		  ConnectTimeout 20
		  HashKnownHosts yes
		  VerifyHostKeyDNS yes
		  VisualHostKey yes
		  StrictHostKeyChecking yes
This defines settings for a given host, as provided on the command line, allowing you to define different settings for different hosts, which is most useful for specifying which user account to use and which port to connect on. The wildcard "*" applies to all hosts, unless a more specific configuration block exists.
The location of your private key when connecting to the specified host.
The same as on the server, but now on the client-side, we disable the use of passwords to authenticate.
How many times we try to connect to the server before giving up.
How many seconds we should wait before giving up on the connection.
Controls whether or not ~/.ssh/known_hosts is hashed, which is primarily useful for obfuscating what servers and on which portrs you're connecting to, if this is your sole line of defense, pray your network security is good enough to keep the bad actors out.
Controls whether or not we check for host key fingerprints in DNS TXT records, this is a great additional means of verification that you're connecting to the correct host, but it does require maintaining these records when hosts are added or keys are changed.
Controls whether or not ssh displays the ASCII-art version of the server's host key.

Final Notes

I should note that this page is nowhere near exhaustive in the customizations I've done even to my own sshd and ssh configuratinos, I'm just touching on the ones that are most important in hardening your server from remote logins. Please refer to your system's documentation on both of these programs to get more information on what can be done to make them work best for you in your given situation.