# SSH hardening: lock down OpenSSH on Ubuntu and Debian

> Harden OpenSSH beyond the basics: a drop-in config, allowed users, login limits, idle timeouts, a safe port change with socket activation and optional 2FA.

Difficulty: Intermediate\
Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13

[Securing a new server](/guides/secure-a-new-linux-server) already turns off root and password logins. This guide goes further: it limits who may log in, how many tries they get, how long idle sessions live, moves SSH to another port safely and adds optional two-factor authentication. Commands are for Ubuntu 24.04/26.04 and Debian 12/13.

## Before you start

- Key-based login works for your sudo user ([SSH keys](/guides/ssh-keys)).
- You have a second way in, such as the web console if your service page shows one.
- **Keep one SSH session open** throughout and test every change in a second terminal.

## Step 1: Write a drop-in configuration

OpenSSH uses the first value it reads for each option, and Ubuntu and Debian read `/etc/ssh/sshd_config.d/*.conf` before the rest of the main file. Put all your settings in one file that sorts first:

```bash
sudo nano /etc/ssh/sshd_config.d/00-hardening.conf
```

```text
# Who may log in, and how
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AllowUsers alex

# Fewer tries, shorter waits
MaxAuthTries 3
LoginGraceTime 30

# Close idle or dead sessions
ClientAliveInterval 300
ClientAliveCountMax 2

# Features most servers do not need
X11Forwarding no
AllowAgentForwarding no
```

> **Note**
>
> **Exception for Coolify:** servers managed by Coolify need `PermitRootLogin prohibit-password` instead of `no`, because Coolify connects as root with its own SSH key. Keep `PasswordAuthentication no`, so root can log in with a key only, never with a password, and add `root` to `AllowUsers` if you use it. See [install Coolify](/guides/install-coolify).

`AllowUsers` lists the accounts that may log in at all; separate several with spaces, or use `AllowGroups` with a group such as `sshusers`. Leave out `AllowAgentForwarding no` if you use agent forwarding on purpose.

## Step 2: Check and apply

```bash
sudo sshd -t
sudo sshd -T | grep -E 'permitrootlogin|passwordauthentication|allowusers|maxauthtries'
sudo systemctl restart ssh
```

`sshd -t` must print nothing. `sshd -T` shows the values in effect; if one is not what you wrote, another file sets it first.

**Verify:** a new login as `alex` works; a login as any other user is refused with `Permission denied`.

## Step 3: Change the port safely (optional)

A different port only reduces log noise, but if you want it, do it in this order. Allow the new port in the firewall **before** SSH moves:

```bash
sudo ufw allow 2222/tcp
```

Add the port to your drop-in file:

```text
Port 2222
```

Apply it:

**Ubuntu**

On Ubuntu, SSH is socket-activated: a generator reads `Port` from the configuration and sets up `ssh.socket`. Reload and restart the socket:

```bash
sudo sshd -t
sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
```
**Debian**

```bash
sudo sshd -t
sudo systemctl restart ssh
```

**Verify:** `sudo ss -tlnp | grep 2222` shows the listener, and `ssh -p 2222 alex@203.0.113.10` works from a new terminal. Only then remove the old rule: `sudo ufw delete allow OpenSSH`.

## Step 4: Use a hardware-backed key (optional)

With a FIDO2 security key, create a key that needs the device and a touch, and with `verify-required` also its PIN:

```bash
ssh-keygen -t ed25519-sk -O verify-required
```

Add the public key to `authorized_keys` as usual. Keep a second key or a console in case the device is lost.

## Step 5: Add a one-time code (optional)

Time-based codes add a second factor on top of the key. Install the PAM module and set it up **as your user** (not root):

```bash
sudo apt install libpam-google-authenticator
google-authenticator
```

Answer the questions, scan the QR code with your authenticator app and store the emergency codes safely. Then add this line to `/etc/pam.d/sshd`:

```text
auth required pam_google_authenticator.so
```

In your drop-in file, change `KbdInteractiveAuthentication no` to `yes` and require both factors:

```text
KbdInteractiveAuthentication yes
AuthenticationMethods publickey,keyboard-interactive
```

If you want SSH to ask only for the code and not also the account password, comment out the line `@include common-auth` in `/etc/pam.d/sshd`. Check with `sudo sshd -t` and restart SSH.

**Verify:** a new login asks for the key passphrase and then a `Verification code`. Test it before you close your other session.

## Step 6: Watch the logs

```bash
sudo journalctl -u ssh --since today | grep -E 'Accepted|Failed|Invalid'
```

`Accepted publickey for alex` lines are your logins; check that you recognise every source address. Many `Invalid user` lines from random addresses are normal background scanning and harmless with these settings. To block repeat offenders automatically, see [Fail2ban and CrowdSec](/guides/fail2ban-crowdsec).

## Troubleshooting

**`Permission denied` for your own user after Step 2.** The name in `AllowUsers` does not match, or the user logs in with a different name. Fix the file from your open session.

**The new port does not answer on Ubuntu.** You restarted `ssh` instead of reloading the generator and the socket. Run `sudo systemctl daemon-reload` and `sudo systemctl restart ssh.socket`.

**Two-factor prompts never appear.** `KbdInteractiveAuthentication` is still `no` in an earlier file, or `UsePAM` is off. Check with `sudo sshd -T | grep -E 'kbdinteractive|usepam|authenticationmethods'`.

**Locked out.** Use the console and follow [locked out after a firewall change](/guides/locked-out-after-firewall-change).

## Next steps

- Block repeated attempts: [Fail2ban and CrowdSec](/guides/fail2ban-crowdsec).
- Fix connection errors: [SSH connection problems](/guides/ssh-connection-problems).

## Frequently asked questions

### Is changing the SSH port worth it?

It cuts the noise of automated scans in your logs, but it is not protection on its own. Do it if you like quieter logs, after allowing the new port in the firewall, and keep key-only logins as the real defence.

### Why put the settings in sshd\_config.d instead of sshd\_config?

Package updates may replace the main file or ask you to merge changes. A drop-in file survives updates, and because OpenSSH uses the first value it reads, a file named 00-something takes precedence over later defaults.

### Does two-factor authentication for SSH replace keys?

No, it adds to them. With AuthenticationMethods publickey,keyboard-interactive a login needs both the key and a code. Keep a tested fallback, such as the web console, before you turn it on.

### How do I see the settings that are really in effect?

Run sudo sshd -T. It prints the final value of every option after all files have been read, which shows whether an earlier file overrides yours.

### What is a hardware-backed SSH key?

A key of type ed25519-sk lives on a FIDO2 security key and only works while the device is plugged in and touched. Even if your computer is compromised, the key cannot be copied.

---

Source: <https://hyperdc.com/guides/security/ssh-hardening>\
Updated: 2026-10-09
