Skip to content

How to harden the SSH server

This guide changes the OpenSSH server (sshd) so that only members of one group can log in, only with a public key, never directly as root, and with tighter limits on failed and unfinished logins. All the changes go in one drop-in file under /etc/ssh/sshd_config.d/, so they are easy to review, survive package upgrades that replace sshd_config, and can be undone by deleting one file.

Before you begin

  • You administer the server from an ordinary (non-root) account that can use sudo, that account already logs in over SSH with a key, and you are connected with it now. In the commands below, <USER> is that account's name.
  • If you currently log in only as root, first create such an account, give it sudo rights and a key, and check that you can log in with it and run sudo. This guide turns off root logins, so without that account you would be locked out.
  • You have a way back in that doesn't depend on SSH, such as a virtual machine console or out-of-band management, in case something goes wrong.

Keep your current session open

These changes can lock you out of the server. Keep the SSH session you make the changes in open until you have confirmed, from a second session, that you can still log in with your key. Reloading sshd doesn't disconnect existing sessions, so the first one remains your way back if the new settings refuse you. After the reload, only accounts in the ssh-users group can log in. Any other account that uses SSH today (other administrators, deployment or backup jobs, Git or SFTP users) stops working unless you add it in step 3.

Steps

  1. Check that the main configuration file reads the drop-in directory:

    $ sudo grep -i '^include' /etc/ssh/sshd_config
    Include /etc/ssh/sshd_config.d/*.conf
    

    The default sshd_config on Debian, Ubuntu, AlmaLinux, Rocky Linux and Arch has this line before any active (uncommented) setting. If the command prints nothing, the file was probably kept from an older release or replaced locally. Add the line above before the first active setting in /etc/ssh/sshd_config before you continue; otherwise the drop-in file is ignored.

    On Gentoo the line (with the path in quotes) is near the end of the file, after the commented-out defaults, with a note that all Match blocks must come after it. That's fine as long as those defaults stay commented out: a setting you uncomment above the Include line is read first and wins over the drop-in file.

  2. Find the accounts that log in over SSH today. List the accounts that have an authorized_keys file:

    $ getent passwd | while IFS=: read -r name _ _ _ _ home _; do if sudo test -s "$home/.ssh/authorized_keys"; then echo "$name"; fi; done
    labuser
    

    Then look for successful logins in the server log:

    $ sudo journalctl --since "30 days ago" --grep 'Accepted (publickey|password|keyboard-interactive) for'
    Nov 07 10:15:02 server sshd-session[1342]: Accepted publickey for labuser from 192.0.2.10 port 50122 ssh2: ED25519 SHA256:…
    
    $ sudo journalctl --since "30 days ago" --grep 'Accepted (publickey|password|keyboard-interactive) for'
    Nov 07 10:15:02 server sshd-session[1342]: Accepted publickey for labuser from 192.0.2.10 port 50122 ssh2: ED25519 SHA256:…
    
    $ sudo journalctl --since "30 days ago" --grep 'Accepted (publickey|password|keyboard-interactive) for'
    Nov 07 10:15:02 server sshd-session[1342]: Accepted publickey for labuser from 192.0.2.10 port 50122 ssh2: ED25519 SHA256:…
    

    With OpenRC, sshd logs to the system logger. With Gentoo's sysklogd configuration, authentication messages are in /var/log/auth.log:

    $ sudo grep -E 'Accepted (publickey|password|keyboard-interactive) for' /var/log/auth.log
    Nov  7 10:15:02 server sshd-session[1342]: Accepted publickey for labuser from 192.0.2.10 port 50122 ssh2: ED25519 SHA256:…
    

    With the systemd profile (not verified), use the journalctl command from the other tabs.

    Logs only go back as far as they are kept, so also ask the people and check the automation (deployment, backups, monitoring, Git or SFTP users) that connect to this server. Note every account that must keep SSH access, and whether it logs in with a password: those accounts need a key before you continue.

  3. Create a group for the accounts that may log in over SSH, and add your own account to it:

    $ sudo groupadd ssh-users
    $ sudo usermod -aG ssh-users <USER>
    $ id <USER>
    uid=1000(labuser) gid=1000(labuser) groups=1000(labuser),1001(ssh-users)
    

    Check that ssh-users appears in the output. Add every other account you noted in step 2 in the same way, with sudo usermod -aG ssh-users <ACCOUNT>, where <ACCOUNT> is the account's name. sshd looks up group membership for every new login, so you don't need to log out and back in for this to take effect.

  4. Create the file /etc/ssh/sshd_config.d/00-hardening.conf with this content:

    # Local SSH hardening. sshd uses the first value it reads for each
    # keyword, so this file is named to sort before other drop-ins.
    PermitRootLogin no
    PasswordAuthentication no
    KbdInteractiveAuthentication no
    AllowGroups ssh-users
    MaxAuthTries 3
    LoginGraceTime 30
    

    • PermitRootLogin no refuses logins as root by any method. Administrators log in as themselves and use sudo.
    • PasswordAuthentication no and KbdInteractiveAuthentication no together turn off passwords. Both are needed: with PAM, a password can also be accepted through keyboard-interactive authentication. Gentoo's default configuration, for example, disables the first but leaves the second enabled.
    • AllowGroups ssh-users refuses every account that isn't in the group, including system and service accounts.
    • MaxAuthTries 3 closes a connection after three failed attempts (the default is 6).
    • LoginGraceTime 30 gives a client 30 seconds to authenticate (the default is 120).

    For most keywords sshd keeps the first value it finds, and the Include line reads drop-ins in name order. The 00- prefix makes this file win over the distribution's own drop-ins (such as 50-cloud-init.conf or 50-redhat.conf) and over the settings in sshd_config that come after the Include line.

  5. Check the configuration for errors:

    sudo sshd -t
    

    No output means the configuration is valid. Don't reload the service until this command succeeds.

  6. Reload the SSH service so it reads the new configuration:

    sudo systemctl reload ssh
    

    On Debian and Ubuntu the service is called ssh. (On Ubuntu it's started through ssh.socket; reloading ssh is still correct for these settings.)

    sudo systemctl reload sshd
    
    sudo systemctl reload sshd
    
    sudo rc-service sshd reload
    

    With the systemd profile (not verified), run sudo systemctl reload sshd instead.

    Your current session stays connected.

  7. Keeping the first session open, open a second terminal and log in again from your workstation, where <SERVER> is the server's host name or address:

    ssh <USER>@<SERVER>
    

    If your agent holds several keys, ssh may use up the three attempts allowed by MaxAuthTries before it offers the right one, and fail with Too many authentication failures. In that case name the key, with ssh -o IdentitiesOnly=yes -i ~/.ssh/<KEY_FILE> <USER>@<SERVER>, where <KEY_FILE> is your private key file.

    If this works, close the first session only when you're done. If it doesn't, use the first session to fix the problem, as described in Troubleshooting.

Verify

  1. Show the settings sshd now uses:

    $ sudo sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication|allowgroups|maxauthtries|logingracetime) '
    logingracetime 30
    maxauthtries 3
    permitrootlogin no
    passwordauthentication no
    kbdinteractiveauthentication no
    allowgroups ssh-users
    

    sshd -T prints the configuration it would use, after reading every file. Newer OpenSSH releases print the keywords in mixed case (PermitRootLogin no), which is why the command ignores case. If a value is different, a setting read before your drop-in file wins; on Gentoo, check for uncommented settings above the Include line (see step 1).

  2. Confirm that the running server no longer offers passwords. On the server, try to log in to your own account without a key:

    $ ssh -o PubkeyAuthentication=no -o BatchMode=yes -o StrictHostKeyChecking=accept-new localhost true
    Warning: Permanently added 'localhost' (ED25519) to the list of known hosts.
    labuser@localhost: Permission denied (publickey).
    

    The list in brackets is the authentication methods the server still accepts. It should contain publickey and not password or keyboard-interactive. On RHEL, AlmaLinux and Rocky Linux it also contains gssapi-keyex,gssapi-with-mic (Kerberos), which those distributions enable by default.

Troubleshooting

sshd -t reports an error. It names the file and line. For example, after writing maybe instead of yes or no on line 4:

/etc/ssh/sshd_config.d/00-hardening.conf line 4: unsupported option "maybe".

Fix that line and run sudo sshd -t again. The running server is unaffected until you reload it.

A new session is refused after the reload. Look at the server log from your first session. On systemd distributions, run sudo journalctl -u ssh (Debian and Ubuntu) or sudo journalctl -u sshd (the others); on Gentoo with OpenRC and sysklogd, look in /var/log/auth.log. A line such as User alice from 203.0.113.10 not allowed because none of user's groups are listed in AllowGroups means the account isn't in ssh-users. If you can't find the cause, undo the change from the first session, then reload the service as in step 6:

sudo rm /etc/ssh/sshd_config.d/00-hardening.conf

Too many authentication failures. The client offered more keys than MaxAuthTries allows before reaching the right one; every key an agent holds counts as an attempt. Tell the client which key to use, with ssh -i ~/.ssh/<KEY_FILE> -o IdentitiesOnly=yes as in step 7, or with IdentityFile and IdentitiesOnly yes for that host in ~/.ssh/config.

A setting doesn't change in sshd -T. Another file, or a line in sshd_config above the Include line, sets the same keyword earlier. Find it with sudo grep -ri '<KEYWORD>' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/, where <KEYWORD> is the setting's name, and rename your drop-in so that it sorts first, or remove the other setting. A Match block in any file can also override settings for some connections; see the settings for a specific connection with sudo sshd -T -C user=<USER>,host=<SERVER>,addr=<ADDRESS>, where <ADDRESS> is the client's IP address.