SERVER SECURITY GUIDE

Locked Out of Your VPS After Changing SSH? How to Recover

Short answer: first find out whether the server is unreachable or is rejecting you. If a connection to the SSH port is refused or times out, suspect a firewall rule (including one added by fail2ban), a changed port, a stopped sshd or a broken sshd_config. If the connection opens and you are then turned away, suspect keys, file permissions, AllowUsers, disabled password login or PAM. Either way you repair it from outside SSH: your provider's web console, rescue mode, or the disk attached to another instance. Menus and names differ per provider, so follow your provider's own documentation for those steps. This guide covers the diagnosis, each repair, and a rollout routine that stops it happening again.

Quick win (10 minutes)

  • Run nc -vz -w 5 YOUR_SERVER_IP 22 (use your SSH port). Success means the network path and sshd are fine and the problem is authentication; refused or a timeout means the problem is before authentication.
  • Run ssh -v against the server and note where the output stops: that single fact narrows the cause more than anything else.
  • Open your provider's web console or rescue mode page now and confirm you can reach it, before you need it urgently.
  • From the console, run sudo sshd -t, sudo systemctl status ssh and sudo ufw status verbose to check the config, the service and the firewall.
  • Do not close any session that still works, and do not reboot or restore a snapshot until you know what you would lose.

First, find out what is actually broken

"I cannot log in" covers two very different failures, and the repair depends on which one you have. Work it out from your own machine before you touch anything on the server.

Step 1: does anything answer on the SSH port? The nc command can test a TCP port without sending any data. In the OpenBSD manual, -z means "only scan for listening daemons, without sending any data to them", -v makes the output more verbose and -w sets a timeout in seconds.

nc -vz -w 5 203.0.113.10 22

Use your real address and your real SSH port. There are three outcomes:

  • It reports success. The network path is open and something is listening. Your problem is authentication or a rule applied after the connection (keys, permissions, AllowUsers, PAM). Skip to the repair sections for those.
  • "Connection refused". Something answered but nothing accepted the connection. Typical causes are sshd not running, sshd listening on a different port than the one you tried, or a firewall rule that rejects rather than drops.
  • It hangs until the timeout. Nothing answered. The Nmap documentation describes a filtered port as one where packet filtering stops probes from getting through, and notes that filters that simply drop probes without responding are far more common than ones that return errors. So a timeout most often points to a firewall, either on the server or one that sits in front of it, or to a wrong address or a server that is down.

Note that nc flags differ between implementations. The flags above are from the OpenBSD version, so if yours rejects them, check its manual page.

Step 2: if the port is open, ask ssh what happens. Put the connection details in a Host block in ~/.ssh/config, which saves typing the user and address every time and gives you one place to change the port later:

Host myserver
    HostName 203.0.113.10
    User deploy
    Port 22
ssh -v -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes myserver

The ssh manual says -v prints debugging messages about progress, and repeating it raises the detail up to a maximum of three. IdentitiesOnly limits ssh to the identity you named instead of everything your agent holds. Read the output for the point where it stops: before the connection is established, during key exchange, or at the authentication step.

Step 3: rule out your own side. Try from a different network such as a phone hotspot, and from a different machine if you have one. If it works from there, your own address is being blocked, which points at an allow list, a firewall rule or a ban. If you only see a host key warning, that is a client-side matter covered later and it is not a lock-out.

Step 4: once you have a console, read the server. Check the service, what is actually listening, and the recent log:

sudo systemctl status ssh
sudo ss -tlnp | grep sshd
sudo journalctl -u ssh -e

The unit is called sshd on some distributions. ss -tlnp lists listening TCP sockets with numeric ports and the owning process, so you can see whether sshd is bound to the port you expect. journalctl -u ssh -e jumps to the end of that unit's log. On Debian family systems the authentication log may also be in /var/log/auth.log. The server's own log usually names the reason for a rejection.

Escape hatches, in order

You need a way in that does not depend on the SSH service you just broke. Providers offer some or all of the options below, under different names and in different menus. This guide does not describe any provider's interface, so follow your provider's own documentation for the exact steps. Try the options in this order, from least to most disruptive.

  1. Web console or VNC console. This gives you a login prompt on the server's virtual screen, so it works even if the network rules or sshd are broken. It needs a local account with a password. If none of your accounts has a local password, the console may not let you in, and you will need rescue mode or whatever your provider documents for resetting access.
  2. Rescue or recovery mode. The server boots a temporary system and you work on your own disk from there. This is the right tool when the installed system will not boot normally or you cannot log in at the console at all.
  3. Attach the disk to another instance. If the provider supports it, you attach the broken server's disk to a second server, mount it and edit the files directly. It is the same idea as rescue mode with a system you control.
  4. Restore a snapshot or backup. This is the last resort, because it throws away everything written after the snapshot. If you can, take a fresh snapshot before restoring anything, so you can still get at the data you would otherwise lose. A tested backup is what makes this option safe: see the 3-2-1 backup guide.

Be aware that some providers also offer a network firewall that is configured in their panel rather than on the server. If the port still times out after the server's own firewall looks correct, check that layer in your provider's documentation too.

If you work from a rescue system or a second instance, the pattern is always the same: find the partition, mount it, change the file, unmount, and boot back into the normal system as your provider describes.

lsblk -f
sudo mount /dev/YOUR_ROOT_PARTITION /mnt
sudo cp -a /mnt/etc/ssh/sshd_config /mnt/etc/ssh/sshd_config.broken
sudo nano /mnt/etc/ssh/sshd_config
sudo umount /mnt

lsblk -f lists block devices with their filesystems, and mount attaches the filesystem found on a device to the directory tree. cp -a copies while preserving attributes, so keep the original before you edit it. To run commands as if you were on the real system, sudo chroot /mnt starts a shell with /mnt as its root, so a path such as /etc/ssh resolves inside your own system. That helps with Include paths and with sshd -t. If the rescue environment lacks something that sshd needs and the error is about the environment rather than your config, validate after the first normal boot instead, from the console.

Repair a broken sshd_config

From the console, check the configuration first. The sshd manual describes -t as test mode: it only checks the validity of the configuration file and the sanity of the keys.

sudo sshd -t

If it reports an error, fix that line. If you made a backup before the change, restore it, test it and reload:

sudo cp -a /etc/ssh/sshd_config /etc/ssh/sshd_config.broken
sudo cp -a /etc/ssh/sshd_config.bak /etc/ssh/sshd_config
sudo sshd -t
sudo systemctl reload ssh

If a restart left sshd stopped, reload has nothing to reload, so use sudo systemctl start ssh instead and check systemctl status ssh. Remember that the unit may be named sshd. Our sshd_config reference covers the file itself.

The setting you changed may be overridden. The sshd_config manual states that for each keyword the first obtained value will be used. The Include directive takes glob patterns, processes matches in lexical order and looks up relative paths in /etc/ssh. Some distributions ship an Include line for /etc/ssh/sshd_config.d/ near the top of the main file, so a value in an included file can win over the one you wrote lower down. That is how "I turned password login off" can silently do nothing, and also how a forgotten drop-in file can lock you out when you thought the main file was the only source. Find every place a keyword is set, then ask sshd for the effective result:

grep -n '^Include' /etc/ssh/sshd_config
sudo grep -rniE 'passwordauthentication|permitrootlogin|allowusers|port|listenaddress' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/
sudo sshd -T | grep -iE '^(passwordauthentication|permitrootlogin|allowusers|port|listenaddress) '

The -T option prints the effective configuration and also performs the extra checks of -t. To see how Match blocks apply to a particular connection, add a connection specification with -C, for example sudo sshd -T -C user=deploy,addr=198.51.100.7,host=server. Without -C, sshd has no connection details to match against.

Two settings in the output deserve a second look. Port and ListenAddress: if sshd now listens on a port you did not open, or only on an address you cannot reach, you get refused connections even though the service is running. The manual notes that Port defaults to 22 and that without ListenAddress sshd listens on all local addresses. Change the file that wins, run sshd -t, reload, and keep the console open until a new connection works.

Repair a firewall that blocks SSH

If nc timed out or was refused and sshd is running and listening, the firewall is the next suspect. This often follows enabling a firewall without allowing SSH first, moving sshd to a new port without opening it, or restricting SSH to an address that has since changed. Our Linux firewall guide explains how ufw, nftables and iptables relate. From the console, look at what is enforced right now.

ufw. The ufw manual documents status numbered for a numbered list of rules. Then allow the port you really use:

sudo ufw status verbose
sudo ufw status numbered
sudo ufw allow 22/tcp
sudo ufw status numbered

If you moved SSH, allow that port instead (for example sudo ufw allow 2222/tcp). If you restricted SSH to one source address and your own address changed, add a rule for the new one, using the full syntax from the manual:

sudo ufw allow proto tcp from 198.51.100.7 to any port 22

If a deny rule for the port is already in place, delete it by number, and list the rules again before deleting a second one, because the numbers can shift:

sudo ufw status numbered
sudo ufw delete 3

The manual says --dry-run shows the changes without modifying anything, which is useful for a command you are unsure about. As an emergency measure, sudo ufw disable unloads the firewall and disables it on boot. That removes all protection, so use it only to get back in, then fix the rules and run sudo ufw enable again. The same manual notes that ufw prompts when you enable the firewall while running under ssh, and that enabling may drop existing connections such as ssh. Adding the SSH rule first avoids that. See also our ufw reference.

nftables. List the ruleset with handles, then insert an accept rule at the top or delete the offending rule by its handle, as shown in the nftables wiki:

sudo nft -a list ruleset
sudo nft insert rule inet filter input tcp dport 22 accept
sudo nft delete rule inet filter input handle 7

Adjust the family, table and chain names to match your listing, because inet filter input is only the layout used in our guide. If your rules are saved to a file that loads at boot, such as /etc/nftables.conf, fix the file too, otherwise the block returns at the next reboot.

iptables. Per the iptables manual, -S prints the rules of a chain, -L with -n -v --line-numbers lists them numerically with positions, -I inserts at a given position and -D deletes by number:

sudo iptables -S INPUT
sudo iptables -L INPUT -n -v --line-numbers
sudo iptables -I INPUT 1 -p tcp --dport 22 -j ACCEPT
sudo iptables -D INPUT 4

Order matters in iptables, which is why the accept rule goes in position 1 rather than at the end behind a drop. Delete by number only after a fresh listing. After you can connect again, test from a new terminal, then make the correct rules permanent in whatever mechanism your server uses to load them at boot.

Want your SSH and firewall rules reviewed?

A free, read-only server check looks at how SSH and your firewall are configured, without changing anything.

Get your free server check Talk to a security engineer

Repair keys, permissions, AllowUsers and password login

If nc succeeded but ssh says your login is not accepted, the cause is usually one of the four below. Our Permission denied (publickey) guide goes deeper on the key side.

Wrong permissions on ~/.ssh. The sshd manual says that if authorized_keys, the ~/.ssh directory or the home directory is writable by other users, the file could be modified or replaced, and sshd will not use it unless StrictModes is set to no. The sshd_config manual lists StrictModes as checking file modes and ownership of the user's files and home directory before accepting a login, and says it is on by default. Inspect the whole path, then fix it:

namei -l /home/deploy/.ssh/authorized_keys
sudo chmod go-w /home/deploy
sudo chmod 700 /home/deploy/.ssh
sudo chmod 600 /home/deploy/.ssh/authorized_keys
sudo chown -R deploy:deploy /home/deploy/.ssh

namei -l shows the owner and mode of every component of the path, so a directory writable by the wrong user stands out. Do not turn StrictModes off as a shortcut. The recommended permissions in the manual are read and write for the user and nothing for others on authorized_keys, and the same for the directory. See the authorized_keys reference.

AllowUsers excludes you. The manual says that when AllowUsers is specified, login is allowed only for user names that match one of its patterns, and that by default all users are allowed. A new AllowUsers line that lists only the other administrators locks you out, as does a DenyUsers entry, which takes precedence. AllowGroups and DenyGroups work the same way for groups. Check the effective values for your own user and address:

sudo sshd -T -C user=deploy,addr=198.51.100.7,host=server | grep -iE '^(allowusers|denyusers|allowgroups|denygroups) '

If your name is missing, add it (or remove the restriction), run sshd -t and reload. The AllowUsers reference explains the pattern syntax.

Password login disabled and no working key. This is the classic result of setting PasswordAuthentication to no before the key was confirmed. Per the manual, PasswordAuthentication defaults to yes, and PermitRootLogin defaults to prohibit-password, which disables password and keyboard-interactive login for root. So root never had password SSH by default. You have two ways out from the console. The cleaner one is to put your public key where sshd will read it. On your own machine, ssh-keygen -y -f ~/.ssh/id_ed25519 reads a private key file and prints the matching public key, which you can then append to the user's authorized_keys with the permissions above. The temporary one is to switch password login back on in the file that wins, test, reload, log in, install the key properly and switch it off again. Do not leave it on. If a drop-in file sets the value, change that file.

The key-only setup itself is described in our SSH hardening guide. Once a key login works from a new terminal, turn password authentication off again.

Repair a TOTP or PAM misconfiguration

Two-factor setups through PAM are the easiest way to lock yourself out, because the failure is in a file outside sshd_config. Our SSH two-factor guide describes the setup and has its own rollout order. Here is the recovery side.

The usual symptoms are a key that is accepted and then a verification code prompt that never accepts anything, or a user who never ran the enrolment command. The TOTP module reads the user's secret from the home directory, so a user without one is rejected unless the nullok option is present. The module's documentation describes nullok as letting users log in without a code if they have not set one up yet.

Fix it in this order, from least to most invasive:

  1. Go back to key-only login in sshd_config. The AuthenticationMethods keyword takes comma-separated lists, and the manual says successful authentication requires completing every method in at least one list. If it is set to publickey,keyboard-interactive, set it back to publickey, run sudo sshd -t and reload. This leaves PAM alone and restores the login you had before.
  2. Or let un-enrolled users in temporarily. Add nullok to the module line, have each user run the enrolment command, then remove nullok.
  3. Last resort: comment out the PAM line. In PAM, lines starting with # are comments, as the pam.d manual notes. Open the service file, normally /etc/pam.d/sshd, and put a # in front of the line that loads pam_google_authenticator.so.

The caveat for the last option is that you have just removed the second factor, and depending on what else the file includes, the login may now ask for a password instead. The manual for sshd_config also warns that with UsePAM enabled you should disable either PasswordAuthentication or KbdInteractiveAuthentication, because PAM keyboard-interactive login usually plays the same role as a password. Treat commenting the line as a temporary bridge: restore the line after enrolment, and test from a second terminal before closing the console. Do not edit one PAM file while your only session depends on it. If a code never works even for an enrolled user, check the server clock, since time-based codes depend on it. Keep the scratch codes you generated offline.

fail2ban bans and the host key warning

fail2ban banned your own address. After several failed attempts, for instance while you were fixing a key, fail2ban may ban your address in the firewall. From the console (or from a different network that is not banned), look at the jail and lift the ban. The fail2ban-client manual documents each command:

sudo fail2ban-client status
sudo fail2ban-client status sshd
sudo fail2ban-client set sshd unbanip 198.51.100.7

status shows the server status and, with a jail name, that jail's banned addresses. set JAIL unbanip IP unbans in that jail. unban IP removes the ban in all jails and in the database, and unban --all clears everything, which is rarely what you want on a server under constant scanning. To stop it recurring, add your fixed address or VPN exit to ignoreip in your jail configuration. The jail.conf manual describes it as a list of addresses not to ban, which may include a CIDR mask. The client can also add one at runtime with set JAIL addignoreip IP, but put it in the configuration so it survives a restart of your tooling. Be careful: an ignored address that an attacker can reach, such as a shared office network, weakens the protection. Setup and tuning are in our fail2ban guide, and if you are not sure why a ban did or did not happen, see fail2ban not banning and SSH brute force protection.

"Remote host identification has changed" is not a lock-out. This warning comes from your own computer. The ssh manual says that if a host's identity changed, ssh warns and disables password authentication to guard against spoofing and man-in-the-middle attacks, and StrictHostKeyChecking controls what happens with hosts whose key changed. It usually appears after you rebuild or reinstall a server, or use a rescue system that presents a different key. Do not bypass it blindly. Compare the fingerprint first. On the server console, the default hash for ssh-keygen -l is SHA256:

ssh-keygen -l -f /etc/ssh/ssh_host_ed25519_key.pub

If it matches what your client reports, remove the stale entry on your computer and reconnect. The -R option of ssh-keygen removes all keys for a host name from a known_hosts file, with an optional port written as [host]:port:

ssh-keygen -R 203.0.113.10
ssh-keygen -R "[203.0.113.10]:2222"

If it does not match and you did not rebuild the server, stop and treat it as a possible interception or a different machine at that address. See the escalation section.

Roll out SSH changes without locking yourself out

Most lock-outs are avoidable with a fixed routine. The two-factor guide has its own rollout order, and this section is consistent with it and adds a few more safeguards.

  1. Keep a second session open for the whole change. Never close the one that works until a new one has succeeded.
  2. Back up the config before you edit: sudo cp -a /etc/ssh/sshd_config /etc/ssh/sshd_config.bak. Remember files in /etc/ssh/sshd_config.d/ as well.
  3. Validate before applying. sudo sshd -t catches syntax errors and sudo sshd -T shows what will actually apply.
  4. Reload, do not restart. The sshd manual says sshd rereads its configuration on SIGHUP by executing itself again with the same options. sudo systemctl reload ssh is the service-level equivalent (the unit is sshd on some systems). Reload does not replace the second session; it only avoids a gap while the service restarts.
  5. Test from a new terminal with ssh -o ConnectTimeout=10 myserver. The ssh_config manual describes ConnectTimeout as a timeout in seconds, so a blocked port fails quickly instead of hanging.
  6. Schedule a rollback when the change could cut you off, such as a firewall rule or an authentication change. The at command runs a command later. Per its manual it reads commands from standard input or a file, runs them with /bin/sh, accepts times such as now + 10 minutes, lists pending jobs with atq and deletes them with atrm.
  7. Keep the console link handy, and log in to it once in advance so you know the credentials work.
sudo cp -a /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
echo 'cp -a /etc/ssh/sshd_config.bak /etc/ssh/sshd_config; systemctl reload ssh' | sudo at now + 10 minutes
sudo atq
# ... make the change, run sudo sshd -t, reload, test from a new terminal ...
sudo atrm JOB_NUMBER

If you are testing a firewall change, the safe revert for ufw is echo 'ufw disable' | sudo at now + 5 minutes, which, as described above, removes protection until you fix the rules. Cancel the job with atrm once you have proved the new rules work. If the at command is not installed, install it from your distribution before the change, not during it. Because a scheduled job fires on a timer, it can also undo a change you are still testing, so set the delay generously and check atq. Read the at manual for your system, since details differ between implementations.

For a full checklist of what else to put in place, see the SSH hardening guide, and keep PAM changes in the same routine.

When to escalate

Escalate to your provider's support when you have no console, no rescue mode and no disk attach option, when the console login fails for an account you know should work, when the disk is encrypted and you do not have the unlock method, or when a snapshot restore is the only path and you need advice on what it would overwrite. Give support the server identifier, the time of your last working login, what you changed and what you already tried, and do not ask them to run commands you have not read.

Escalate to a security response, not just a repair, when the lock-out was not your own change. Warning signs are a modified sshd_config that you did not edit, authorized_keys entries you do not recognise, a host key that changed with no rebuild, or logins in the log from addresses you do not know. In that case do not simply restore the file and move on. Preserve the logs, change credentials from a clean machine and follow our incident response guide.

If you would rather have a second pair of eyes on your SSH and firewall setup before the next change, the free check below is read-only and does not touch your configuration.

Frequently asked questions

How do I fix "Connection refused" on SSH when it worked yesterday?

Open your provider's console and check that sshd is running and listening on the port you are using, for example with systemctl status ssh and ss -tlnp. If it is, look at the firewall with ufw status verbose or nft list ruleset, because a rule may reject the port.

Why do I get "Permission denied (publickey)" after editing sshd_config?

The connection works but your key is not accepted. Common causes are an AllowUsers line that omits you, wrong permissions on your home directory, .ssh or authorized_keys, a drop-in file in /etc/ssh/sshd_config.d/ that wins over your setting, or password login turned off before a key was installed. Run sshd -T with -C for your user to see the effective settings.

Can I get back into my VPS if ufw locked me out?

Yes, if your provider offers a console or rescue mode. Log in there and run sudo ufw allow 22/tcp (or your SSH port), check with sudo ufw status numbered, and test from a new terminal. As a short-term emergency step, sudo ufw disable turns the firewall off, but re-enable it once SSH is allowed.

Should I use reload or restart after editing sshd_config?

Run sshd -t first, then reload. The sshd manual says sshd rereads its configuration when it receives SIGHUP, and systemctl reload ssh does this at the service level. Neither replaces keeping a second session open and testing from a new terminal.

Is the "REMOTE HOST IDENTIFICATION HAS CHANGED" warning a lock-out?

No, it is raised by your own computer, which has a different host key on record for that address. After a rebuild or reinstall it is expected. Compare the fingerprint with ssh-keygen -l -f on the server console, and if it matches, remove the old entry with ssh-keygen -R. If you did not rebuild the server and it does not match, treat it as suspicious.

How do I unban my own IP address from fail2ban?

From the console or another network, run sudo fail2ban-client set sshd unbanip followed by the address. To remove it from all jails use fail2ban-client unban followed by the address. Add your fixed address to ignoreip in the jail configuration to prevent a repeat.

What can I do if my provider has no console or rescue mode?

Without any access that does not depend on SSH, you cannot repair sshd from inside the server. Contact your provider's support and ask what recovery options exist for your plan. For the future, choose a provider and plan that give you out-of-band access before you need it.

How can I schedule an automatic rollback while I test an SSH change?

Back up the config, then queue a revert with the at command, for example by piping a cp and reload command to sudo at now + 10 minutes. Check it with atq and cancel it with atrm once the new setup is proven. Check that at is installed first.

Not sure where your server stands?

Get a free, read-only security check, or talk to a security engineer about managed protection.

Get your free server check Talk to a security engineer