Hardening SSH on Linux

Turning off password sign-in and root access, the sshd_config keys that matter, and how to apply them without locking yourself out.

Linux6 min read

On this page
  1. Get key sign-in working first
  2. Create a user to sign in as
  3. Find out where your settings actually live
  4. The settings that matter
  5. Apply the change without losing the server
  6. A timed rollback, for the changes you are least sure about
  7. Changing the SSH port
  8. If you do lock yourself out
  9. Related

Two changes do most of the work: sign in with a key instead of a password, and stop root signing in with a password. Everything else on this page is smaller than those two, and every one of them can lock you out of your own server if you apply it before you test it.

Get key sign-in working first

Nothing below is safe until a key works. Do this part on its own and confirm it, then come back for the config.

On your own machine, create a key if you do not have one:

Shell
ssh-keygen -t ed25519 -C "you@example.com"

Copy the public half to the server:

Shell
ssh-copy-id -i ~/.ssh/id_ed25519.pub root@203.0.113.10

If ssh-copy-id is not available, append the public key by hand on the server and fix the permissions, which SSH refuses to work around:

Shell
mkdir -p ~/.ssh
chmod 700 ~/.ssh
printf '%s\n' 'ssh-ed25519 AAAA...your key... you@example.com' >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

On RHEL, Alma, and Rocky, restore the SELinux labels afterwards or the daemon will ignore the file:

Shell
restorecon -R -v ~/.ssh

Now open a second terminal and prove the key works on its own, with passwords ruled out:

Shell
ssh -o PubkeyAuthentication=yes -o PasswordAuthentication=no root@203.0.113.10

If that gets you a shell, continue. If it asks for a password or is refused, fix that before changing anything on the server.

Create a user to sign in as

You need this only if you intend to stop root signing in entirely. Creating the account and granting it sudo is covered in Users, groups and sudo on Linux; the SSH half is giving that user the same key, with ownership and permissions preserved:

Shell
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy/

Test the new account in a second terminal before you go further, including sudo, because a user who cannot escalate cannot undo what you are about to do:

Shell
ssh deploy@203.0.113.10
sudo -v

Find out where your settings actually live

Current OpenSSH reads /etc/ssh/sshd_config and, from an Include line near the top of it, everything in /etc/ssh/sshd_config.d/. For most keywords the first value wins, so a drop-in file included at the top of the config beats anything you type further down the main file. Cloud images frequently ship a drop-in that sets PasswordAuthentication yes, and it is the usual reason a careful edit appears to do nothing.

Check whether the include exists, and what is in there:

Shell
grep -i '^include' /etc/ssh/sshd_config
ls -l /etc/ssh/sshd_config.d/

Then ask the daemon what it has actually resolved, as root. This is the only answer that counts:

Shell
sshd -T | grep -Ei 'permitrootlogin|passwordauthentication|pubkeyauthentication|kbdinteractive'

Put your changes in the file that wins. Either edit the drop-in that already sets the keyword, or add your own file in /etc/ssh/sshd_config.d/ with a name that sorts before it, then run sshd -T again to confirm the value changed.

The settings that matter

SettingValueWhat it does
PubkeyAuthenticationyesAllows key sign-in. Confirm it before turning passwords off.
PasswordAuthenticationnoStops password sign-in outright. This is the change that matters most.
KbdInteractiveAuthenticationnoCloses the other route to a password prompt. Older builds call this ChallengeResponseAuthentication. Leaving it on can hand sign-in back to PAM and prompt for a password anyway.
PermitRootLoginprohibit-passwordRoot can still sign in with a key but never with a password. Use no instead once another user with sudo is tested.
PermitEmptyPasswordsnoRefuses accounts with no password set.
MaxAuthTries3Drops the connection after three failed attempts, cutting the cost of a guessing run.
LoginGraceTime20Seconds an unauthenticated connection may sit open.
AllowUsers or AllowGroupsyour usersAnyone not listed cannot sign in at all, whatever their credentials. Adding yourself wrongly here locks you out.
X11ForwardingnoTurns off a feature a server almost never needs.

Leave UsePAM yes alone. Turning it off changes account and session handling well beyond SSH.

Apply the change without losing the server

Session 1, the one you edited fromSession 2, a brand new one1Keep this session openIt survives the reload, so it staysusable even if the new config is wrong.2sshd -tSilence means the config parses. Anerror names the file and line.3systemctl reload sshsshd on RHEL, Alma and Rocky. Existingconnections are not dropped.4ssh deploy@203.0.113.10The test. Until this works, assume theserver is unreachable.5It works: close session 1Only now, once a fresh session is in.It fails: fix it in session 1Session 1 is still root. Put the previousconfig back, reload, and test again.In session 1, the one you edited from1Keep this session openIt survives the reload, so it staysusable even if the new config is wrong.2sshd -tSilence means the config parses. An errornames the file and line.3systemctl reload sshsshd on RHEL, Alma and Rocky. Existingconnections are not dropped.In session 2, a new terminal4ssh deploy@203.0.113.10The test. Until this works, assume theserver is unreachable.5It works: close session 1Only now, once a fresh session is in.or, if it failsIt fails: fix it in session 1Session 1 is still root. Put the previousconfig back, reload, and test again.
Two sessions, and which one each step happens in. The numbers match the steps below. Session 1 stays open from start to finish, so whichever way step 4 goes, you still have a way to act. Illustration.
  1. Keep your current session open

    Do not close the terminal you edited from. An existing session survives a reload, so it stays usable even if the new config is wrong.

  2. Check the syntax

    Shell
    sshd -t

    Silence means the config parses. An error prints the file and line. Fix it before going any further.

  3. Reload the daemon

    On Debian and Ubuntu the unit is ssh:

    Shell
    systemctl reload ssh

    On RHEL, Alma, and Rocky it is sshd:

    Shell
    systemctl reload sshd

    Reload re-reads the config without dropping connections.

  4. Open a new session in a second terminal

    Shell
    ssh deploy@203.0.113.10

    This is the test. Until a fresh connection succeeds, assume the server is unreachable and do not close anything.

  5. Only now close the first session

    If the new session failed, the old one is still root on the machine: put the previous configuration back, reload, and test again before you go anywhere.

A timed rollback, for the changes you are least sure about

Schedule the old config to come back on its own, so a mistake repairs itself while you are still trying to reconnect:

Shell
cp /etc/ssh/sshd_config /root/sshd_config.bak
systemd-run --on-active=10min --timer-property=AccuracySec=1s \
  /bin/sh -c 'cp /root/sshd_config.bak /etc/ssh/sshd_config; systemctl reload ssh'

It prints the unit it created, something like run-r4f1a.timer. If the new session works, cancel the rollback:

Shell
systemctl stop run-r4f1a.timer

Back up and restore the drop-in file instead if that is where you made the change.

Changing the SSH port

Moving SSH off port 22 stops the constant background scanning from reaching it, which makes your logs readable. Be clear about what that is worth: it is noise reduction, not security. Anyone scanning your address for open ports finds the new one in seconds, and a service that would have fallen to a weak password on port 22 falls to the same password on port 2222. Key-only sign-in is the control that stops the attack; the port is housekeeping.

If you still want it, four things have to agree, and the order they are done in matters.

5You connect from a second terminalssh -p 2222 deploy@203.0.113.104The panel's rules, before the serverAn inbound rule for TCP 2222 gives the port its rate limitsand checks.Default for unlisted portsset to Deny all, and no rule:never arrives3The host firewall, ufw or firewalldAllow TCP 2222 before you remove 22.2222 not allowed: dropped onthe server2SELinux, on RHEL, Alma and Rockysemanage port -a -t ssh_port_t -p tcp 2222Not labelled: the daemonfails to bind1SSH listening on 2222ListenStream=2222 in a socket drop-in if ssh.socket isenabled, otherwise Port 2222 in the file that wins.Set in the wrong place:nothing on 22225You connect from a second terminalssh -p 2222 deploy@203.0.113.104The panel's rules, before the serverAn inbound rule for TCP 2222 gives theport its rate limits and checks.Default for unlisted ports set to Denyall, and no rule: never arrives3The host firewall, ufw or firewalldAllow TCP 2222 before you remove 22.2222 not allowed: dropped on the server2SELinux, on RHEL, Alma and Rockysemanage port -a -t ssh_port_t -p tcp 2222Not labelled: the daemon fails to bind1SSH listening on 2222ListenStream=2222 in a socket drop-in ifssh.socket is enabled, otherwise Port2222 in the file that wins.Set in the wrong place: nothing on 2222
The path a connection to the new port takes, from your terminal inwards, with each place numbered by the step below that sets it. The listener, SELinux and the host firewall each stop the connection if they are left on the old port; the panel's rule stops it only under Deny all, and otherwise adds the port's limits and checks. The steps start from the inside, and you connect last. Illustration.
  1. Find out whether systemd owns the port

    Shell
    systemctl is-enabled ssh.socket

    If that prints enabled, the socket accepts connections and starts the daemon, and Port in sshd_config is ignored. Set the port in a socket drop-in instead. Create /etc/systemd/system/ssh.socket.d/port.conf:

    Config
    [Socket]
    ListenStream=
    ListenStream=2222

    The empty ListenStream= clears the inherited value. Without that line you get both ports. Then:

    Shell
    systemctl daemon-reload
    systemctl restart ssh.socket

    If the command reports the socket is disabled or does not exist, which is the usual case on RHEL, Alma, and Rocky, set Port 2222 in the SSH config in the file that wins, as above.

  2. Tell SELinux about the port, on the RHEL family

    Shell
    dnf install -y policycoreutils-python-utils
    semanage port -a -t ssh_port_t -p tcp 2222

    Skip this and the daemon fails to bind. systemctl status sshd shows the error.

  3. Open the port in the host firewall

    Add the new port before you remove the old one. See Using a firewall on your Linux server.

  4. Add a port rule in the panel

    Give the new port its own inbound rule for TCP, so the port's rate limits and protocol checks apply to it. If Default for unlisted ports is set to Deny all, the connection never arrives without one. See Port rules. Keep the rule for port 22 until the new port is proven, then remove it.

  5. Connect on the new port from a second terminal

    Shell
    ssh -p 2222 deploy@203.0.113.10

If you run fail2ban, its sshd jail resolves port = ssh through /etc/services and will keep watching port 22. Set port = 2222 in the jail. See Setting up fail2ban.

If you do lock yourself out

Open Console access in the panel. The console is attached to the machine rather than to the network stack inside it, so it works when SSH does not. Sign in there, undo the change, run sshd -t, reload the daemon, and test from a normal terminal again.

Need a server, or a hand with one?

Pick a plan to deploy a server, or open a ticket in the panel. Support runs 24/7/365.